Class: OmnifocusMcp::Infrastructure::ScriptRunner

Inherits:
Object
  • Object
show all
Defined in:
lib/omnifocus_mcp/infrastructure/script_runner.rb

Overview

Runs OmniFocus automation scripts (JXA, OmniJS, AppleScript) via osascript.

Instances accept an injectable runner so unit specs can supply a fake that returns canned [stdout, stderr, status] triples without invoking osascript. Class methods delegate to a default singleton for compatibility with the previous Utils::ScriptExecution API.

Constant Summary collapse

OMNIFOCUS_SCRIPTS_DIR =
File.expand_path("../utils/omnifocus_scripts", __dir__).freeze
STDOUT_PREVIEW_LIMIT =
200

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(runner: nil) ⇒ ScriptRunner

Returns a new instance of ScriptRunner.



47
48
49
# File 'lib/omnifocus_mcp/infrastructure/script_runner.rb', line 47

def initialize(runner: nil)
  @runner = runner
end

Instance Attribute Details

#runnerObject



51
# File 'lib/omnifocus_mcp/infrastructure/script_runner.rb', line 51

def runner = @runner ||= method(:capture_osascript).to_proc

Class Method Details

.capture_osascriptObject



42
# File 'lib/omnifocus_mcp/infrastructure/script_runner.rb', line 42

def capture_osascript(...) = default.capture_osascript(...)

.defaultObject



26
# File 'lib/omnifocus_mcp/infrastructure/script_runner.rb', line 26

def default = @default ||= new

.escape_content(content) ⇒ Object



40
# File 'lib/omnifocus_mcp/infrastructure/script_runner.rb', line 40

def escape_content(content) = default.escape_content(content)

.execute_applescript(source) ⇒ Object



38
# File 'lib/omnifocus_mcp/infrastructure/script_runner.rb', line 38

def execute_applescript(source) = default.execute_applescript(source)

.execute_jxa(script) ⇒ Object



35
# File 'lib/omnifocus_mcp/infrastructure/script_runner.rb', line 35

def execute_jxa(script) = default.execute_jxa(script)

.execute_omnifocus_script(script_path, args: nil) ⇒ Object



37
# File 'lib/omnifocus_mcp/infrastructure/script_runner.rb', line 37

def execute_omnifocus_script(script_path, args: nil) = default.execute_omnifocus_script(script_path, args: args)

.execute_omnifocus_source(source, args: nil) ⇒ Object



36
# File 'lib/omnifocus_mcp/infrastructure/script_runner.rb', line 36

def execute_omnifocus_source(source, args: nil) = default.execute_omnifocus_source(source, args: args)

.reset!Object



27
# File 'lib/omnifocus_mcp/infrastructure/script_runner.rb', line 27

def reset! = @default = new

.resolve_script_path(script_path) ⇒ Object



41
# File 'lib/omnifocus_mcp/infrastructure/script_runner.rb', line 41

def resolve_script_path(script_path) = default.resolve_script_path(script_path)

.runnerObject



29
# File 'lib/omnifocus_mcp/infrastructure/script_runner.rb', line 29

def runner = default.runner

.runner=(runner) ⇒ Object



31
32
33
# File 'lib/omnifocus_mcp/infrastructure/script_runner.rb', line 31

def runner=(runner)
  default.runner = runner
end

.with_temp_script(content:, prefix:, ext:) ⇒ Object



39
# File 'lib/omnifocus_mcp/infrastructure/script_runner.rb', line 39

def with_temp_script(content:, prefix:, ext:, &) = default.with_temp_script(content:, prefix:, ext:, &)

Instance Method Details

#capture_osascript(*argv) ⇒ Object

Runs osascript (or a test double) with an optional timeout so a hung automation call cannot block the MCP server indefinitely.



119
120
121
122
123
124
# File 'lib/omnifocus_mcp/infrastructure/script_runner.rb', line 119

def capture_osascript(*argv)
  timeout_sec = Config.script_timeout_sec
  return Open3.capture3(*argv) unless timeout_sec

  capture_osascript_with_timeout(*argv, timeout_sec:)
end

#escape_content(content) ⇒ Object

Escape a string for safe embedding in a JXA template literal.



107
# File 'lib/omnifocus_mcp/infrastructure/script_runner.rb', line 107

def escape_content(content) = JsEmbed.template_literal(content)

#execute_applescript(source) ⇒ Object

Execute a raw AppleScript source string via osascript.

Returns the [stdout, stderr, status] triple from the runner so callers can surface stderr/status without re-running the script.



89
90
91
92
93
# File 'lib/omnifocus_mcp/infrastructure/script_runner.rb', line 89

def execute_applescript(source)
  with_temp_script(content: source, prefix: "applescript", ext: "applescript") do |path|
    runner.call("osascript", path)
  end
end

#execute_jxa(script) ⇒ Object

Execute a JXA script (string source) and return Result with the parsed JSON value.



55
56
57
58
# File 'lib/omnifocus_mcp/infrastructure/script_runner.rb', line 55

def execute_jxa(script)
  run_jxa_source_result(source: script, prefix: "jxa_script")
    .and_then { |stdout| parse_jxa_output(stdout) }
end

#execute_omnifocus_script(script_path, args: nil) ⇒ Object

Execute an OmniJS script from disk inside OmniFocus.

script_path may be a real filesystem path or an @scriptName.js shorthand that resolves against OMNIFOCUS_SCRIPTS_DIR.



76
77
78
79
80
81
82
83
# File 'lib/omnifocus_mcp/infrastructure/script_runner.rb', line 76

def execute_omnifocus_script(script_path, args: nil)
  # Force UTF-8: bundled OmniJS files may contain non-ASCII bytes; the
  # platform default of US-ASCII would otherwise raise inside the
  # regex-based escape pass.
  File.read(resolve_script_path(script_path), encoding: Encoding::UTF_8).then do |source|
    execute_omnifocus_source(source, args:)
  end
end

#execute_omnifocus_source(source, args: nil) ⇒ Object

Execute an OmniJS script source (string) inside OmniFocus via app.evaluateJavascript. Returns Result with the parsed JSON value.

args (Array) is prepended as a const argv = [...] block before the script body.



65
66
67
68
69
70
# File 'lib/omnifocus_mcp/infrastructure/script_runner.rb', line 65

def execute_omnifocus_source(source, args: nil)
  wrap_omnifocus_source(source:, args:).then do |wrapped|
    run_jxa_source_result(source: wrapped, prefix: "jxa_wrapper")
      .and_then { |stdout| parse_omnifocus_output(stdout) }
  end
end

#resolve_script_path(script_path) ⇒ Object

Resolve @scriptName.js to an absolute path inside the gem's bundled OmniJS directory. Plain paths pass through unchanged.



111
112
113
114
115
# File 'lib/omnifocus_mcp/infrastructure/script_runner.rb', line 111

def resolve_script_path(script_path)
  return script_path unless script_path.start_with?("@")

  File.join(OMNIFOCUS_SCRIPTS_DIR, script_path[1..])
end

#with_temp_script(content:, prefix:, ext:) ⇒ Object

Materialize content to a tempfile, yield its path to the block, and guarantee cleanup (even on exception). Uses Tempfile.create, which removes the file when the block exits.



98
99
100
101
102
103
104
# File 'lib/omnifocus_mcp/infrastructure/script_runner.rb', line 98

def with_temp_script(content:, prefix:, ext:)
  Tempfile.create([prefix, ".#{ext}"]) do |file|
    file.write(content)
    file.flush
    yield file.path
  end
end