@evanovation/open-cursor 2.4.15
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/LICENSE +28 -0
- package/README.md +270 -0
- package/dist/cli/discover.js +527 -0
- package/dist/cli/mcptool.js +10339 -0
- package/dist/cli/opencode-cursor.js +2989 -0
- package/dist/index.js +20588 -0
- package/dist/plugin-entry.js +19848 -0
- package/package.json +82 -0
- package/scripts/cursor-agent-runner.mjs +272 -0
- package/scripts/sdk-runner.mjs +412 -0
- package/src/acp/metrics.ts +83 -0
- package/src/acp/sessions.ts +107 -0
- package/src/acp/tools.ts +209 -0
- package/src/auth.ts +175 -0
- package/src/cli/discover.ts +53 -0
- package/src/cli/mcptool.ts +133 -0
- package/src/cli/model-discovery.ts +71 -0
- package/src/cli/opencode-cursor.ts +1195 -0
- package/src/client/cursor-agent-child.ts +459 -0
- package/src/client/sdk-child.ts +550 -0
- package/src/client/simple.ts +293 -0
- package/src/commands/status.ts +39 -0
- package/src/index.ts +39 -0
- package/src/mcp/client-manager.ts +166 -0
- package/src/mcp/config.ts +169 -0
- package/src/mcp/tool-bridge.ts +133 -0
- package/src/models/config.ts +64 -0
- package/src/models/discovery.ts +105 -0
- package/src/models/index.ts +3 -0
- package/src/models/pricing.ts +196 -0
- package/src/models/sync.ts +247 -0
- package/src/models/types.ts +11 -0
- package/src/models/variants.ts +446 -0
- package/src/plugin-entry.ts +28 -0
- package/src/plugin-toggle.ts +81 -0
- package/src/plugin.ts +2802 -0
- package/src/provider/backend.ts +71 -0
- package/src/provider/boundary.ts +168 -0
- package/src/provider/passthrough-tracker.ts +38 -0
- package/src/provider/runtime-interception.ts +818 -0
- package/src/provider/tool-loop-guard.ts +644 -0
- package/src/provider/tool-schema-compat.ts +800 -0
- package/src/provider.ts +268 -0
- package/src/proxy/formatter.ts +60 -0
- package/src/proxy/handler.ts +29 -0
- package/src/proxy/incremental-prompt.ts +74 -0
- package/src/proxy/prompt-builder.ts +204 -0
- package/src/proxy/server.ts +207 -0
- package/src/proxy/session-resume.ts +312 -0
- package/src/proxy/tool-loop.ts +359 -0
- package/src/proxy/types.ts +13 -0
- package/src/services/toast-service.ts +81 -0
- package/src/streaming/ai-sdk-parts.ts +109 -0
- package/src/streaming/delta-tracker.ts +89 -0
- package/src/streaming/line-buffer.ts +44 -0
- package/src/streaming/openai-sse.ts +118 -0
- package/src/streaming/parser.ts +22 -0
- package/src/streaming/types.ts +158 -0
- package/src/tools/core/executor.ts +25 -0
- package/src/tools/core/registry.ts +27 -0
- package/src/tools/core/types.ts +31 -0
- package/src/tools/defaults.ts +954 -0
- package/src/tools/discovery.ts +140 -0
- package/src/tools/executors/cli.ts +59 -0
- package/src/tools/executors/local.ts +25 -0
- package/src/tools/executors/mcp.ts +39 -0
- package/src/tools/executors/sdk.ts +39 -0
- package/src/tools/index.ts +8 -0
- package/src/tools/registry.ts +34 -0
- package/src/tools/router.ts +123 -0
- package/src/tools/schema.ts +58 -0
- package/src/tools/skills/loader.ts +61 -0
- package/src/tools/skills/resolver.ts +21 -0
- package/src/tools/types.ts +29 -0
- package/src/types.ts +8 -0
- package/src/usage.ts +112 -0
- package/src/utils/binary.ts +71 -0
- package/src/utils/errors.ts +224 -0
- package/src/utils/logger.ts +191 -0
- package/src/utils/perf.ts +76 -0
|
@@ -0,0 +1,550 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* sdk-child.ts
|
|
3
|
+
*
|
|
4
|
+
* Spawns sdk-runner.mjs as a persistent singleton process.
|
|
5
|
+
* The runner reads NDJSON requests from stdin: {"id":"...","model":"...","cwd":"...","prompt":"..."}
|
|
6
|
+
* and emits wrapped NDJSON responses to stdout: {"id":"...","event":{...}} or {"id":"...","done":true,"exitCode":...}
|
|
7
|
+
*
|
|
8
|
+
* This module demultiplexes per-request by:
|
|
9
|
+
* 1. Maintaining a singleton runner process (lazy spawn on first use)
|
|
10
|
+
* 2. Generating a unique request id per create*Child call
|
|
11
|
+
* 3. Writing the request to runner stdin
|
|
12
|
+
* 4. Filtering runner stdout by id to re-emit per-request events
|
|
13
|
+
* 5. Closing the per-request stream when "done" is received
|
|
14
|
+
*
|
|
15
|
+
* Benefits:
|
|
16
|
+
* - Node process boot + SDK import cost paid once for all requests
|
|
17
|
+
* - Requests run concurrently inside the runner (OpenCode fires several at once)
|
|
18
|
+
* - Also exposes listModelsViaRunner() for model discovery (op: "listModels")
|
|
19
|
+
*
|
|
20
|
+
* Limitations:
|
|
21
|
+
* - kill() on a single child does not interrupt the in-flight SDK run
|
|
22
|
+
* - If a different apiKey arrives (rare), the runner is re-spawned with the new key
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
import { spawn } from "node:child_process";
|
|
26
|
+
import { fileURLToPath } from "node:url";
|
|
27
|
+
import { dirname, resolve } from "node:path";
|
|
28
|
+
import { existsSync } from "node:fs";
|
|
29
|
+
import { EventEmitter } from "node:events";
|
|
30
|
+
import { PassThrough } from "node:stream";
|
|
31
|
+
import { createLogger } from "../utils/logger.js";
|
|
32
|
+
import { randomBytes } from "node:crypto";
|
|
33
|
+
|
|
34
|
+
const log = createLogger("sdk-child");
|
|
35
|
+
const textEncoder = new TextEncoder();
|
|
36
|
+
|
|
37
|
+
const EVENT_KEY = '"event":';
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Extract the inner event JSON from a wrapper line like {"id":"...","event":{...}}
|
|
41
|
+
* without re-serializing the parsed object. Falls back to the full line if
|
|
42
|
+
* the format is unexpected.
|
|
43
|
+
*/
|
|
44
|
+
/** @internal Exported for testing only. */
|
|
45
|
+
export function extractEventJson(line: string): string {
|
|
46
|
+
const idx = line.indexOf(EVENT_KEY);
|
|
47
|
+
if (idx < 0) return line;
|
|
48
|
+
const start = idx + EVENT_KEY.length;
|
|
49
|
+
const end = line.lastIndexOf("}");
|
|
50
|
+
if (end <= start) return line;
|
|
51
|
+
return line.substring(start, end);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// ─── Utilities ──────────────────────────────────────────────────────────────
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Resolve the Node binary path from PATH or environment override.
|
|
58
|
+
*/
|
|
59
|
+
function resolveNodeBinary(): string {
|
|
60
|
+
return process.env.CURSOR_ACP_NODE_BIN || "node";
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Resolve the path to sdk-runner.mjs, handling both src/ (dev) and dist/ (built) contexts.
|
|
65
|
+
* Returns the absolute path or throws if not found.
|
|
66
|
+
*/
|
|
67
|
+
export function resolveRunnerPath(
|
|
68
|
+
currentFile: string = fileURLToPath(import.meta.url),
|
|
69
|
+
checkExists: (path: string) => boolean = existsSync,
|
|
70
|
+
env: Pick<NodeJS.ProcessEnv, "CURSOR_ACP_SDK_RUNNER_PATH"> = process.env,
|
|
71
|
+
): string {
|
|
72
|
+
const override = env.CURSOR_ACP_SDK_RUNNER_PATH?.trim();
|
|
73
|
+
if (override) {
|
|
74
|
+
if (checkExists(override)) {
|
|
75
|
+
return override;
|
|
76
|
+
}
|
|
77
|
+
throw new Error(`CURSOR_ACP_SDK_RUNNER_PATH does not exist: ${override}`);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const currentDir = dirname(currentFile);
|
|
81
|
+
|
|
82
|
+
const candidates = [
|
|
83
|
+
// Source layout: src/client/sdk-child.ts -> scripts/sdk-runner.mjs.
|
|
84
|
+
// Non-bundled dist layout: dist/client/sdk-child.js -> scripts/sdk-runner.mjs.
|
|
85
|
+
resolve(currentDir, "../../scripts/sdk-runner.mjs"),
|
|
86
|
+
// Bundled package layout: dist/plugin-entry.js -> scripts/sdk-runner.mjs.
|
|
87
|
+
resolve(currentDir, "../scripts/sdk-runner.mjs"),
|
|
88
|
+
];
|
|
89
|
+
|
|
90
|
+
for (const candidate of candidates) {
|
|
91
|
+
if (checkExists(candidate)) {
|
|
92
|
+
return candidate;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
log.error("Could not resolve sdk-runner.mjs", {
|
|
97
|
+
currentFile,
|
|
98
|
+
candidates,
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
throw new Error(
|
|
102
|
+
`sdk-runner.mjs not found. Tried: ${candidates.join(", ")}`,
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Generate a unique request id (hex string).
|
|
108
|
+
*/
|
|
109
|
+
function generateRequestId(): string {
|
|
110
|
+
return randomBytes(8).toString("hex");
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// ─── Singleton Runner ──────────────────────────────────────────────────────
|
|
114
|
+
|
|
115
|
+
interface PendingRequest {
|
|
116
|
+
controller: ReadableStreamDefaultController<Uint8Array>;
|
|
117
|
+
promiseResolver: (code: number) => void;
|
|
118
|
+
promiseRejector: (err: Error) => void;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Manages the persistent runner process and per-request demultiplexing.
|
|
123
|
+
*/
|
|
124
|
+
class SdkRunnerSingleton {
|
|
125
|
+
private runnerProcess: ReturnType<typeof spawn> | null = null;
|
|
126
|
+
private lastApiKey: string | null = null;
|
|
127
|
+
private pendingRequests = new Map<string, PendingRequest>();
|
|
128
|
+
private lineBuffer = "";
|
|
129
|
+
private starting: Promise<void> | null = null;
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Ensure the runner is spawned (or re-spawn if apiKey changed).
|
|
133
|
+
* Concurrent callers share the same spawn (lock via this.starting) —
|
|
134
|
+
* OpenCode fires multiple requests at once (e.g. title-gen + chat).
|
|
135
|
+
*/
|
|
136
|
+
async ensureRunning(apiKey: string): Promise<void> {
|
|
137
|
+
// If apiKey changed, kill the old process and respawn
|
|
138
|
+
if (this.lastApiKey && this.lastApiKey !== apiKey) {
|
|
139
|
+
log.info("API key changed, restarting runner");
|
|
140
|
+
this.kill();
|
|
141
|
+
this.starting = null;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
if (this.runnerProcess) {
|
|
145
|
+
return; // already running
|
|
146
|
+
}
|
|
147
|
+
if (this.starting) {
|
|
148
|
+
return this.starting; // spawn already in progress
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
this.starting = this.doSpawn(apiKey);
|
|
152
|
+
try {
|
|
153
|
+
await this.starting;
|
|
154
|
+
} finally {
|
|
155
|
+
this.starting = null;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
private async doSpawn(apiKey: string): Promise<void> {
|
|
160
|
+
const nodeBin = resolveNodeBinary();
|
|
161
|
+
const runnerPath = resolveRunnerPath();
|
|
162
|
+
|
|
163
|
+
log.info("spawning persistent sdk runner", {
|
|
164
|
+
runnerPath,
|
|
165
|
+
nodeBin,
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
this.lastApiKey = apiKey;
|
|
169
|
+
|
|
170
|
+
this.runnerProcess = spawn(nodeBin, [runnerPath], {
|
|
171
|
+
env: { ...process.env, CURSOR_API_KEY: apiKey },
|
|
172
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
// Handle runner stdout (demultiplex by request id)
|
|
176
|
+
this.runnerProcess.stdout?.on("data", (chunk) => {
|
|
177
|
+
this.handleStdoutChunk(chunk);
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
// Forward stderr to our logger (diagnostics only)
|
|
181
|
+
this.runnerProcess.stderr?.on("data", (chunk) => {
|
|
182
|
+
const text = chunk.toString("utf8").trimEnd();
|
|
183
|
+
for (const line of text.split("\n")) {
|
|
184
|
+
if (line) {
|
|
185
|
+
log.debug(`[runner stderr] ${line}`);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
// Handle runner exit
|
|
191
|
+
this.runnerProcess.on("close", (code) => {
|
|
192
|
+
log.error(`sdk runner exited with code ${code}`);
|
|
193
|
+
this.runnerProcess = null;
|
|
194
|
+
// Fail all pending requests
|
|
195
|
+
for (const [id, pending] of this.pendingRequests.entries()) {
|
|
196
|
+
pending.promiseRejector(new Error(`Runner exited with code ${code}`));
|
|
197
|
+
pending.controller.error(new Error(`Runner exited with code ${code}`));
|
|
198
|
+
}
|
|
199
|
+
this.pendingRequests.clear();
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
this.runnerProcess.on("error", (err) => {
|
|
203
|
+
log.error("sdk runner spawn error", { error: err.message });
|
|
204
|
+
this.runnerProcess = null;
|
|
205
|
+
// Fail all pending requests
|
|
206
|
+
for (const [id, pending] of this.pendingRequests.entries()) {
|
|
207
|
+
pending.promiseRejector(err);
|
|
208
|
+
pending.controller.error(err);
|
|
209
|
+
}
|
|
210
|
+
this.pendingRequests.clear();
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Send a request to the runner.
|
|
216
|
+
*/
|
|
217
|
+
sendRequest(requestId: string, model: string, cwd: string, prompt: string): void {
|
|
218
|
+
if (!this.runnerProcess || !this.runnerProcess.stdin) {
|
|
219
|
+
throw new Error("Runner process not ready");
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
const request = { id: requestId, model, cwd, prompt };
|
|
223
|
+
this.runnerProcess.stdin.write(JSON.stringify(request) + "\n");
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Send a raw request to the runner (for operations like listModels).
|
|
229
|
+
* The request object is sent as-is; the caller is responsible for including id.
|
|
230
|
+
*/
|
|
231
|
+
sendRawRequest(request: Record<string, any>): void {
|
|
232
|
+
if (!this.runnerProcess || !this.runnerProcess.stdin) {
|
|
233
|
+
throw new Error("Runner process not ready");
|
|
234
|
+
}
|
|
235
|
+
this.runnerProcess.stdin.write(JSON.stringify(request) + "\n");
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Handle a chunk of stdout from the runner.
|
|
240
|
+
* Lines are wrapped: {"id":"...","event":{...}} or {"id":"...","done":true,"exitCode":...}
|
|
241
|
+
*/
|
|
242
|
+
private handleStdoutChunk(chunk: Buffer | Uint8Array): void {
|
|
243
|
+
this.lineBuffer += chunk.toString("utf8");
|
|
244
|
+
const lines = this.lineBuffer.split("\n");
|
|
245
|
+
this.lineBuffer = lines.pop() ?? ""; // keep incomplete line
|
|
246
|
+
|
|
247
|
+
for (const line of lines) {
|
|
248
|
+
if (!line.trim()) continue;
|
|
249
|
+
|
|
250
|
+
try {
|
|
251
|
+
const wrapped = JSON.parse(line);
|
|
252
|
+
const requestId = wrapped.id;
|
|
253
|
+
if (!requestId) {
|
|
254
|
+
log.warn("Wrapped response missing id", { wrapped });
|
|
255
|
+
continue;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
const pending = this.pendingRequests.get(requestId);
|
|
259
|
+
if (!pending) {
|
|
260
|
+
log.warn(`Received response for unknown request ${requestId}`, { wrapped });
|
|
261
|
+
continue;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
if (wrapped.done) {
|
|
265
|
+
// Request complete
|
|
266
|
+
log.info(`Request ${requestId} complete with exitCode ${wrapped.exitCode}`);
|
|
267
|
+
pending.controller.close();
|
|
268
|
+
pending.promiseResolver(wrapped.exitCode ?? 0);
|
|
269
|
+
this.pendingRequests.delete(requestId);
|
|
270
|
+
} else if (wrapped.event) {
|
|
271
|
+
const eventJson = extractEventJson(line);
|
|
272
|
+
pending.controller.enqueue(textEncoder.encode(eventJson + "\n"));
|
|
273
|
+
}
|
|
274
|
+
} catch (err) {
|
|
275
|
+
log.error("Failed to parse wrapped response line", {
|
|
276
|
+
line,
|
|
277
|
+
error: err instanceof Error ? err.message : String(err),
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* Register a pending request and return the id.
|
|
285
|
+
*/
|
|
286
|
+
registerPending(
|
|
287
|
+
controller: ReadableStreamDefaultController<Uint8Array>,
|
|
288
|
+
promiseResolver: (code: number) => void,
|
|
289
|
+
promiseRejector: (err: Error) => void,
|
|
290
|
+
): string {
|
|
291
|
+
const id = generateRequestId();
|
|
292
|
+
this.pendingRequests.set(id, { controller, promiseResolver, promiseRejector });
|
|
293
|
+
return id;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* Kill the runner process (hard kill).
|
|
298
|
+
*/
|
|
299
|
+
kill(): void {
|
|
300
|
+
if (this.runnerProcess) {
|
|
301
|
+
try {
|
|
302
|
+
this.runnerProcess.kill("SIGKILL");
|
|
303
|
+
} catch {
|
|
304
|
+
// ignore
|
|
305
|
+
}
|
|
306
|
+
this.runnerProcess = null;
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
const singleton = new SdkRunnerSingleton();
|
|
312
|
+
|
|
313
|
+
export function stopSdkRunner(): void {
|
|
314
|
+
singleton.kill();
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// ─── BUN-compatible child ──────────────────────────────────────────────────
|
|
318
|
+
|
|
319
|
+
export interface SdkBunChild {
|
|
320
|
+
stdout: ReadableStream<Uint8Array>;
|
|
321
|
+
stderr: ReadableStream<Uint8Array>;
|
|
322
|
+
exited: Promise<number>;
|
|
323
|
+
kill(): void;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/**
|
|
327
|
+
* Creates a Bun-spawn-compatible object using the persistent runner.
|
|
328
|
+
* Each call returns a fresh per-request pair of streams.
|
|
329
|
+
*/
|
|
330
|
+
export function createSdkBunChild(options: {
|
|
331
|
+
apiKey: string;
|
|
332
|
+
model: string;
|
|
333
|
+
prompt: string;
|
|
334
|
+
cwd: string;
|
|
335
|
+
}): SdkBunChild {
|
|
336
|
+
log.info("creating sdk bun child", {
|
|
337
|
+
model: options.model,
|
|
338
|
+
cwd: options.cwd,
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
let requestId: string;
|
|
342
|
+
let resolveExited!: (code: number) => void;
|
|
343
|
+
let rejectExited!: (err: Error) => void;
|
|
344
|
+
|
|
345
|
+
const exited = new Promise<number>((resolve, reject) => {
|
|
346
|
+
resolveExited = resolve;
|
|
347
|
+
rejectExited = reject;
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
const stdout = new ReadableStream<Uint8Array>({
|
|
351
|
+
start: async (controller) => {
|
|
352
|
+
try {
|
|
353
|
+
// Ensure runner is alive with this apiKey
|
|
354
|
+
await singleton.ensureRunning(options.apiKey);
|
|
355
|
+
|
|
356
|
+
// Register this request
|
|
357
|
+
requestId = singleton.registerPending(controller, resolveExited, rejectExited);
|
|
358
|
+
log.info(`request ${requestId} registered (bun)`);
|
|
359
|
+
|
|
360
|
+
// Send the request to the runner
|
|
361
|
+
singleton.sendRequest(requestId, options.model, options.cwd, options.prompt);
|
|
362
|
+
} catch (err) {
|
|
363
|
+
const error = err instanceof Error ? err : new Error(String(err));
|
|
364
|
+
log.error("Failed to start request (bun)", { error: error.message });
|
|
365
|
+
controller.error(error);
|
|
366
|
+
rejectExited(error);
|
|
367
|
+
}
|
|
368
|
+
},
|
|
369
|
+
cancel() {
|
|
370
|
+
// Best-effort: could stop forwarding events, but runner continues
|
|
371
|
+
// For now, just log it
|
|
372
|
+
log.debug(`request ${requestId} cancelled (bun)`);
|
|
373
|
+
},
|
|
374
|
+
});
|
|
375
|
+
|
|
376
|
+
// Stub stderr (runner diagnostics go to parent stderr via logger)
|
|
377
|
+
const stderr = new ReadableStream<Uint8Array>({
|
|
378
|
+
start(controller) {
|
|
379
|
+
// No stderr for individual requests; it's global to the runner process
|
|
380
|
+
controller.close();
|
|
381
|
+
},
|
|
382
|
+
});
|
|
383
|
+
|
|
384
|
+
return {
|
|
385
|
+
stdout,
|
|
386
|
+
stderr,
|
|
387
|
+
exited,
|
|
388
|
+
kill() {
|
|
389
|
+
log.debug(`kill() called on bun child ${requestId}` );
|
|
390
|
+
// Best-effort cancellation; runner process stays alive
|
|
391
|
+
},
|
|
392
|
+
};
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
// ─── Node-compatible child ─────────────────────────────────────────────────
|
|
396
|
+
|
|
397
|
+
/**
|
|
398
|
+
* EventEmitter-based child that mirrors the shape of Node child_process.spawn().
|
|
399
|
+
* Emits "close" with exit code and "error" on failure.
|
|
400
|
+
*/
|
|
401
|
+
export class SdkNodeChild extends EventEmitter {
|
|
402
|
+
public readonly stdout: PassThrough = new PassThrough();
|
|
403
|
+
public readonly stderr: PassThrough = new PassThrough();
|
|
404
|
+
|
|
405
|
+
private requestId: string | null = null;
|
|
406
|
+
|
|
407
|
+
async spawn(options: { apiKey: string; model: string; prompt: string; cwd: string }) {
|
|
408
|
+
try {
|
|
409
|
+
log.info("spawning (via singleton) sdk node child", {
|
|
410
|
+
model: options.model,
|
|
411
|
+
cwd: options.cwd,
|
|
412
|
+
});
|
|
413
|
+
|
|
414
|
+
// Ensure runner is alive with this apiKey
|
|
415
|
+
await singleton.ensureRunning(options.apiKey);
|
|
416
|
+
|
|
417
|
+
// Create a ReadableStream to demultiplex from the singleton
|
|
418
|
+
let requestId: string;
|
|
419
|
+
let resolveExited: (code: number) => void;
|
|
420
|
+
let rejectExited: (err: Error) => void;
|
|
421
|
+
|
|
422
|
+
const exited = new Promise<number>((resolve, reject) => {
|
|
423
|
+
resolveExited = resolve;
|
|
424
|
+
rejectExited = reject;
|
|
425
|
+
});
|
|
426
|
+
|
|
427
|
+
const dummyController = {
|
|
428
|
+
enqueue: (data: Uint8Array) => {
|
|
429
|
+
this.stdout.write(data);
|
|
430
|
+
},
|
|
431
|
+
close: () => {
|
|
432
|
+
this.stdout.end();
|
|
433
|
+
},
|
|
434
|
+
error: (err: Error) => {
|
|
435
|
+
this.stdout.destroy(err);
|
|
436
|
+
},
|
|
437
|
+
} as unknown as ReadableStreamDefaultController<Uint8Array>;
|
|
438
|
+
|
|
439
|
+
// Register this request
|
|
440
|
+
requestId = singleton.registerPending(dummyController, (code) => {
|
|
441
|
+
this.stderr.end();
|
|
442
|
+
this.emit("close", code);
|
|
443
|
+
resolveExited(code);
|
|
444
|
+
}, (err) => {
|
|
445
|
+
this.stderr.end();
|
|
446
|
+
this.emit("error", err);
|
|
447
|
+
rejectExited(err);
|
|
448
|
+
});
|
|
449
|
+
|
|
450
|
+
this.requestId = requestId;
|
|
451
|
+
log.info(`request ${requestId} registered (node)`);
|
|
452
|
+
|
|
453
|
+
// Send the request to the runner
|
|
454
|
+
singleton.sendRequest(requestId, options.model, options.cwd, options.prompt);
|
|
455
|
+
} catch (err) {
|
|
456
|
+
const error = err instanceof Error ? err : new Error(String(err));
|
|
457
|
+
log.error("Failed to spawn sdk node child", { error: error.message });
|
|
458
|
+
this.emit("error", error);
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
kill() {
|
|
463
|
+
if (this.requestId) {
|
|
464
|
+
log.debug(`kill() called on node child ${this.requestId}`);
|
|
465
|
+
}
|
|
466
|
+
// Best-effort: runner process stays alive; individual request cannot be interrupted
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
export function createSdkNodeChild(options: {
|
|
471
|
+
apiKey: string;
|
|
472
|
+
model: string;
|
|
473
|
+
prompt: string;
|
|
474
|
+
cwd: string;
|
|
475
|
+
}): SdkNodeChild {
|
|
476
|
+
const child = new SdkNodeChild();
|
|
477
|
+
child.spawn(options).catch((err) => {
|
|
478
|
+
log.error("Spawn error", { error: err instanceof Error ? err.message : String(err) });
|
|
479
|
+
});
|
|
480
|
+
return child;
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
|
|
484
|
+
/**
|
|
485
|
+
* List available models via the SDK runner.
|
|
486
|
+
*
|
|
487
|
+
* This function:
|
|
488
|
+
* 1. Ensures the runner is spawned with the provided apiKey
|
|
489
|
+
* 2. Sends a listModels request
|
|
490
|
+
* 3. Accumulates events and returns the models array from the models event
|
|
491
|
+
* 4. Times out after 15 seconds
|
|
492
|
+
*/
|
|
493
|
+
/**
|
|
494
|
+
* List available models via the SDK runner.
|
|
495
|
+
*
|
|
496
|
+
* This function:
|
|
497
|
+
* 1. Ensures the runner is spawned with the provided apiKey
|
|
498
|
+
* 2. Sends a listModels request
|
|
499
|
+
* 3. Accumulates events and returns the models array from the models event
|
|
500
|
+
* 4. Times out after 15 seconds
|
|
501
|
+
*/
|
|
502
|
+
export async function listModelsViaRunner(apiKey: string): Promise<Array<{ id: string; name: string }>> {
|
|
503
|
+
try {
|
|
504
|
+
// Ensure runner is alive
|
|
505
|
+
await singleton.ensureRunning(apiKey);
|
|
506
|
+
|
|
507
|
+
// Return a promise that accumulates events
|
|
508
|
+
return new Promise(async (resolve, reject) => {
|
|
509
|
+
const timeout = setTimeout(() => reject(new Error("Timeout")), 15000);
|
|
510
|
+
const events: any[] = [];
|
|
511
|
+
let gotModels = false;
|
|
512
|
+
|
|
513
|
+
const decoder = new TextDecoder();
|
|
514
|
+
const controller = {
|
|
515
|
+
enqueue: (data: Uint8Array) => {
|
|
516
|
+
try {
|
|
517
|
+
const event = JSON.parse(decoder.decode(data).trim());
|
|
518
|
+
events.push(event);
|
|
519
|
+
if (event.type === "models") gotModels = true;
|
|
520
|
+
} catch (err) {
|
|
521
|
+
log.warn("listModels: failed to parse event", { error: String(err) });
|
|
522
|
+
}
|
|
523
|
+
},
|
|
524
|
+
close: () => {},
|
|
525
|
+
error: (e: Error) => reject(e),
|
|
526
|
+
} as any;
|
|
527
|
+
|
|
528
|
+
const id = singleton.registerPending(
|
|
529
|
+
controller,
|
|
530
|
+
(code: number) => {
|
|
531
|
+
clearTimeout(timeout as any);
|
|
532
|
+
if (!gotModels) return reject(new Error("No models"));
|
|
533
|
+
if (code !== 0) return reject(new Error(`Code ${code}`));
|
|
534
|
+
const m = events.find((e) => e.type === "models");
|
|
535
|
+
resolve(m?.models ?? []);
|
|
536
|
+
},
|
|
537
|
+
(e) => {
|
|
538
|
+
clearTimeout(timeout as any);
|
|
539
|
+
reject(e);
|
|
540
|
+
}
|
|
541
|
+
);
|
|
542
|
+
|
|
543
|
+
singleton.sendRawRequest({ id, op: "listModels" });
|
|
544
|
+
});
|
|
545
|
+
} catch (err) {
|
|
546
|
+
throw new Error(`listModelsViaRunner failed: ${String(err)}`);
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
|