@oh-my-pi/pi-coding-agent 16.1.12 → 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.
- package/CHANGELOG.md +27 -0
- package/dist/cli.js +5650 -2860
- package/dist/types/config/settings-schema.d.ts +44 -14
- package/dist/types/cursor.d.ts +9 -9
- package/dist/types/eval/__tests__/julia-prelude.test.d.ts +1 -0
- package/dist/types/eval/backend-helpers.d.ts +23 -0
- package/dist/types/eval/executor-base.d.ts +118 -0
- package/dist/types/eval/index.d.ts +2 -0
- package/dist/types/eval/jl/executor.d.ts +44 -0
- package/dist/types/eval/jl/index.d.ts +11 -0
- package/dist/types/eval/jl/kernel.d.ts +28 -0
- package/dist/types/eval/jl/prelude.d.ts +1 -0
- package/dist/types/eval/jl/runtime.d.ts +22 -0
- package/dist/types/eval/kernel-base.d.ts +105 -0
- package/dist/types/eval/py/kernel.d.ts +3 -61
- package/dist/types/eval/rb/executor.d.ts +77 -0
- package/dist/types/eval/rb/index.d.ts +11 -0
- package/dist/types/eval/rb/kernel.d.ts +31 -0
- package/dist/types/eval/rb/prelude.d.ts +1 -0
- package/dist/types/eval/rb/runtime.d.ts +23 -0
- package/dist/types/eval/runtime-env.d.ts +24 -0
- package/dist/types/eval/types.d.ts +3 -3
- package/dist/types/extensibility/extensions/runner.d.ts +2 -15
- package/dist/types/extensibility/hooks/loader.d.ts +1 -25
- package/dist/types/extensibility/session-handler-types.d.ts +23 -0
- package/dist/types/jsonrpc/message-framing.d.ts +35 -0
- package/dist/types/mnemopi/embed-client.d.ts +7 -24
- package/dist/types/modes/components/chat-transcript-builder.d.ts +1 -1
- package/dist/types/modes/components/custom-editor.d.ts +11 -0
- package/dist/types/modes/components/selector-helpers.d.ts +53 -0
- package/dist/types/modes/interactive-mode.d.ts +0 -2
- package/dist/types/modes/theme/theme.d.ts +8 -1
- package/dist/types/modes/types.d.ts +0 -2
- package/dist/types/modes/utils/copy-targets.d.ts +1 -1
- package/dist/types/modes/utils/interactive-context-helpers.d.ts +14 -0
- package/dist/types/modes/utils/transcript-render-helpers.d.ts +54 -0
- package/dist/types/sdk.d.ts +1 -1
- package/dist/types/session/agent-session.d.ts +2 -2
- package/dist/types/stt/asr-client.d.ts +3 -29
- package/dist/types/subprocess/worker-client.d.ts +149 -0
- package/dist/types/subprocess/worker-runtime.d.ts +107 -0
- package/dist/types/tiny/title-client.d.ts +14 -34
- package/dist/types/tools/eval-backends.d.ts +6 -3
- package/dist/types/tools/eval-render.d.ts +6 -5
- package/dist/types/tools/eval.d.ts +13 -15
- package/dist/types/tools/index.d.ts +3 -3
- package/dist/types/tts/tts-client.d.ts +3 -28
- package/dist/types/tui/code-cell.d.ts +7 -0
- package/dist/types/utils/file-display-mode.d.ts +1 -1
- package/dist/types/web/parallel.d.ts +6 -0
- package/package.json +12 -12
- package/src/config/settings-schema.ts +50 -18
- package/src/config/settings.ts +5 -0
- package/src/dap/client.ts +13 -107
- package/src/eval/__tests__/julia-prelude.test.ts +77 -0
- package/src/eval/backend-helpers.ts +48 -0
- package/src/eval/executor-base.ts +425 -0
- package/src/eval/index.ts +2 -0
- package/src/eval/jl/executor.ts +540 -0
- package/src/eval/jl/index.ts +54 -0
- package/src/eval/jl/kernel.ts +235 -0
- package/src/eval/jl/prelude.jl +930 -0
- package/src/eval/jl/prelude.ts +3 -0
- package/src/eval/jl/runner.jl +634 -0
- package/src/eval/jl/runtime.ts +118 -0
- package/src/eval/js/index.ts +3 -14
- package/src/eval/kernel-base.ts +569 -0
- package/src/eval/py/executor.ts +43 -252
- package/src/eval/py/index.ts +9 -20
- package/src/eval/py/kernel.ts +29 -544
- package/src/eval/rb/executor.ts +504 -0
- package/src/eval/rb/index.ts +54 -0
- package/src/eval/rb/kernel.ts +230 -0
- package/src/eval/rb/prelude.rb +721 -0
- package/src/eval/rb/prelude.ts +3 -0
- package/src/eval/rb/runner.rb +474 -0
- package/src/eval/rb/runtime.ts +132 -0
- package/src/eval/runtime-env.ts +104 -0
- package/src/eval/types.ts +3 -3
- package/src/extensibility/extensions/runner.ts +4 -11
- package/src/extensibility/hooks/loader.ts +3 -21
- package/src/extensibility/session-handler-types.ts +21 -0
- package/src/internal-urls/docs-index.generated.txt +1 -1
- package/src/jsonrpc/message-framing.ts +142 -0
- package/src/lsp/client.ts +13 -109
- package/src/mnemopi/embed-client.ts +43 -198
- package/src/modes/components/agent-dashboard.ts +17 -40
- package/src/modes/components/chat-transcript-builder.ts +18 -102
- package/src/modes/components/custom-editor.ts +19 -0
- package/src/modes/components/extensions/extension-dashboard.ts +4 -13
- package/src/modes/components/extensions/extension-list.ts +16 -44
- package/src/modes/components/history-search.ts +4 -16
- package/src/modes/components/selector-helpers.ts +129 -0
- package/src/modes/components/settings-selector.ts +7 -5
- package/src/modes/components/tree-selector.ts +13 -18
- package/src/modes/controllers/event-controller.ts +3 -9
- package/src/modes/controllers/input-controller.ts +32 -54
- package/src/modes/interactive-mode.ts +5 -7
- package/src/modes/theme/theme.ts +35 -0
- package/src/modes/types.ts +0 -2
- package/src/modes/utils/copy-targets.ts +3 -2
- package/src/modes/utils/interactive-context-helpers.ts +27 -0
- package/src/modes/utils/transcript-render-helpers.ts +157 -0
- package/src/modes/utils/ui-helpers.ts +21 -126
- package/src/prompts/system/system-prompt.md +10 -10
- package/src/prompts/tools/bash.md +0 -1
- package/src/prompts/tools/eval.md +6 -4
- package/src/prompts/tools/find.md +0 -4
- package/src/prompts/tools/read.md +1 -2
- package/src/prompts/tools/replace.md +1 -1
- package/src/prompts/tools/search.md +0 -1
- package/src/sdk.ts +13 -7
- package/src/session/agent-session.ts +9 -7
- package/src/stt/asr-client.ts +35 -215
- package/src/stt/asr-worker.ts +29 -181
- package/src/subprocess/worker-client.ts +297 -0
- package/src/subprocess/worker-runtime.ts +277 -0
- package/src/task/executor.ts +4 -4
- package/src/tiny/title-client.ts +53 -219
- package/src/tiny/worker.ts +29 -180
- package/src/tools/eval-backends.ts +10 -3
- package/src/tools/eval-render.ts +17 -8
- package/src/tools/eval.ts +187 -22
- package/src/tools/index.ts +51 -28
- package/src/tts/tts-client.ts +38 -206
- package/src/tts/tts-worker.ts +23 -97
- package/src/tui/code-cell.ts +12 -1
- package/src/utils/file-display-mode.ts +2 -3
- package/src/web/parallel.ts +43 -42
- package/src/web/search/providers/parallel.ts +10 -99
|
@@ -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
|
+
}
|