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.



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

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

Instance Attribute Details

#runnerObject



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

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

Class Method Details

.capture_osascriptObject



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

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



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

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

.execute_applescript(source) ⇒ Object



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

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

.execute_jxa(script) ⇒ Object



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

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

.execute_omnifocus_script(script_path, args: nil) ⇒ Object



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

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

.execute_omnifocus_source(source, args: nil) ⇒ Object



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

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

.reset!Object



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

def reset!
  @default = new
end

.resolve_script_path(script_path) ⇒ Object



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

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

.runnerObject



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

def runner = default.runner

.runner=(runner) ⇒ Object



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

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

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



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

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.



122
123
124
125
126
127
# File 'lib/omnifocus_mcp/infrastructure/script_runner.rb', line 122

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.



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

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.



92
93
94
95
96
# File 'lib/omnifocus_mcp/infrastructure/script_runner.rb', line 92

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.



58
59
60
61
# File 'lib/omnifocus_mcp/infrastructure/script_runner.rb', line 58

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`.



79
80
81
82
83
84
85
86
# File 'lib/omnifocus_mcp/infrastructure/script_runner.rb', line 79

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<String>) is prepended as a `const argv = […]` block before the script body.



68
69
70
71
72
73
# File 'lib/omnifocus_mcp/infrastructure/script_runner.rb', line 68

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.



114
115
116
117
118
# File 'lib/omnifocus_mcp/infrastructure/script_runner.rb', line 114

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.



101
102
103
104
105
106
107
# File 'lib/omnifocus_mcp/infrastructure/script_runner.rb', line 101

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