@oh-my-pi/pi-coding-agent 16.1.13 → 16.1.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (129) hide show
  1. package/CHANGELOG.md +27 -0
  2. package/dist/cli.js +5547 -2759
  3. package/dist/types/config/settings-schema.d.ts +44 -14
  4. package/dist/types/eval/__tests__/julia-prelude.test.d.ts +1 -0
  5. package/dist/types/eval/backend-helpers.d.ts +23 -0
  6. package/dist/types/eval/executor-base.d.ts +118 -0
  7. package/dist/types/eval/index.d.ts +2 -0
  8. package/dist/types/eval/jl/executor.d.ts +44 -0
  9. package/dist/types/eval/jl/index.d.ts +11 -0
  10. package/dist/types/eval/jl/kernel.d.ts +28 -0
  11. package/dist/types/eval/jl/prelude.d.ts +1 -0
  12. package/dist/types/eval/jl/runtime.d.ts +22 -0
  13. package/dist/types/eval/kernel-base.d.ts +105 -0
  14. package/dist/types/eval/py/kernel.d.ts +3 -61
  15. package/dist/types/eval/rb/executor.d.ts +77 -0
  16. package/dist/types/eval/rb/index.d.ts +11 -0
  17. package/dist/types/eval/rb/kernel.d.ts +31 -0
  18. package/dist/types/eval/rb/prelude.d.ts +1 -0
  19. package/dist/types/eval/rb/runtime.d.ts +23 -0
  20. package/dist/types/eval/runtime-env.d.ts +24 -0
  21. package/dist/types/eval/types.d.ts +3 -3
  22. package/dist/types/extensibility/extensions/runner.d.ts +2 -15
  23. package/dist/types/extensibility/hooks/loader.d.ts +1 -25
  24. package/dist/types/extensibility/session-handler-types.d.ts +23 -0
  25. package/dist/types/jsonrpc/message-framing.d.ts +35 -0
  26. package/dist/types/mnemopi/embed-client.d.ts +7 -24
  27. package/dist/types/modes/components/chat-transcript-builder.d.ts +1 -1
  28. package/dist/types/modes/components/custom-editor.d.ts +11 -0
  29. package/dist/types/modes/components/selector-helpers.d.ts +53 -0
  30. package/dist/types/modes/interactive-mode.d.ts +0 -2
  31. package/dist/types/modes/theme/theme.d.ts +8 -1
  32. package/dist/types/modes/types.d.ts +0 -2
  33. package/dist/types/modes/utils/copy-targets.d.ts +1 -1
  34. package/dist/types/modes/utils/interactive-context-helpers.d.ts +14 -0
  35. package/dist/types/modes/utils/transcript-render-helpers.d.ts +54 -0
  36. package/dist/types/sdk.d.ts +1 -1
  37. package/dist/types/session/agent-session.d.ts +2 -2
  38. package/dist/types/stt/asr-client.d.ts +3 -29
  39. package/dist/types/subprocess/worker-client.d.ts +149 -0
  40. package/dist/types/subprocess/worker-runtime.d.ts +107 -0
  41. package/dist/types/tiny/title-client.d.ts +14 -34
  42. package/dist/types/tools/eval-backends.d.ts +6 -3
  43. package/dist/types/tools/eval-render.d.ts +6 -5
  44. package/dist/types/tools/eval.d.ts +13 -15
  45. package/dist/types/tools/index.d.ts +3 -3
  46. package/dist/types/tts/tts-client.d.ts +3 -28
  47. package/dist/types/tui/code-cell.d.ts +7 -0
  48. package/dist/types/utils/file-display-mode.d.ts +1 -1
  49. package/dist/types/web/parallel.d.ts +6 -0
  50. package/package.json +12 -12
  51. package/src/config/settings-schema.ts +50 -18
  52. package/src/config/settings.ts +5 -0
  53. package/src/dap/client.ts +13 -107
  54. package/src/eval/__tests__/julia-prelude.test.ts +77 -0
  55. package/src/eval/backend-helpers.ts +48 -0
  56. package/src/eval/executor-base.ts +425 -0
  57. package/src/eval/index.ts +2 -0
  58. package/src/eval/jl/executor.ts +540 -0
  59. package/src/eval/jl/index.ts +54 -0
  60. package/src/eval/jl/kernel.ts +235 -0
  61. package/src/eval/jl/prelude.jl +930 -0
  62. package/src/eval/jl/prelude.ts +3 -0
  63. package/src/eval/jl/runner.jl +634 -0
  64. package/src/eval/jl/runtime.ts +118 -0
  65. package/src/eval/js/index.ts +3 -14
  66. package/src/eval/kernel-base.ts +569 -0
  67. package/src/eval/py/executor.ts +43 -252
  68. package/src/eval/py/index.ts +9 -20
  69. package/src/eval/py/kernel.ts +29 -544
  70. package/src/eval/rb/executor.ts +504 -0
  71. package/src/eval/rb/index.ts +54 -0
  72. package/src/eval/rb/kernel.ts +230 -0
  73. package/src/eval/rb/prelude.rb +721 -0
  74. package/src/eval/rb/prelude.ts +3 -0
  75. package/src/eval/rb/runner.rb +474 -0
  76. package/src/eval/rb/runtime.ts +132 -0
  77. package/src/eval/runtime-env.ts +104 -0
  78. package/src/eval/types.ts +3 -3
  79. package/src/extensibility/extensions/runner.ts +4 -11
  80. package/src/extensibility/hooks/loader.ts +3 -21
  81. package/src/extensibility/session-handler-types.ts +21 -0
  82. package/src/internal-urls/docs-index.generated.txt +1 -1
  83. package/src/jsonrpc/message-framing.ts +142 -0
  84. package/src/lsp/client.ts +13 -109
  85. package/src/mnemopi/embed-client.ts +43 -198
  86. package/src/modes/components/agent-dashboard.ts +17 -40
  87. package/src/modes/components/chat-transcript-builder.ts +18 -102
  88. package/src/modes/components/custom-editor.ts +19 -0
  89. package/src/modes/components/extensions/extension-dashboard.ts +4 -13
  90. package/src/modes/components/extensions/extension-list.ts +16 -44
  91. package/src/modes/components/history-search.ts +4 -16
  92. package/src/modes/components/selector-helpers.ts +129 -0
  93. package/src/modes/components/settings-selector.ts +7 -5
  94. package/src/modes/components/tree-selector.ts +13 -18
  95. package/src/modes/controllers/event-controller.ts +3 -9
  96. package/src/modes/controllers/input-controller.ts +32 -54
  97. package/src/modes/interactive-mode.ts +5 -7
  98. package/src/modes/theme/theme.ts +35 -0
  99. package/src/modes/types.ts +0 -2
  100. package/src/modes/utils/copy-targets.ts +3 -2
  101. package/src/modes/utils/interactive-context-helpers.ts +27 -0
  102. package/src/modes/utils/transcript-render-helpers.ts +157 -0
  103. package/src/modes/utils/ui-helpers.ts +21 -126
  104. package/src/prompts/system/system-prompt.md +10 -10
  105. package/src/prompts/tools/bash.md +0 -1
  106. package/src/prompts/tools/eval.md +6 -4
  107. package/src/prompts/tools/find.md +0 -4
  108. package/src/prompts/tools/read.md +1 -2
  109. package/src/prompts/tools/replace.md +1 -1
  110. package/src/prompts/tools/search.md +0 -1
  111. package/src/sdk.ts +13 -7
  112. package/src/session/agent-session.ts +9 -7
  113. package/src/stt/asr-client.ts +35 -215
  114. package/src/stt/asr-worker.ts +29 -181
  115. package/src/subprocess/worker-client.ts +297 -0
  116. package/src/subprocess/worker-runtime.ts +277 -0
  117. package/src/task/executor.ts +4 -4
  118. package/src/tiny/title-client.ts +53 -219
  119. package/src/tiny/worker.ts +29 -180
  120. package/src/tools/eval-backends.ts +10 -3
  121. package/src/tools/eval-render.ts +17 -8
  122. package/src/tools/eval.ts +187 -22
  123. package/src/tools/index.ts +51 -28
  124. package/src/tts/tts-client.ts +38 -206
  125. package/src/tts/tts-worker.ts +23 -97
  126. package/src/tui/code-cell.ts +12 -1
  127. package/src/utils/file-display-mode.ts +2 -3
  128. package/src/web/parallel.ts +43 -42
  129. package/src/web/search/providers/parallel.ts +10 -99
