@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.
Files changed (80) hide show
  1. package/LICENSE +28 -0
  2. package/README.md +270 -0
  3. package/dist/cli/discover.js +527 -0
  4. package/dist/cli/mcptool.js +10339 -0
  5. package/dist/cli/opencode-cursor.js +2989 -0
  6. package/dist/index.js +20588 -0
  7. package/dist/plugin-entry.js +19848 -0
  8. package/package.json +82 -0
  9. package/scripts/cursor-agent-runner.mjs +272 -0
  10. package/scripts/sdk-runner.mjs +412 -0
  11. package/src/acp/metrics.ts +83 -0
  12. package/src/acp/sessions.ts +107 -0
  13. package/src/acp/tools.ts +209 -0
  14. package/src/auth.ts +175 -0
  15. package/src/cli/discover.ts +53 -0
  16. package/src/cli/mcptool.ts +133 -0
  17. package/src/cli/model-discovery.ts +71 -0
  18. package/src/cli/opencode-cursor.ts +1195 -0
  19. package/src/client/cursor-agent-child.ts +459 -0
  20. package/src/client/sdk-child.ts +550 -0
  21. package/src/client/simple.ts +293 -0
  22. package/src/commands/status.ts +39 -0
  23. package/src/index.ts +39 -0
  24. package/src/mcp/client-manager.ts +166 -0
  25. package/src/mcp/config.ts +169 -0
  26. package/src/mcp/tool-bridge.ts +133 -0
  27. package/src/models/config.ts +64 -0
  28. package/src/models/discovery.ts +105 -0
  29. package/src/models/index.ts +3 -0
  30. package/src/models/pricing.ts +196 -0
  31. package/src/models/sync.ts +247 -0
  32. package/src/models/types.ts +11 -0
  33. package/src/models/variants.ts +446 -0
  34. package/src/plugin-entry.ts +28 -0
  35. package/src/plugin-toggle.ts +81 -0
  36. package/src/plugin.ts +2802 -0
  37. package/src/provider/backend.ts +71 -0
  38. package/src/provider/boundary.ts +168 -0
  39. package/src/provider/passthrough-tracker.ts +38 -0
  40. package/src/provider/runtime-interception.ts +818 -0
  41. package/src/provider/tool-loop-guard.ts +644 -0
  42. package/src/provider/tool-schema-compat.ts +800 -0
  43. package/src/provider.ts +268 -0
  44. package/src/proxy/formatter.ts +60 -0
  45. package/src/proxy/handler.ts +29 -0
  46. package/src/proxy/incremental-prompt.ts +74 -0
  47. package/src/proxy/prompt-builder.ts +204 -0
  48. package/src/proxy/server.ts +207 -0
  49. package/src/proxy/session-resume.ts +312 -0
  50. package/src/proxy/tool-loop.ts +359 -0
  51. package/src/proxy/types.ts +13 -0
  52. package/src/services/toast-service.ts +81 -0
  53. package/src/streaming/ai-sdk-parts.ts +109 -0
  54. package/src/streaming/delta-tracker.ts +89 -0
  55. package/src/streaming/line-buffer.ts +44 -0
  56. package/src/streaming/openai-sse.ts +118 -0
  57. package/src/streaming/parser.ts +22 -0
  58. package/src/streaming/types.ts +158 -0
  59. package/src/tools/core/executor.ts +25 -0
  60. package/src/tools/core/registry.ts +27 -0
  61. package/src/tools/core/types.ts +31 -0
  62. package/src/tools/defaults.ts +954 -0
  63. package/src/tools/discovery.ts +140 -0
  64. package/src/tools/executors/cli.ts +59 -0
  65. package/src/tools/executors/local.ts +25 -0
  66. package/src/tools/executors/mcp.ts +39 -0
  67. package/src/tools/executors/sdk.ts +39 -0
  68. package/src/tools/index.ts +8 -0
  69. package/src/tools/registry.ts +34 -0
  70. package/src/tools/router.ts +123 -0
  71. package/src/tools/schema.ts +58 -0
  72. package/src/tools/skills/loader.ts +61 -0
  73. package/src/tools/skills/resolver.ts +21 -0
  74. package/src/tools/types.ts +29 -0
  75. package/src/types.ts +8 -0
  76. package/src/usage.ts +112 -0
  77. package/src/utils/binary.ts +71 -0
  78. package/src/utils/errors.ts +224 -0
  79. package/src/utils/logger.ts +191 -0
  80. 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
+