@code-yeongyu/senpi-codemode 2026.7.25-2

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 (63) hide show
  1. package/CHANGELOG.md +250 -0
  2. package/LICENSE +22 -0
  3. package/README.md +161 -0
  4. package/package.json +58 -0
  5. package/src/bridge/http-server.ts +236 -0
  6. package/src/bridge/protocol.ts +198 -0
  7. package/src/bridge/reserved.ts +9 -0
  8. package/src/bridges/agent-bridge.ts +197 -0
  9. package/src/bridges/output-bridge.ts +96 -0
  10. package/src/bridges/schema-injection.ts +3 -0
  11. package/src/codemode/runtime.ts +258 -0
  12. package/src/codemode/tools.ts +106 -0
  13. package/src/completion/handler.ts +192 -0
  14. package/src/completion/tool-bridge.ts +55 -0
  15. package/src/config/settings.ts +215 -0
  16. package/src/extension/runtime-factory.ts +114 -0
  17. package/src/extension/session-manager-proxy.ts +116 -0
  18. package/src/extension/session-manager.ts +215 -0
  19. package/src/host-sdk.ts +1 -0
  20. package/src/index.ts +181 -0
  21. package/src/interpreters/detect.ts +161 -0
  22. package/src/kernels/jl/kernel.ts +37 -0
  23. package/src/kernels/jl/prelude.jl +283 -0
  24. package/src/kernels/jl/runner.jl +327 -0
  25. package/src/kernels/js/context-manager.ts +296 -0
  26. package/src/kernels/js/inline-worker-entry.js +23 -0
  27. package/src/kernels/js/inline-worker.ts +15 -0
  28. package/src/kernels/js/kernel-contract.ts +38 -0
  29. package/src/kernels/js/local-module-loader.ts +108 -0
  30. package/src/kernels/js/prelude.ts +15 -0
  31. package/src/kernels/js/rewrite-imports.ts +164 -0
  32. package/src/kernels/js/run-queue.ts +82 -0
  33. package/src/kernels/js/worker-core.d.ts +18 -0
  34. package/src/kernels/js/worker-core.js +94 -0
  35. package/src/kernels/js/worker-entry.js +23 -0
  36. package/src/kernels/js/worker-host.ts +117 -0
  37. package/src/kernels/js/worker-indirect-eval.js +88 -0
  38. package/src/kernels/js/worker-runtime.js +401 -0
  39. package/src/kernels/py/kernel-contract.ts +32 -0
  40. package/src/kernels/py/kernel.ts +290 -0
  41. package/src/kernels/py/prelude.py +954 -0
  42. package/src/kernels/py/process.ts +119 -0
  43. package/src/kernels/py/transport.ts +237 -0
  44. package/src/kernels/rb/kernel.ts +26 -0
  45. package/src/kernels/rb/prelude.rb +270 -0
  46. package/src/kernels/rb/runner.rb +204 -0
  47. package/src/kernels/shared/subprocess-contract.ts +22 -0
  48. package/src/kernels/shared/subprocess-kernel.ts +266 -0
  49. package/src/kernels/shared/subprocess-process.ts +174 -0
  50. package/src/kernels/shared/subprocess-queue.ts +101 -0
  51. package/src/kernels/shared/subprocess-run.ts +98 -0
  52. package/src/output/output-meta.ts +89 -0
  53. package/src/output/streaming-output.ts +296 -0
  54. package/src/prompt/eval-prompt.ts +319 -0
  55. package/src/timeouts/bridge-timeout.ts +16 -0
  56. package/src/timeouts/idle-timeout.ts +84 -0
  57. package/src/tool/cell-handler.ts +279 -0
  58. package/src/tool/eval-tool.ts +285 -0
  59. package/src/tool/image.ts +274 -0
  60. package/src/tool/json-tree.ts +247 -0
  61. package/src/tool/render.ts +876 -0
  62. package/src/tool/status-events.ts +12 -0
  63. package/src/tool/types.ts +114 -0