@@ -0,0 +1,3 @@
1
+ import rubyPrelude from "./prelude.rb" with { type: "text" };
2
+
3
+ export const RUBY_PRELUDE = rubyPrelude;
@@ -0,0 +1,474 @@
1
+ # frozen_string_literal: false
2
+ # OMP Ruby runner — subprocess wrapper used by the coding-agent host.
3
+ #
4
+ # Mirrors the Python runner (eval/py/runner.py): a persistent Ruby process that
5
+ # speaks NDJSON over stdin/stdout. The host writes one JSON request per line
6
+ # ({id, code, cwd?, env?, silent?}) and the runner replies with frames:
7
+ # {type:"started", id}
8
+ # {type:"stdout"|"stderr", id, data}
9
+ # {type:"display"|"result", id, bundle} # bundle = Jupyter-style MIME hash
10
+ # {type:"error", id, ename, evalue, traceback:[...]}
11
+ # {type:"done", id, status, executionCount, cancelled}
12
+ # A {type:"exit"} request (or stdin EOF) shuts the runner down.
13
+ #
14
+ # Each cell is evaluated in the persistent TOPLEVEL_BINDING so local variables,
15
+ # methods, and constants survive across cells. The last expression's value is
16
+ # auto-displayed (like IRB) unless it is nil, an assignment, or a definition.
17
+ #
18
+ # Frame channel isolation: the original stdout is dup'd onto a private IO for
19
+ # protocol frames, then fd 1/fd 2 are repointed at internal pipes. Child
20
+ # processes that inherit stdout/stderr land in those pipes and drain threads
21
+ # re-emit their bytes as stdout/stderr frames instead of corrupting the NDJSON
22
+ # channel. Ruby-level writes go through $stdout/$stderr proxies that emit frames
23
+ # synchronously so they order correctly with display output.
24
+
25
+ require "json"
26
+
27
+ # ---------------------------------------------------------------------------
28
+ # Frame channel + fd capture setup
29
+ # ---------------------------------------------------------------------------
30
+
31
+ $__omp_out_mutex = Mutex.new
32
+ $__omp_raw_stderr = (STDERR.dup rescue STDERR)
33
+ $__omp_current_rid = nil
34
+ $__omp_capture_rid = nil
35
+ $__omp_exec_count = 0
36
+ $__omp_active_exec = 0
37
+ $__omp_silent = false
38
+
39
+ begin
40
+ $__omp_frame_io = STDOUT.dup
41
+ $__omp_frame_io.sync = true
42
+ __omp_stdout_cap_r, __omp_stdout_cap_w = IO.pipe
43
+ STDOUT.reopen(__omp_stdout_cap_w)
44
+ STDOUT.sync = true
45
+ __omp_stdout_cap_w.close
46
+ $__omp_stdout_capture_read = __omp_stdout_cap_r
47
+ rescue StandardError
48
+ $__omp_frame_io = STDOUT
49
+ ($__omp_frame_io.sync = true) rescue nil
50
+ $__omp_stdout_capture_read = nil
51
+ end
52
+
53
+ begin
54
+ __omp_stderr_cap_r, __omp_stderr_cap_w = IO.pipe
55
+ STDERR.reopen(__omp_stderr_cap_w)
56
+ STDERR.sync = true
57
+ __omp_stderr_cap_w.close
58
+ $__omp_stderr_capture_read = __omp_stderr_cap_r
59
+ rescue StandardError
60
+ $__omp_stderr_capture_read = nil
61
+ end
62
+
63
+ # Protect the protocol channel from user code: read requests on a private dup of
64
+ # the original stdin, then repoint fd 0 at /dev/null so a user `gets`/`STDIN.gets`
65
+ # inside a cell sees EOF instead of consuming the next JSON request.
66
+ begin
67
+ $__omp_proto_stdin = STDIN.dup
68
+ STDIN.reopen(File.open(File::NULL, "r"))
69
+ rescue StandardError
70
+ $__omp_proto_stdin = STDIN
71
+ end
72
+
73
+ # ---------------------------------------------------------------------------
74
+ # Frame writer + helpers (top-level private methods, available to user code)
75
+ # ---------------------------------------------------------------------------
76
+
77
+ def __omp_scrub(str)
78
+ s = str.to_s
79
+ begin
80
+ s = s.encoding == Encoding::UTF_8 ? s : s.encode(Encoding::UTF_8, invalid: :replace, undef: :replace)
81
+ rescue StandardError
82
+ s = s.dup.force_encoding(Encoding::UTF_8)
83
+ end
84
+ s.valid_encoding? ? s : s.scrub("\uFFFD")
85
+ end
86
+
87
+ def __omp_emit(frame)
88
+ line =
89
+ begin
90
+ JSON.generate(frame)
91
+ rescue StandardError
92
+ JSON.generate(
93
+ "type" => (frame["type"] || "stdout"),
94
+ "id" => frame["id"],
95
+ "data" => "<unserializable frame>\n",
96
+ )
97
+ end
98
+ $__omp_out_mutex.synchronize do
99
+ $__omp_frame_io.write(line)
100
+ $__omp_frame_io.write("\n")
101
+ $__omp_frame_io.flush
102
+ end
103
+ rescue StandardError
104
+ nil
105
+ end
106
+
107
+ def __omp_run_id
108
+ $__omp_current_rid
109
+ end
110
+
111
+ def __omp_emit_stream(kind, text)
112
+ rid = $__omp_current_rid
113
+ if rid.nil?
114
+ ($__omp_raw_stderr.write(text) rescue nil)
115
+ return
116
+ end
117
+ __omp_emit("type" => kind, "id" => rid, "data" => __omp_scrub(text))
118
+ end
119
+
120
+ def __omp_emit_display(bundle, kind = "display")
121
+ rid = $__omp_current_rid
122
+ return if rid.nil?
123
+ __omp_emit("type" => kind, "id" => rid, "bundle" => bundle)
124
+ end
125
+
126
+ def __omp_emit_status(op, data = {})
127
+ status = { "op" => op.to_s }
128
+ data.each { |k, v| status[k.to_s] = v }
129
+ __omp_emit_display({ "application/x-omp-status" => status }, "display")
130
+ end
131
+
132
+ # Build a Jupyter-style MIME bundle for a value. Strings render as plain text,
133
+ # Hash/Array render as JSON (plus a text/plain repr) so the model sees structure,
134
+ # and anything else falls back to its inspect string. Objects may opt into a
135
+ # richer bundle by defining `to_omp_mime` returning a Hash of mime => value.
136
+ def __omp_mime_bundle(value)
137
+ case value
138
+ when String
139
+ { "text/plain" => __omp_scrub(value) }
140
+ when Hash, Array
141
+ safe = begin
142
+ JSON.parse(JSON.generate(value))
143
+ rescue StandardError
144
+ nil
145
+ end
146
+ if safe.nil?
147
+ { "text/plain" => __omp_scrub(value.inspect) }
148
+ else
149
+ { "application/json" => safe, "text/plain" => __omp_scrub(value.inspect) }
150
+ end
151
+ when nil
152
+ { "text/plain" => "nil" }
153
+ else
154
+ if value.respond_to?(:to_omp_mime)
155
+ mime = (value.to_omp_mime rescue nil)
156
+ mime.is_a?(Hash) ? mime : { "text/plain" => __omp_scrub(value.inspect) }
157
+ else
158
+ { "text/plain" => __omp_scrub(value.inspect) }
159
+ end
160
+ end
161
+ end
162
+
163
+ def __omp_present(value, kind = "display")
164
+ __omp_emit_display(__omp_mime_bundle(value), kind)
165
+ end
166
+
167
+ # ---------------------------------------------------------------------------
168
+ # User stdout/stderr proxies — emit typed frames for the current request.
169
+ # ---------------------------------------------------------------------------
170
+
171
+ class OmpStreamProxy
172
+ def initialize(kind, io, fileno)
173
+ @kind = kind
174
+ @io = io
175
+ @fileno = fileno
176
+ end
177
+
178
+ def write(*args)
179
+ total = 0
180
+ args.each do |arg|
181
+ s = arg.to_s
182
+ next if s.empty?
183
+ total += s.bytesize
184
+ __omp_emit_stream(@kind, s)
185
+ end
186
+ total
187
+ end
188
+
189
+ def print(*args)
190
+ args.each { |a| write(a) }
191
+ nil
192
+ end
193
+
194
+ def <<(obj)
195
+ write(obj)
196
+ self
197
+ end
198
+
199
+ def puts(*args)
200
+ if args.empty?
201
+ write("\n")
202
+ else
203
+ args.each do |arg|
204
+ if arg.is_a?(Array)
205
+ arg.empty? ? write("\n") : puts(*arg)
206
+ else
207
+ s = arg.to_s
208
+ write(s.end_with?("\n") ? s : "#{s}\n")
209
+ end
210
+ end
211
+ end
212
+ nil
213
+ end
214
+
215
+ def printf(fmt, *args)
216
+ write(format(fmt, *args))
217
+ nil
218
+ end
219
+
220
+ def write_nonblock(s, *)
221
+ write(s)
222
+ end
223
+
224
+ def flush; self; end
225
+ def sync; true; end
226
+ def sync=(value); value; end
227
+ def tty?; false; end
228
+ def isatty; false; end
229
+ def fileno; @fileno; end
230
+ def to_io; @io; end
231
+ def closed?; false; end
232
+ def fsync; 0; end
233
+ def external_encoding
234
+ (@io.external_encoding rescue Encoding::UTF_8)
235
+ end
236
+ end
237
+
238
+ # ---------------------------------------------------------------------------
239
+ # fd-1/fd-2 capture drains (child-process stdout/stderr) + parent watchdog
240
+ # ---------------------------------------------------------------------------
241
+
242
+ def __omp_start_capture_drain(io, kind)
243
+ return if io.nil?
244
+ Thread.new do
245
+ loop do
246
+ chunk =
247
+ begin
248
+ io.readpartial(65_536)
249
+ rescue EOFError, IOError, Errno::EBADF
250
+ break
251
+ rescue StandardError
252
+ break
253
+ end
254
+ next if chunk.nil? || chunk.empty?
255
+ rid = $__omp_capture_rid
256
+ if rid.nil?
257
+ ($__omp_raw_stderr.write(chunk) rescue nil)
258
+ else
259
+ __omp_emit("type" => kind, "id" => rid, "data" => __omp_scrub(chunk))
260
+ end
261
+ end
262
+ end
263
+ end
264
+
265
+ def __omp_start_parent_watchdog
266
+ return unless RUBY_PLATFORM !~ /mswin|mingw|cygwin/
267
+ return unless Process.respond_to?(:ppid)
268
+ original = (Process.ppid rescue 0)
269
+ return if original <= 1
270
+ Thread.new do
271
+ loop do
272
+ begin
273
+ Process.exit!(0) if Process.ppid != original
274
+ rescue StandardError
275
+ break
276
+ end
277
+ sleep 10
278
+ end
279
+ end
280
+ end
281
+
282
+ # ---------------------------------------------------------------------------
283
+ # Signal handling — SIGINT raises Interrupt only while a cell is executing.
284
+ # ---------------------------------------------------------------------------
285
+
286
+ def __omp_install_idle_sigint
287
+ Signal.trap("INT", "IGNORE") rescue nil
288
+ end
289
+
290
+ def __omp_install_exec_sigint
291
+ Signal.trap("INT", "DEFAULT") rescue nil
292
+ end
293
+
294
+ def __omp_begin_exec
295
+ $__omp_active_exec += 1
296
+ __omp_install_exec_sigint
297
+ end
298
+
299
+ def __omp_end_exec
300
+ $__omp_active_exec -= 1 if $__omp_active_exec > 0
301
+ __omp_install_idle_sigint if $__omp_active_exec.zero?
302
+ end
303
+
304
+ # ---------------------------------------------------------------------------
305
+ # Per-request runtime (cwd + managed env) + auto-result suppression
306
+ # ---------------------------------------------------------------------------
307
+
308
+ OMP_MANAGED_ENV_KEYS = %w[
309
+ PI_SESSION_FILE
310
+ PI_ARTIFACTS_DIR
311
+ PI_TOOL_BRIDGE_URL
312
+ PI_TOOL_BRIDGE_TOKEN
313
+ PI_TOOL_BRIDGE_SESSION
314
+ PI_EVAL_LOCAL_ROOTS
315
+ ].freeze
316
+
317
+ def __omp_apply_request_runtime(req)
318
+ cwd = req["cwd"]
319
+ if cwd.is_a?(String) && !cwd.empty?
320
+ (Dir.chdir(cwd) rescue nil)
321
+ $LOAD_PATH.delete(cwd)
322
+ $LOAD_PATH.unshift(cwd)
323
+ end
324
+ env = req["env"]
325
+ if env.is_a?(Hash)
326
+ OMP_MANAGED_ENV_KEYS.each do |key|
327
+ next unless env.key?(key)
328
+ value = env[key]
329
+ if value.is_a?(String)
330
+ ENV[key] = value
331
+ elsif value.nil?
332
+ ENV.delete(key)
333
+ end
334
+ end
335
+ end
336
+ end
337
+
338
+ # Last value-bearing AST node types we should NOT auto-display (statements /
339
+ # definitions, mirroring IPython's "only display a trailing expression"). Falls
340
+ # back to displaying any non-nil value when the AST is unavailable.
341
+ OMP_NON_DISPLAY_NODES = %i[
342
+ LASGN IASGN GASGN CVASGN DASGN OP_ASGN OP_CDECL CDECL MASGN CASGN
343
+ DEFN DEFS CLASS MODULE SCLASS ALIAS UNDEF
344
+ ].freeze
345
+
346
+ def __omp_ast_last(node)
347
+ return nil unless node.is_a?(RubyVM::AbstractSyntaxTree::Node)
348
+ case node.type
349
+ when :SCOPE
350
+ __omp_ast_last(node.children[2])
351
+ when :BLOCK
352
+ kids = node.children.compact
353
+ kids.empty? ? nil : __omp_ast_last(kids.last)
354
+ else
355
+ node
356
+ end
357
+ end
358
+
359
+ def __omp_should_display_result?(src)
360
+ return true unless defined?(RubyVM::AbstractSyntaxTree)
361
+ node =
362
+ begin
363
+ RubyVM::AbstractSyntaxTree.parse(src)
364
+ rescue StandardError, SyntaxError
365
+ return true
366
+ end
367
+ last = __omp_ast_last(node)
368
+ return true if last.nil?
369
+ !OMP_NON_DISPLAY_NODES.include?(last.type)
370
+ end
371
+
372
+ # ---------------------------------------------------------------------------
373
+ # Request dispatch
374
+ # ---------------------------------------------------------------------------
375
+
376
+ def __omp_emit_error(rid, exc, name_override = nil)
377
+ ename = name_override || exc.class.name
378
+ evalue = (exc.message.to_s rescue "")
379
+ backtrace = (exc.backtrace || [])
380
+ user_tb = backtrace.select { |l| l.include?("(eval)") }
381
+ user_tb = backtrace.first(20) if user_tb.empty?
382
+ traceback = ["#{ename}: #{evalue}"]
383
+ user_tb.each { |line| traceback << " #{line}" }
384
+ __omp_emit(
385
+ "type" => "error",
386
+ "id" => rid,
387
+ "ename" => ename,
388
+ "evalue" => __omp_scrub(evalue),
389
+ "traceback" => traceback.map { |l| __omp_scrub(l) },
390
+ )
391
+ end
392
+
393
+ def __omp_handle_request(req)
394
+ rid = req["id"].to_s
395
+ $__omp_current_rid = rid
396
+ $__omp_capture_rid = rid
397
+ $__omp_silent = req["silent"] == true
398
+ $__omp_exec_count += 1
399
+ count = $__omp_exec_count
400
+ __omp_emit("type" => "started", "id" => rid)
401
+
402
+ status = "ok"
403
+ cancelled = false
404
+ begin
405
+ begin
406
+ __omp_apply_request_runtime(req)
407
+ src = req["code"].to_s
408
+ rescue Exception => e # rubocop:disable Lint/RescueException
409
+ __omp_emit_error(rid, e)
410
+ __omp_emit("type" => "done", "id" => rid, "status" => "error", "executionCount" => count, "cancelled" => false)
411
+ return
412
+ end
413
+
414
+ __omp_begin_exec
415
+ begin
416
+ value = TOPLEVEL_BINDING.eval(src, "(eval)")
417
+ unless $__omp_silent || value.nil? || !__omp_should_display_result?(src)
418
+ __omp_present(value, "result")
419
+ end
420
+ rescue Interrupt => e
421
+ cancelled = true
422
+ status = "error"
423
+ __omp_emit_error(rid, e, "Interrupt")
424
+ rescue SystemExit => e
425
+ status = "error"
426
+ __omp_emit_error(rid, e)
427
+ rescue Exception => e # rubocop:disable Lint/RescueException
428
+ status = "error"
429
+ __omp_emit_error(rid, e)
430
+ ensure
431
+ __omp_end_exec
432
+ end
433
+
434
+ __omp_emit("type" => "done", "id" => rid, "status" => status, "executionCount" => count, "cancelled" => cancelled)
435
+ ensure
436
+ $__omp_capture_rid = nil if $__omp_capture_rid == rid
437
+ $__omp_current_rid = nil
438
+ end
439
+ end
440
+
441
+ # ---------------------------------------------------------------------------
442
+ # Main loop
443
+ # ---------------------------------------------------------------------------
444
+
445
+ def __omp_main
446
+ $stdout = OmpStreamProxy.new("stdout", STDOUT, 1)
447
+ $stderr = OmpStreamProxy.new("stderr", STDERR, 2)
448
+ __omp_install_idle_sigint
449
+ __omp_start_parent_watchdog
450
+ __omp_start_capture_drain($__omp_stdout_capture_read, "stdout")
451
+ __omp_start_capture_drain($__omp_stderr_capture_read, "stderr")
452
+
453
+ $__omp_proto_stdin.each_line do |raw|
454
+ line = raw.strip
455
+ next if line.empty?
456
+ req =
457
+ begin
458
+ JSON.parse(line)
459
+ rescue JSON::ParserError => e
460
+ __omp_emit(
461
+ "type" => "error",
462
+ "id" => "",
463
+ "ename" => "ProtocolError",
464
+ "evalue" => "Invalid JSON request: #{e.message}",
465
+ "traceback" => [],
466
+ )
467
+ next
468
+ end
469
+ break if req.is_a?(Hash) && req["type"] == "exit"
470
+ __omp_handle_request(req) if req.is_a?(Hash)
471
+ end
472
+ end
473
+
474
+ __omp_main
@@ -0,0 +1,132 @@
1
+ /**
2
+ * Ruby runtime resolution utilities.
3
+ *
4
+ * Resolves the Ruby interpreter for the local kernel and filters the
5
+ * environment to a safe allowlist before exposing it to user cell code. Much
6
+ * simpler than the Python sibling — Ruby has no venv layout to detect — but it
7
+ * mirrors the same allowlist/denylist + explicit-interpreter shape.
8
+ */
9
+ import { createEnvFilter, enumerateRuntimes, resolveExplicitPath, resolveRuntime } from "../runtime-env";
10
+
11
+ const DEFAULT_ENV_ALLOWLIST = [
12
+ "PATH",
13
+ "HOME",
14
+ "USER",
15
+ "LOGNAME",
16
+ "SHELL",
17
+ "LANG",
18
+ "LC_ALL",
19
+ "LC_CTYPE",
20
+ "LC_MESSAGES",
21
+ "TERM",
22
+ "TERM_PROGRAM",
23
+ "TERM_PROGRAM_VERSION",
24
+ "TMPDIR",
25
+ "TEMP",
26
+ "TMP",
27
+ "XDG_CACHE_HOME",
28
+ "XDG_CONFIG_HOME",
29
+ "XDG_DATA_HOME",
30
+ "XDG_RUNTIME_DIR",
31
+ "SSH_AUTH_SOCK",
32
+ "SSH_AGENT_PID",
33
+ "LD_LIBRARY_PATH",
34
+ "DYLD_LIBRARY_PATH",
35
+ ];
36
+
37
+ const WINDOWS_ENV_ALLOWLIST = [
38
+ "APPDATA",
39
+ "COMPUTERNAME",
40
+ "COMSPEC",
41
+ "HOMEDRIVE",
42
+ "HOMEPATH",
43
+ "LOCALAPPDATA",
44
+ "NUMBER_OF_PROCESSORS",
45
+ "OS",
46
+ "PATH",
47
+ "PATHEXT",
48
+ "PROCESSOR_ARCHITECTURE",
49
+ "PROCESSOR_IDENTIFIER",
50
+ "PROGRAMDATA",
51
+ "PROGRAMFILES",
52
+ "PROGRAMFILES(X86)",
53
+ "PROGRAMW6432",
54
+ "SESSIONNAME",
55
+ "SYSTEMDRIVE",
56
+ "SYSTEMROOT",
57
+ "TEMP",
58
+ "TMP",
59
+ "USERDOMAIN",
60
+ "USERPROFILE",
61
+ "USERNAME",
62
+ "WINDIR",
63
+ ];
64
+
65
+ const DEFAULT_ENV_DENYLIST = [
66
+ "OPENAI_API_KEY",
67
+ "ANTHROPIC_API_KEY",
68
+ "GOOGLE_API_KEY",
69
+ "GEMINI_API_KEY",
70
+ "OPENROUTER_API_KEY",
71
+ "PERPLEXITY_API_KEY",
72
+ "PERPLEXITY_COOKIES",
73
+ "EXA_API_KEY",
74
+ "AZURE_OPENAI_API_KEY",
75
+ "MISTRAL_API_KEY",
76
+ ];
77
+
78
+ // Ruby version managers and gem layout live behind these prefixes; passing them
79
+ // through lets `bundle`/`gem`/rbenv/asdf-shimmed code resolve consistently.
80
+ const DEFAULT_ENV_ALLOW_PREFIXES = ["LC_", "XDG_", "PI_", "GEM_", "BUNDLE", "RBENV_", "RUBY", "CHRUBY_", "ASDF_"];
81
+
82
+ export interface RubyRuntime {
83
+ /** Path to the ruby executable. */
84
+ rubyPath: string;
85
+ /** Filtered environment variables. */
86
+ env: Record<string, string | undefined>;
87
+ }
88
+
89
+ export const filterEnv = createEnvFilter({
90
+ allowList: DEFAULT_ENV_ALLOWLIST,
91
+ windowsAllowList: WINDOWS_ENV_ALLOWLIST,
92
+ denyList: DEFAULT_ENV_DENYLIST,
93
+ allowPrefixes: DEFAULT_ENV_ALLOW_PREFIXES,
94
+ });
95
+
96
+ /**
97
+ * Resolve an explicitly configured interpreter (`ruby.interpreter`) into a
98
+ * runtime, bypassing discovery. Does not probe the executable — callers must
99
+ * check it actually runs. `~` expands to the home directory and relative paths
100
+ * resolve against `cwd`.
101
+ */
102
+ export function resolveExplicitRubyRuntime(
103
+ interpreter: string,
104
+ cwd: string,
105
+ baseEnv: Record<string, string | undefined>,
106
+ ): RubyRuntime {
107
+ const rubyPath = resolveExplicitPath(interpreter, cwd);
108
+ return { rubyPath, env: { ...baseEnv } };
109
+ }
110
+
111
+ /**
112
+ * Enumerate candidate Ruby runtimes in priority order. With an explicit
113
+ * interpreter that is the only candidate; otherwise the first `ruby` on PATH.
114
+ */
115
+ export function enumerateRubyRuntimes(
116
+ cwd: string,
117
+ baseEnv: Record<string, string | undefined>,
118
+ interpreter?: string,
119
+ ): RubyRuntime[] {
120
+ return enumerateRuntimes(cwd, baseEnv, "ruby", (rubyPath, env) => ({ rubyPath, env }), interpreter);
121
+ }
122
+
123
+ /**
124
+ * Resolve the highest-priority Ruby runtime. Throws when none exists.
125
+ */
126
+ export function resolveRubyRuntime(
127
+ cwd: string,
128
+ baseEnv: Record<string, string | undefined>,
129
+ interpreter?: string,
130
+ ): RubyRuntime {
131
+ return resolveRuntime(cwd, baseEnv, "ruby", (rubyPath, env) => ({ rubyPath, env }), interpreter);
132
+ }
@@ -0,0 +1,104 @@
1
+ /**
2
+ * Generic interpreter environment-filtering and runtime-resolution helpers
3
+ * shared by the per-language eval runtime modules (jl/runtime, rb/runtime).
4
+ */
5
+ import * as os from "node:os";
6
+ import * as path from "node:path";
7
+ import { $which } from "@oh-my-pi/pi-utils";
8
+
9
+ export const CASE_INSENSITIVE_ENV = process.platform === "win32";
10
+
11
+ // Secret-shaped names that must never leak into eval cells even when they fall
12
+ // under a broad allow-prefix.
13
+ export const SECRET_KEY_PATTERN =
14
+ /API[_-]?KEY|APIKEY|SECRET|TOKEN|PASSWORD|PASSWD|CREDENTIAL|ACCESS[_-]?KEY|PRIVATE[_-]?KEY/i;
15
+
16
+ export interface EnvFilterOptions {
17
+ allowList: string[];
18
+ windowsAllowList: string[];
19
+ denyList: string[];
20
+ allowPrefixes: string[];
21
+ }
22
+
23
+ /**
24
+ * Creates an environment filter function based on the provided allowlists, denylists, and prefixes.
25
+ */
26
+ export function createEnvFilter(
27
+ options: EnvFilterOptions,
28
+ ): (env: Record<string, string | undefined>) => Record<string, string | undefined> {
29
+ const normalizedAllowList = new Set(
30
+ [...options.allowList, ...options.windowsAllowList].map(key => (CASE_INSENSITIVE_ENV ? key.toUpperCase() : key)),
31
+ );
32
+ const normalizedDenyList = new Set(options.denyList.map(key => (CASE_INSENSITIVE_ENV ? key.toUpperCase() : key)));
33
+ const normalizedAllowPrefixes = CASE_INSENSITIVE_ENV
34
+ ? options.allowPrefixes.map(prefix => prefix.toUpperCase())
35
+ : options.allowPrefixes;
36
+
37
+ return (env: Record<string, string | undefined>): Record<string, string | undefined> => {
38
+ const filtered: Record<string, string | undefined> = {};
39
+ for (const key in env) {
40
+ const value = env[key];
41
+ if (value === undefined) continue;
42
+ const normalizedKey = CASE_INSENSITIVE_ENV ? key.toUpperCase() : key;
43
+ if (normalizedDenyList.has(normalizedKey)) continue;
44
+ if (normalizedAllowList.has(normalizedKey)) {
45
+ filtered[normalizedKey === "PATH" ? "PATH" : key] = value;
46
+ continue;
47
+ }
48
+ if (SECRET_KEY_PATTERN.test(normalizedKey)) continue;
49
+ if (normalizedAllowPrefixes.some(prefix => normalizedKey.startsWith(prefix))) {
50
+ filtered[key] = value;
51
+ }
52
+ }
53
+ return filtered;
54
+ };
55
+ }
56
+
57
+ /**
58
+ * Resolve an explicitly configured interpreter path, expanding `~` to the home directory.
59
+ */
60
+ export function resolveExplicitPath(interpreter: string, cwd: string): string {
61
+ const expanded =
62
+ interpreter === "~"
63
+ ? os.homedir()
64
+ : interpreter.startsWith("~/")
65
+ ? path.join(os.homedir(), interpreter.slice(2))
66
+ : interpreter;
67
+ return path.isAbsolute(expanded) ? expanded : path.resolve(cwd, expanded);
68
+ }
69
+
70
+ /**
71
+ * Enumerates candidate runtimes in priority order.
72
+ */
73
+ export function enumerateRuntimes<T>(
74
+ cwd: string,
75
+ baseEnv: Record<string, string | undefined>,
76
+ binaryName: string,
77
+ createRuntime: (executablePath: string, env: Record<string, string | undefined>) => T,
78
+ interpreter?: string,
79
+ ): T[] {
80
+ if (interpreter) {
81
+ const executablePath = resolveExplicitPath(interpreter, cwd);
82
+ return [createRuntime(executablePath, baseEnv)];
83
+ }
84
+ const systemPath = $which(binaryName);
85
+ return systemPath ? [createRuntime(systemPath, baseEnv)] : [];
86
+ }
87
+
88
+ /**
89
+ * Resolves the highest-priority runtime. Throws when none exists.
90
+ */
91
+ export function resolveRuntime<T>(
92
+ cwd: string,
93
+ baseEnv: Record<string, string | undefined>,
94
+ binaryName: string,
95
+ createRuntime: (executablePath: string, env: Record<string, string | undefined>) => T,
96
+ interpreter?: string,
97
+ ): T {
98
+ const [runtime] = enumerateRuntimes(cwd, baseEnv, binaryName, createRuntime, interpreter);
99
+ if (!runtime) {
100
+ const displayName = binaryName.charAt(0).toUpperCase() + binaryName.slice(1);
101
+ throw new Error(`${displayName} executable not found on PATH`);
102
+ }
103
+ return runtime;
104
+ }