@@ -0,0 +1,283 @@
1
+ # allow: SIZE_OK — this dependency-free kernel prelude is loaded as one runtime asset.
2
+ const SENPI_B64_ALPHABET = collect("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/")
3
+
4
+ function senpi_base64(text::AbstractString)
5
+ bytes = codeunits(text)
6
+ out = IOBuffer()
7
+ index = 1
8
+ while index <= length(bytes)
9
+ first_byte = bytes[index]
10
+ second_byte = index + 1 <= length(bytes) ? bytes[index + 1] : UInt8(0)
11
+ third_byte = index + 2 <= length(bytes) ? bytes[index + 2] : UInt8(0)
12
+ Base.write(out, SENPI_B64_ALPHABET[(first_byte >> 2) + 1])
13
+ Base.write(out, SENPI_B64_ALPHABET[(((first_byte & 0x03) << 4) | (second_byte >> 4)) + 1])
14
+ Base.write(out, index + 1 <= length(bytes) ? SENPI_B64_ALPHABET[(((second_byte & 0x0f) << 2) | (third_byte >> 6)) + 1] : '=')
15
+ Base.write(out, index + 2 <= length(bytes) ? SENPI_B64_ALPHABET[(third_byte & 0x3f) + 1] : '=')
16
+ index += 3
17
+ end
18
+ String(take!(out))
19
+ end
20
+
21
+ function senpi_status_events_enabled()
22
+ get(senpi_connection, "statusEvents", true) !== false
23
+ end
24
+
25
+ function senpi_emit_status(op::AbstractString, fields::AbstractDict=Dict{String, Any}(); force::Bool=false)
26
+ (force || senpi_status_events_enabled()) || return nothing
27
+ event = Dict{String, Any}("op" => string(op))
28
+ for (key, value) in fields
29
+ event[string(key)] = value
30
+ end
31
+ senpi_emit(Dict("type" => "status", "event" => event))
32
+ nothing
33
+ end
34
+
35
+ function senpi_with_bridge_timeout_pause(operation::Function)
36
+ senpi_emit_status("timeout-pause"; force=true)
37
+ try
38
+ return operation()
39
+ finally
40
+ senpi_emit_status("timeout-resume"; force=true)
41
+ end
42
+ end
43
+
44
+ function senpi_url_decode(value::AbstractString)
45
+ out = IOBuffer()
46
+ index = 1
47
+ while index <= ncodeunits(value)
48
+ character = Char(codeunit(value, index))
49
+ if character == '%' && index + 2 <= ncodeunits(value)
50
+ hex = value[index + 1:index + 2]
51
+ parsed = tryparse(UInt8, hex; base=16)
52
+ if parsed !== nothing
53
+ Base.write(out, parsed)
54
+ index += 3
55
+ continue
56
+ end
57
+ end
58
+ Base.write(out, character)
59
+ index += 1
60
+ end
61
+ String(take!(out))
62
+ end
63
+
64
+ function senpi_resolve_path(value::AbstractString)
65
+ raw = string(value)
66
+ matched = match(r"^([a-z][a-z0-9+.\-]*)://(.*)$"i, raw)
67
+ matched === nothing && return abspath(raw)
68
+ scheme = lowercase(string(matched.captures[1]))
69
+ roots = get(senpi_connection, "localRoots", nothing)
70
+ root = roots isa AbstractDict ? get(roots, scheme, nothing) : nothing
71
+ root isa AbstractString && !isempty(root) || error("Protocol paths are not supported by this helper: $raw")
72
+ relative = senpi_url_decode(replace(string(matched.captures[2]), '\\' => '/'))
73
+ root_path = abspath(string(root))
74
+ isempty(relative) && return root_path
75
+ (startswith(relative, '/') || ".." in split(relative, '/')) && error("Unsafe $scheme:// path (absolute or traversal): $raw")
76
+ resolved = abspath(joinpath(root_path, relative))
77
+ (resolved == root_path || startswith(resolved, root_path * string(Base.Filesystem.path_separator))) || error("$scheme:// path escapes its root: $raw")
78
+ resolved
79
+ end
80
+
81
+ function senpi_display_payload(value)
82
+ if value isa AbstractDict
83
+ kind = get(value, "type", get(value, :type, nothing))
84
+ text_value = get(value, "text", get(value, :text, nothing))
85
+ if kind == "markdown" && text_value !== nothing
86
+ return "text/markdown", string(text_value), false
87
+ end
88
+ mime_type = get(value, "mimeType", get(value, :mimeType, nothing))
89
+ data = get(value, "data", get(value, :data, nothing))
90
+ if kind == "image" && mime_type isa AbstractString && data !== nothing
91
+ return string(mime_type), string(data), true
92
+ end
93
+ return "application/json", senpi_json(value), false
94
+ end
95
+ value isa AbstractVector && return "application/json", senpi_json(value), false
96
+ "text/plain", string(value), false
97
+ end
98
+
99
+ function display(value)
100
+ mime_type, payload, encoded = senpi_display_payload(value)
101
+ senpi_emit(Dict("type" => "display", "mimeType" => mime_type, "dataBase64" => encoded ? payload : senpi_base64(payload)))
102
+ nothing
103
+ end
104
+
105
+ function display_image(base64_value::AbstractString, mime_type::AbstractString="image/png")
106
+ senpi_emit(Dict("type" => "display", "mimeType" => string(mime_type), "dataBase64" => string(base64_value)))
107
+ nothing
108
+ end
109
+
110
+ function text(value)
111
+ senpi_emit(Dict("type" => "text", "stream" => "stdout", "data" => string(value)))
112
+ nothing
113
+ end
114
+
115
+ function print(values...)
116
+ text(join(string.(values), ""))
117
+ end
118
+
119
+ function read(path::AbstractString, offset::Integer=1, limit::Union{Integer, Nothing}=nothing)
120
+ resolved = senpi_resolve_path(path)
121
+ content = Base.read(resolved, String)
122
+ if offset > 1 || limit !== nothing
123
+ lines = split(content, '\n'; keepempty=true)
124
+ start = max(1, Int(offset))
125
+ finish = limit === nothing ? length(lines) : min(length(lines), start + Int(limit) - 1)
126
+ content = start <= length(lines) ? join(lines[start:finish], '\n') : ""
127
+ end
128
+ senpi_emit_status("read", Dict("path" => resolved, "chars" => length(content), "preview" => first(content, min(length(content), 500))))
129
+ content
130
+ end
131
+
132
+ function write(path::AbstractString, content)
133
+ resolved = senpi_resolve_path(path)
134
+ mkpath(dirname(resolved))
135
+ data = string(content)
136
+ open(resolved, "w") do io
137
+ Base.write(io, data)
138
+ end
139
+ senpi_emit_status("write", Dict("path" => resolved, "chars" => length(data)))
140
+ resolved
141
+ end
142
+
143
+ function env(key=nothing, value=nothing)
144
+ if key === nothing
145
+ values = Dict{String, String}(string(name) => string(item) for (name, item) in ENV)
146
+ senpi_emit_status("env", Dict("count" => length(values), "keys" => sort(collect(keys(values)))[1:min(20, length(values))]))
147
+ return values
148
+ end
149
+ name = string(key)
150
+ if value === nothing
151
+ resolved = get(ENV, name, nothing)
152
+ senpi_emit_status("env", Dict("key" => name, "value" => resolved, "action" => "get"))
153
+ return resolved
154
+ end
155
+ resolved = string(value)
156
+ ENV[name] = resolved
157
+ senpi_emit_status("env", Dict("key" => name, "value" => resolved, "action" => "set"))
158
+ resolved
159
+ end
160
+
161
+ struct SenpiToolProxy end
162
+
163
+ struct SenpiToolCallable
164
+ name::String
165
+ end
166
+
167
+ function (callable::SenpiToolCallable)(args=nothing; kwargs...)
168
+ values = Dict{String, Any}()
169
+ if args !== nothing
170
+ args isa AbstractDict || error("tool.$(callable.name)(...) expects a dictionary of arguments")
171
+ for (key, value) in args
172
+ values[string(key)] = value
173
+ end
174
+ end
175
+ for (key, value) in kwargs
176
+ values[string(key)] = value
177
+ end
178
+ senpi_with_bridge_timeout_pause(() -> senpi_call_tool(callable.name, values))
179
+ end
180
+
181
+ Base.getproperty(::SenpiToolProxy, name::Symbol) = SenpiToolCallable(string(name))
182
+ const tool = SenpiToolProxy()
183
+
184
+ function completion(prompt::AbstractString; model="default", system=nothing, schema=nothing, kwargs...)
185
+ options = Dict{String, Any}("model" => model)
186
+ system !== nothing && (options["system"] = system)
187
+ schema !== nothing && (options["schema"] = schema)
188
+ for (key, value) in kwargs
189
+ options[string(key)] = value
190
+ end
191
+ response = senpi_with_bridge_timeout_pause(() -> senpi_completion(string(prompt), options))
192
+ response isa AbstractDict || return response
193
+ haskey(response, "value") && return response["value"]
194
+ get(response, "text", response)
195
+ end
196
+
197
+ function output(ids...; format="raw", offset=nothing, limit=nothing)
198
+ isempty(ids) && error("At least one output ID is required")
199
+ format in ("raw", "tail") || error("output() format must be 'raw' or 'tail'")
200
+ arguments = Dict{String, Any}("ids" => string.(ids), "format" => format)
201
+ offset !== nothing && (arguments["offset"] = offset)
202
+ limit !== nothing && (arguments["limit"] = limit)
203
+ senpi_with_bridge_timeout_pause(() -> senpi_call_tool("__output__", arguments))
204
+ end
205
+
206
+ function agent(prompt::AbstractString; agent="task", model=nothing, label=nothing, schema=nothing, isolated=nothing, apply=nothing, merge=nothing, handle=false, kwargs...)
207
+ arguments = Dict{String, Any}("prompt" => string(prompt), "agent" => agent)
208
+ for (key, value) in (("model", model), ("label", label), ("schema", schema), ("isolated", isolated), ("apply", apply), ("merge", merge))
209
+ value !== nothing && (arguments[key] = value)
210
+ end
211
+ for (key, value) in kwargs
212
+ arguments[string(key)] = value
213
+ end
214
+ handle && (arguments["handle"] = true)
215
+ response = senpi_with_bridge_timeout_pause(() -> senpi_call_tool("__agent__", arguments))
216
+ record = response isa AbstractDict ? response : Dict{String, Any}()
217
+ text_value = get(record, "text", response)
218
+ parsed = schema === nothing ? text_value : haskey(record, "data") ? record["data"] : senpi_json_parse(string(text_value))
219
+ handle || return parsed
220
+ result = Dict{String, Any}("text" => text_value, "output" => text_value, "id" => get(record, "id", nothing), "agent" => get(record, "agent", agent))
221
+ result["handle"] = get(record, "handle", result["id"] === nothing ? nothing : "agent://" * string(result["id"]))
222
+ schema !== nothing && (result["data"] = parsed)
223
+ result
224
+ end
225
+
226
+ function senpi_pool_map(items, callback)
227
+ values = collect(items)
228
+ isempty(values) && return Any[]
229
+ configured_width = get(senpi_connection, "parallelPoolWidth", 4)
230
+ width = configured_width isa Real && isfinite(configured_width) ? Int(floor(configured_width)) : 4
231
+ workers = min(max(width, 1), length(values))
232
+ tokens = Channel{Nothing}(workers)
233
+ for _ in 1:workers
234
+ put!(tokens, nothing)
235
+ end
236
+ results = Vector{Any}(undef, length(values))
237
+ errors = Dict{Int, Any}()
238
+ lock = ReentrantLock()
239
+ @sync for index in eachindex(values)
240
+ @async begin
241
+ take!(tokens)
242
+ try
243
+ results[index] = callback(values[index])
244
+ catch error
245
+ Base.lock(lock) do
246
+ errors[index] = error
247
+ end
248
+ finally
249
+ put!(tokens, nothing)
250
+ end
251
+ end
252
+ end
253
+ isempty(errors) || throw(errors[minimum(keys(errors))])
254
+ results
255
+ end
256
+
257
+ function parallel(thunks)
258
+ values = collect(thunks)
259
+ for thunk in values
260
+ applicable(thunk) || error("parallel() expects zero-argument callables")
261
+ end
262
+ senpi_pool_map(values, thunk -> thunk())
263
+ end
264
+
265
+ function pipeline(items, stages...)
266
+ values = collect(items)
267
+ for stage in stages
268
+ isempty(values) && return values
269
+ applicable(stage, first(values)) || error("pipeline() stages must be callables")
270
+ values = senpi_pool_map(values, stage)
271
+ end
272
+ values
273
+ end
274
+
275
+ function log(message)
276
+ senpi_emit(Dict("type" => "log", "message" => string(message)))
277
+ nothing
278
+ end
279
+
280
+ function phase(title)
281
+ senpi_emit(Dict("type" => "phase", "title" => string(title)))
282
+ nothing
283
+ end
@@ -0,0 +1,327 @@
1
+ # allow: SIZE_OK — parser, stream capture, bridge calls, and the persistent execution loop share Main globals.
2
+ using Sockets
3
+
4
+ const SENPI_ORIGINAL_STDOUT = stdout
5
+ const SENPI_ORIGINAL_STDIN = stdin
6
+ out_read, out_write = redirect_stdout()
7
+ err_read, err_write = redirect_stderr()
8
+ redirect_stdin(devnull)
9
+
10
+ include("prelude.jl")
11
+
12
+ const senpi_connection = Dict{String, Any}()
13
+ const senpi_write_lock = ReentrantLock()
14
+ global senpi_current_cell = nothing
15
+
16
+ function senpi_escape(text::AbstractString)
17
+ out = IOBuffer()
18
+ for character in text
19
+ if character == '"'
20
+ Base.write(out, "\\\"")
21
+ elseif character == '\\'
22
+ Base.write(out, "\\\\")
23
+ elseif character == '\n'
24
+ Base.write(out, "\\n")
25
+ elseif character == '\r'
26
+ Base.write(out, "\\r")
27
+ elseif character == '\t'
28
+ Base.write(out, "\\t")
29
+ else
30
+ Base.write(out, character)
31
+ end
32
+ end
33
+ String(take!(out))
34
+ end
35
+
36
+ function senpi_json(value)
37
+ if value === nothing
38
+ return "null"
39
+ elseif value isa Bool
40
+ return value ? "true" : "false"
41
+ elseif value isa Number
42
+ return string(value)
43
+ elseif value isa AbstractString
44
+ return "\"" * senpi_escape(value) * "\""
45
+ elseif value isa AbstractDict
46
+ return "{" * join(["\"" * senpi_escape(string(key)) * "\":" * senpi_json(item) for (key, item) in value], ",") * "}"
47
+ elseif value isa AbstractVector || value isa Tuple
48
+ return "[" * join([senpi_json(item) for item in value], ",") * "]"
49
+ end
50
+ "\"" * senpi_escape(string(value)) * "\""
51
+ end
52
+
53
+ function senpi_json_parse(input::AbstractString)
54
+ characters = collect(input)
55
+ cursor = Ref(1)
56
+ length_value = length(characters)
57
+ function skip_space()
58
+ while cursor[] <= length_value && isspace(characters[cursor[]])
59
+ cursor[] += 1
60
+ end
61
+ end
62
+ function parse_string()
63
+ characters[cursor[]] == '"' || error("Expected JSON string")
64
+ cursor[] += 1
65
+ out = IOBuffer()
66
+ while cursor[] <= length_value
67
+ character = characters[cursor[]]
68
+ cursor[] += 1
69
+ character == '"' && return String(take!(out))
70
+ if character != '\\'
71
+ Base.write(out, character)
72
+ continue
73
+ end
74
+ cursor[] <= length_value || error("Unexpected JSON string escape")
75
+ escaped = characters[cursor[]]
76
+ cursor[] += 1
77
+ if escaped == 'u'
78
+ cursor[] + 3 <= length_value || error("Incomplete JSON unicode escape")
79
+ code = parse(Int, String(characters[cursor[]:cursor[] + 3]); base=16)
80
+ Base.write(out, Char(code))
81
+ cursor[] += 4
82
+ else
83
+ mapped = escaped == 'n' ? '\n' : escaped == 'r' ? '\r' : escaped == 't' ? '\t' : escaped == 'b' ? '\b' : escaped == 'f' ? '\f' : escaped
84
+ Base.write(out, mapped)
85
+ end
86
+ end
87
+ error("Unterminated JSON string")
88
+ end
89
+ function parse_value()
90
+ skip_space()
91
+ cursor[] <= length_value || error("Unexpected JSON end")
92
+ character = characters[cursor[]]
93
+ if character == '"'
94
+ return parse_string()
95
+ elseif character == '{'
96
+ cursor[] += 1
97
+ object_result = Dict{String, Any}()
98
+ skip_space()
99
+ if cursor[] <= length_value && characters[cursor[]] == '}'
100
+ cursor[] += 1
101
+ return object_result
102
+ end
103
+ while true
104
+ skip_space()
105
+ key = parse_string()
106
+ skip_space()
107
+ cursor[] <= length_value && characters[cursor[]] == ':' || error("Expected JSON object colon")
108
+ cursor[] += 1
109
+ object_result[key] = parse_value()
110
+ skip_space()
111
+ cursor[] <= length_value || error("Unexpected JSON object end")
112
+ characters[cursor[]] == '}' && (cursor[] += 1; return object_result)
113
+ characters[cursor[]] == ',' || error("Expected JSON object separator")
114
+ cursor[] += 1
115
+ end
116
+ elseif character == '['
117
+ cursor[] += 1
118
+ array_result = Any[]
119
+ skip_space()
120
+ if cursor[] <= length_value && characters[cursor[]] == ']'
121
+ cursor[] += 1
122
+ return array_result
123
+ end
124
+ while true
125
+ push!(array_result, parse_value())
126
+ skip_space()
127
+ cursor[] <= length_value || error("Unexpected JSON array end")
128
+ characters[cursor[]] == ']' && (cursor[] += 1; return array_result)
129
+ characters[cursor[]] == ',' || error("Expected JSON array separator")
130
+ cursor[] += 1
131
+ end
132
+ elseif startswith(String(characters[cursor[]:end]), "true")
133
+ cursor[] += 4
134
+ return true
135
+ elseif startswith(String(characters[cursor[]:end]), "false")
136
+ cursor[] += 5
137
+ return false
138
+ elseif startswith(String(characters[cursor[]:end]), "null")
139
+ cursor[] += 4
140
+ return nothing
141
+ end
142
+ start = cursor[]
143
+ while cursor[] <= length_value && characters[cursor[]] in ['-', '+', '.', 'e', 'E', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9']
144
+ cursor[] += 1
145
+ end
146
+ number = String(characters[start:cursor[] - 1])
147
+ integer = tryparse(Int, number)
148
+ integer === nothing || return integer
149
+ decimal = tryparse(Float64, number)
150
+ decimal === nothing && error("Invalid JSON value")
151
+ decimal
152
+ end
153
+ parsed_result = parse_value()
154
+ skip_space()
155
+ cursor[] > length_value || error("Trailing JSON data")
156
+ parsed_result
157
+ end
158
+
159
+ function senpi_emit(frame)
160
+ lock(senpi_write_lock) do
161
+ println(SENPI_ORIGINAL_STDOUT, senpi_json(frame))
162
+ flush(SENPI_ORIGINAL_STDOUT)
163
+ end
164
+ nothing
165
+ end
166
+
167
+ function senpi_emit_stream(stream::String, bytes::Vector{UInt8})
168
+ senpi_current_cell === nothing && return nothing
169
+ isempty(bytes) && return nothing
170
+ data = try
171
+ String(copy(bytes))
172
+ catch
173
+ repr(bytes)
174
+ end
175
+ senpi_emit(Dict("type" => "text", "stream" => stream, "data" => data))
176
+ nothing
177
+ end
178
+
179
+ function senpi_drain_stream(io, stream::String)
180
+ while true
181
+ bytes = readavailable(io)
182
+ if !isempty(bytes)
183
+ senpi_emit_stream(stream, bytes)
184
+ elseif eof(io)
185
+ return nothing
186
+ else
187
+ yield()
188
+ sleep(0.001)
189
+ end
190
+ end
191
+ end
192
+
193
+ @async senpi_drain_stream(out_read, "stdout")
194
+ @async senpi_drain_stream(err_read, "stderr")
195
+
196
+ function senpi_http_body(response::AbstractString)
197
+ parts = split(response, "\r\n\r\n"; limit=2)
198
+ length(parts) == 2 || return response
199
+ headers, body = parts
200
+ occursin("transfer-encoding: chunked", lowercase(headers)) || return body
201
+ bytes = collect(codeunits(body))
202
+ output = UInt8[]
203
+ cursor = 1
204
+ while cursor <= length(bytes)
205
+ header_end = nothing
206
+ for index in cursor:length(bytes) - 1
207
+ if bytes[index] == 0x0d && bytes[index + 1] == 0x0a
208
+ header_end = index
209
+ break
210
+ end
211
+ end
212
+ header_end === nothing && error("Invalid chunked bridge response")
213
+ chunk_size = parse(Int, split(String(bytes[cursor:header_end - 1]), ";"; limit=2)[1]; base=16)
214
+ cursor = header_end + 2
215
+ chunk_size == 0 && break
216
+ cursor + chunk_size - 1 <= length(bytes) || error("Truncated chunked bridge response")
217
+ append!(output, bytes[cursor:cursor + chunk_size - 1])
218
+ cursor += chunk_size + 2
219
+ end
220
+ String(output)
221
+ end
222
+
223
+ function senpi_bridge_request(path::String, payload)
224
+ port = get(senpi_connection, "port", nothing)
225
+ token = get(senpi_connection, "token", nothing)
226
+ port isa Integer && token isa AbstractString || error("Julia tool bridge is not initialized")
227
+ body = senpi_json(payload)
228
+ socket = connect(ip"127.0.0.1", port)
229
+ try
230
+ request = join([
231
+ "POST " * path * " HTTP/1.1",
232
+ "Host: 127.0.0.1",
233
+ "Authorization: Bearer " * string(token),
234
+ "Content-Type: application/json",
235
+ "Content-Length: " * string(sizeof(body)),
236
+ "Connection: close",
237
+ "",
238
+ body,
239
+ ], "\r\n")
240
+ Base.write(socket, request)
241
+ flush(socket)
242
+ response = Base.read(socket, String)
243
+ parsed = senpi_json_parse(senpi_http_body(response))
244
+ parsed isa AbstractDict || error("Bridge returned invalid JSON")
245
+ get(parsed, "ok", false) === true && return get(parsed, "value", nothing)
246
+ failure = get(parsed, "error", parsed)
247
+ error(failure isa AbstractDict ? string(get(failure, "message", failure)) : string(failure))
248
+ finally
249
+ close(socket)
250
+ end
251
+ end
252
+
253
+ function senpi_call_tool(name::String, arguments)
254
+ senpi_bridge_request("/call", Dict("callId" => "jl-" * string(time_ns()), "toolName" => name, "args" => arguments))
255
+ end
256
+
257
+ function senpi_completion(prompt::String, options)
258
+ senpi_bridge_request("/completion", Dict("prompt" => prompt, "opts" => options))
259
+ end
260
+
261
+ function senpi_error(error)
262
+ message = sprint(showerror, error)
263
+ Dict("name" => string(typeof(error)), "message" => message)
264
+ end
265
+
266
+ function senpi_should_display_result(parsed)
267
+ if parsed isa Expr && parsed.head === :block && !isempty(parsed.args)
268
+ last = parsed.args[end]
269
+ if last isa Expr && last.head in [Symbol("="), :function, :struct, :using, :import, :const, :global, :local, :macro]
270
+ return false
271
+ end
272
+ end
273
+ true
274
+ end
275
+
276
+ function senpi_set_connection(value)
277
+ value isa AbstractDict || error("missing bridge connection")
278
+ empty!(senpi_connection)
279
+ for (key, item) in value
280
+ senpi_connection[string(key)] = item
281
+ end
282
+ end
283
+
284
+ function senpi_run_cell(message)
285
+ cell_id = string(get(message, "cellId", ""))
286
+ code = string(get(message, "code", ""))
287
+ started = time()
288
+ global senpi_current_cell = cell_id
289
+ try
290
+ parsed = Meta.parse("begin\n" * code * "\nend")
291
+ if parsed isa Expr && parsed.head === :error
292
+ error(string(parsed.args[1]))
293
+ end
294
+ value = Core.eval(Main, parsed)
295
+ flush(stdout)
296
+ flush(stderr)
297
+ yield()
298
+ frame = Dict{String, Any}("type" => "result", "cellId" => cell_id, "ok" => true, "durationMs" => round(Int, (time() - started) * 1000))
299
+ value !== nothing && senpi_should_display_result(parsed) && (frame["valueRepr"] = senpi_json(value))
300
+ senpi_emit(frame)
301
+ catch error
302
+ senpi_emit(Dict("type" => "result", "cellId" => cell_id, "ok" => false, "error" => senpi_error(error), "durationMs" => round(Int, (time() - started) * 1000)))
303
+ finally
304
+ global senpi_current_cell = nothing
305
+ end
306
+ end
307
+
308
+ while !eof(SENPI_ORIGINAL_STDIN)
309
+ line = readline(SENPI_ORIGINAL_STDIN)
310
+ isempty(line) && continue
311
+ try
312
+ message = senpi_json_parse(line)
313
+ message isa AbstractDict || error("Bridge frame must be an object")
314
+ kind = get(message, "type", nothing)
315
+ if kind == "init"
316
+ senpi_set_connection(get(message, "connection", nothing))
317
+ senpi_emit(Dict("type" => "ready"))
318
+ elseif kind == "run"
319
+ senpi_run_cell(message)
320
+ elseif kind == "close"
321
+ senpi_emit(Dict("type" => "closed"))
322
+ break
323
+ end
324
+ catch error
325
+ senpi_emit(Dict("type" => "init-failed", "error" => senpi_error(error)))
326
+ end
327
+ end