@euqns/nudge-mcp 0.16.0 → 1.0.0
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/README.md +19 -16
- package/dist/codex-companion.js +452 -129
- package/dist/codex-companion.js.map +1 -1
- package/dist/codex-companion.test.js +27 -1
- package/dist/codex-companion.test.js.map +1 -1
- package/dist/index.js +5 -2
- package/dist/index.js.map +1 -1
- package/dist/version.js +1 -1
- package/dist/version.js.map +1 -1
- package/package.json +1 -1
package/dist/codex-companion.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Codex, } from "@openai/codex-sdk";
|
|
2
|
-
import { spawn, spawnSync } from "node:child_process";
|
|
2
|
+
import { spawn, spawnSync, } from "node:child_process";
|
|
3
3
|
import crypto from "node:crypto";
|
|
4
4
|
import http from "node:http";
|
|
5
5
|
import { createRequire } from "node:module";
|
|
@@ -27,6 +27,62 @@ const PROVIDERS = {
|
|
|
27
27
|
loginCommand: "cursor-agent login",
|
|
28
28
|
},
|
|
29
29
|
};
|
|
30
|
+
const CLAUDE_EFFORTS = [
|
|
31
|
+
{
|
|
32
|
+
reasoningEffort: "low",
|
|
33
|
+
description: "Faster responses with lighter reasoning.",
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
reasoningEffort: "medium",
|
|
37
|
+
description: "Balanced reasoning for everyday work.",
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
reasoningEffort: "high",
|
|
41
|
+
description: "More reasoning for difficult tasks.",
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
reasoningEffort: "xhigh",
|
|
45
|
+
description: "Extended reasoning for complex tasks.",
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
reasoningEffort: "max",
|
|
49
|
+
description: "Claude Code's maximum reasoning effort.",
|
|
50
|
+
},
|
|
51
|
+
];
|
|
52
|
+
const CLAUDE_MODELS = [
|
|
53
|
+
{
|
|
54
|
+
model: "default",
|
|
55
|
+
displayName: "Claude Code default",
|
|
56
|
+
description: "Use the default model from your Claude Code account.",
|
|
57
|
+
isDefault: true,
|
|
58
|
+
defaultReasoningEffort: "medium",
|
|
59
|
+
supportedReasoningEfforts: CLAUDE_EFFORTS,
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
model: "sonnet",
|
|
63
|
+
displayName: "Sonnet",
|
|
64
|
+
description: "Claude Code's balanced model alias.",
|
|
65
|
+
isDefault: false,
|
|
66
|
+
defaultReasoningEffort: "medium",
|
|
67
|
+
supportedReasoningEfforts: CLAUDE_EFFORTS,
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
model: "opus",
|
|
71
|
+
displayName: "Opus",
|
|
72
|
+
description: "Claude Code's most capable model alias.",
|
|
73
|
+
isDefault: false,
|
|
74
|
+
defaultReasoningEffort: "high",
|
|
75
|
+
supportedReasoningEfforts: CLAUDE_EFFORTS,
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
model: "haiku",
|
|
79
|
+
displayName: "Haiku",
|
|
80
|
+
description: "Claude Code's fastest model alias.",
|
|
81
|
+
isDefault: false,
|
|
82
|
+
defaultReasoningEffort: "low",
|
|
83
|
+
supportedReasoningEfforts: CLAUDE_EFFORTS,
|
|
84
|
+
},
|
|
85
|
+
];
|
|
30
86
|
const require = createRequire(import.meta.url);
|
|
31
87
|
const REASONING_EFFORTS = new Set([
|
|
32
88
|
"none",
|
|
@@ -39,7 +95,8 @@ const REASONING_EFFORTS = new Set([
|
|
|
39
95
|
"ultra",
|
|
40
96
|
]);
|
|
41
97
|
function reasoningEffort(value) {
|
|
42
|
-
return typeof value === "string" &&
|
|
98
|
+
return typeof value === "string" &&
|
|
99
|
+
REASONING_EFFORTS.has(value)
|
|
43
100
|
? value
|
|
44
101
|
: null;
|
|
45
102
|
}
|
|
@@ -77,7 +134,10 @@ class CodexMetadataClient {
|
|
|
77
134
|
});
|
|
78
135
|
this.child = child;
|
|
79
136
|
this.stderr = "";
|
|
80
|
-
const lines = readline.createInterface({
|
|
137
|
+
const lines = readline.createInterface({
|
|
138
|
+
input: child.stdout,
|
|
139
|
+
crlfDelay: Infinity,
|
|
140
|
+
});
|
|
81
141
|
lines.on("line", (line) => this.handleLine(line));
|
|
82
142
|
child.stderr.on("data", (chunk) => {
|
|
83
143
|
this.stderr = `${this.stderr}${chunk.toString("utf8")}`.slice(-4_000);
|
|
@@ -164,7 +224,9 @@ class CodexMetadataClient {
|
|
|
164
224
|
.filter((model) => typeof model.model === "string" && model.model.length > 0)
|
|
165
225
|
.map((model) => ({
|
|
166
226
|
model: model.model,
|
|
167
|
-
displayName: typeof model.displayName === "string"
|
|
227
|
+
displayName: typeof model.displayName === "string"
|
|
228
|
+
? model.displayName
|
|
229
|
+
: model.model,
|
|
168
230
|
description: typeof model.description === "string" ? model.description : "",
|
|
169
231
|
isDefault: model.isDefault === true,
|
|
170
232
|
defaultReasoningEffort: reasoningEffort(model.defaultReasoningEffort) ?? "medium",
|
|
@@ -176,10 +238,14 @@ class CodexMetadataClient {
|
|
|
176
238
|
const effort = reasoningEffort(entry.reasoningEffort);
|
|
177
239
|
if (!effort)
|
|
178
240
|
return [];
|
|
179
|
-
return [
|
|
241
|
+
return [
|
|
242
|
+
{
|
|
180
243
|
reasoningEffort: effort,
|
|
181
|
-
description: typeof entry.description === "string"
|
|
182
|
-
|
|
244
|
+
description: typeof entry.description === "string"
|
|
245
|
+
? entry.description
|
|
246
|
+
: "",
|
|
247
|
+
},
|
|
248
|
+
];
|
|
183
249
|
})
|
|
184
250
|
: [],
|
|
185
251
|
}));
|
|
@@ -221,7 +287,7 @@ class CodexMetadataClient {
|
|
|
221
287
|
currentReasoningEffort: selectedEffort,
|
|
222
288
|
models,
|
|
223
289
|
planType: account.account?.type === "chatgpt"
|
|
224
|
-
? account.account.planType ?? null
|
|
290
|
+
? (account.account.planType ?? null)
|
|
225
291
|
: null,
|
|
226
292
|
rateLimits,
|
|
227
293
|
updatedAt: Date.now(),
|
|
@@ -244,7 +310,7 @@ function parseArgs(argv) {
|
|
|
244
310
|
let openBrowserOnStart = true;
|
|
245
311
|
let port = DEFAULT_PORT;
|
|
246
312
|
let portWasExplicit = false;
|
|
247
|
-
let
|
|
313
|
+
let preferredProvider;
|
|
248
314
|
for (let index = 0; index < argv.length; index++) {
|
|
249
315
|
const arg = argv[index];
|
|
250
316
|
const next = argv[index + 1];
|
|
@@ -278,7 +344,7 @@ function parseArgs(argv) {
|
|
|
278
344
|
if (!(next in PROVIDERS)) {
|
|
279
345
|
throw new Error("--provider must be codex, claude, or cursor");
|
|
280
346
|
}
|
|
281
|
-
|
|
347
|
+
preferredProvider = next;
|
|
282
348
|
index++;
|
|
283
349
|
}
|
|
284
350
|
else if (arg.startsWith("--provider=")) {
|
|
@@ -286,7 +352,7 @@ function parseArgs(argv) {
|
|
|
286
352
|
if (!(value in PROVIDERS)) {
|
|
287
353
|
throw new Error("--provider must be codex, claude, or cursor");
|
|
288
354
|
}
|
|
289
|
-
|
|
355
|
+
preferredProvider = value;
|
|
290
356
|
}
|
|
291
357
|
else {
|
|
292
358
|
throw new Error(`Unknown agent companion option: ${arg}`);
|
|
@@ -301,7 +367,7 @@ function parseArgs(argv) {
|
|
|
301
367
|
openBrowser: openBrowserOnStart,
|
|
302
368
|
port,
|
|
303
369
|
portWasExplicit,
|
|
304
|
-
|
|
370
|
+
preferredProvider,
|
|
305
371
|
};
|
|
306
372
|
}
|
|
307
373
|
async function listenOnLoopback(server, preferredPort, allowFallback) {
|
|
@@ -366,7 +432,9 @@ function writeJson(res, status, value, headers) {
|
|
|
366
432
|
res.end(JSON.stringify(value));
|
|
367
433
|
}
|
|
368
434
|
export function buildCompanionPrompt(prompt, context) {
|
|
369
|
-
const pathname = typeof context?.pathname === "string"
|
|
435
|
+
const pathname = typeof context?.pathname === "string"
|
|
436
|
+
? context.pathname.slice(0, 500)
|
|
437
|
+
: undefined;
|
|
370
438
|
const routeContext = pathname
|
|
371
439
|
? `The user opened this chat from the Nudge route ${JSON.stringify(pathname)}.`
|
|
372
440
|
: "The user opened this chat from Nudge.";
|
|
@@ -385,11 +453,15 @@ export function buildCompanionPrompt(prompt, context) {
|
|
|
385
453
|
context.boardRole === "viewer"
|
|
386
454
|
? context.boardRole
|
|
387
455
|
: undefined,
|
|
388
|
-
view: typeof context.view === "string"
|
|
456
|
+
view: typeof context.view === "string"
|
|
457
|
+
? context.view.slice(0, 50)
|
|
458
|
+
: undefined,
|
|
389
459
|
canvasId: typeof context.canvasId === "string"
|
|
390
460
|
? context.canvasId.slice(0, 200)
|
|
391
461
|
: undefined,
|
|
392
|
-
cardId: typeof context.cardId === "string"
|
|
462
|
+
cardId: typeof context.cardId === "string"
|
|
463
|
+
? context.cardId.slice(0, 200)
|
|
464
|
+
: undefined,
|
|
393
465
|
cardTitle: typeof context.cardTitle === "string"
|
|
394
466
|
? context.cardTitle.slice(0, 500)
|
|
395
467
|
: undefined,
|
|
@@ -399,7 +471,9 @@ export function buildCompanionPrompt(prompt, context) {
|
|
|
399
471
|
cardHref: typeof context.cardHref === "string"
|
|
400
472
|
? context.cardHref.slice(0, 1_000)
|
|
401
473
|
: undefined,
|
|
402
|
-
listId: typeof context.listId === "string"
|
|
474
|
+
listId: typeof context.listId === "string"
|
|
475
|
+
? context.listId.slice(0, 200)
|
|
476
|
+
: undefined,
|
|
403
477
|
listTitle: typeof context.listTitle === "string"
|
|
404
478
|
? context.listTitle.slice(0, 500)
|
|
405
479
|
: undefined,
|
|
@@ -409,8 +483,12 @@ export function buildCompanionPrompt(prompt, context) {
|
|
|
409
483
|
repository: context.repository && typeof context.repository === "object"
|
|
410
484
|
? context.repository
|
|
411
485
|
: undefined,
|
|
412
|
-
listCount: typeof context.listCount === "number"
|
|
413
|
-
|
|
486
|
+
listCount: typeof context.listCount === "number"
|
|
487
|
+
? context.listCount
|
|
488
|
+
: undefined,
|
|
489
|
+
cardCount: typeof context.cardCount === "number"
|
|
490
|
+
? context.cardCount
|
|
491
|
+
: undefined,
|
|
414
492
|
}),
|
|
415
493
|
"Use this exact board id for Nudge MCP calls unless the user names another board.",
|
|
416
494
|
context.cardId
|
|
@@ -469,6 +547,20 @@ export function assertCompanionProvider(provider) {
|
|
|
469
547
|
throw new Error(`${definition.displayName} is not authenticated. Run \`${definition.loginCommand}\`.${detail ? `\nCurrent status: ${detail}` : ""}`);
|
|
470
548
|
}
|
|
471
549
|
}
|
|
550
|
+
export function validatedCompanionProviders(validate = assertCompanionProvider) {
|
|
551
|
+
const available = [];
|
|
552
|
+
const failures = new Map();
|
|
553
|
+
for (const provider of Object.keys(PROVIDERS)) {
|
|
554
|
+
try {
|
|
555
|
+
validate(provider);
|
|
556
|
+
available.push(provider);
|
|
557
|
+
}
|
|
558
|
+
catch (error) {
|
|
559
|
+
failures.set(provider, error instanceof Error ? error.message : String(error));
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
return { available, failures };
|
|
563
|
+
}
|
|
472
564
|
export function subscriptionEnvironment(environment = process.env) {
|
|
473
565
|
const blocked = new Set([
|
|
474
566
|
"ANTHROPIC_API_KEY",
|
|
@@ -479,6 +571,156 @@ export function subscriptionEnvironment(environment = process.env) {
|
|
|
479
571
|
]);
|
|
480
572
|
return Object.fromEntries(Object.entries(environment).filter((entry) => entry[1] !== undefined && !blocked.has(entry[0])));
|
|
481
573
|
}
|
|
574
|
+
function modelDisplayName(model) {
|
|
575
|
+
if (model === "auto")
|
|
576
|
+
return "Auto";
|
|
577
|
+
return model
|
|
578
|
+
.replace(/[-_]+/g, " ")
|
|
579
|
+
.replace(/\b\w/g, (letter) => letter.toUpperCase());
|
|
580
|
+
}
|
|
581
|
+
function cursorModel(candidate) {
|
|
582
|
+
const record = candidate && typeof candidate === "object"
|
|
583
|
+
? candidate
|
|
584
|
+
: null;
|
|
585
|
+
const rawModel = typeof candidate === "string"
|
|
586
|
+
? candidate
|
|
587
|
+
: typeof record?.model === "string"
|
|
588
|
+
? record.model
|
|
589
|
+
: typeof record?.id === "string"
|
|
590
|
+
? record.id
|
|
591
|
+
: typeof record?.value === "string"
|
|
592
|
+
? record.value
|
|
593
|
+
: "";
|
|
594
|
+
const model = rawModel.trim();
|
|
595
|
+
if (!/^[a-zA-Z0-9][a-zA-Z0-9._:/\[\]=,-]*$/.test(model))
|
|
596
|
+
return null;
|
|
597
|
+
const displayName = typeof record?.displayName === "string"
|
|
598
|
+
? record.displayName
|
|
599
|
+
: typeof record?.name === "string"
|
|
600
|
+
? record.name
|
|
601
|
+
: modelDisplayName(model);
|
|
602
|
+
return {
|
|
603
|
+
model,
|
|
604
|
+
displayName,
|
|
605
|
+
description: "Available to your Cursor account.",
|
|
606
|
+
isDefault: record?.isDefault === true || model === "auto",
|
|
607
|
+
defaultReasoningEffort: "none",
|
|
608
|
+
supportedReasoningEfforts: [],
|
|
609
|
+
};
|
|
610
|
+
}
|
|
611
|
+
/** Parse both current line output and future JSON output from Cursor's model command. */
|
|
612
|
+
export function parseCursorModels(output) {
|
|
613
|
+
const trimmed = output.trim();
|
|
614
|
+
let candidates = [];
|
|
615
|
+
if (trimmed.startsWith("[") || trimmed.startsWith("{")) {
|
|
616
|
+
try {
|
|
617
|
+
const parsed = JSON.parse(trimmed);
|
|
618
|
+
if (Array.isArray(parsed))
|
|
619
|
+
candidates = parsed;
|
|
620
|
+
else if (parsed && typeof parsed === "object") {
|
|
621
|
+
const record = parsed;
|
|
622
|
+
if (Array.isArray(record.models))
|
|
623
|
+
candidates = record.models;
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
catch {
|
|
627
|
+
// Older Cursor releases print one model id per line.
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
if (candidates.length === 0) {
|
|
631
|
+
candidates = output
|
|
632
|
+
.replace(/\u001b\[[0-9;]*m/g, "")
|
|
633
|
+
.split(/\r?\n/)
|
|
634
|
+
.map((line) => line.trim())
|
|
635
|
+
.filter((line) => line.length > 0)
|
|
636
|
+
.filter((line) => !/^(available\s+)?models?:?$/i.test(line))
|
|
637
|
+
.map((line) => line
|
|
638
|
+
.replace(/^[*✓●>•-]\s*/, "")
|
|
639
|
+
.replace(/\s+\((?:default|current)\)$/i, ""))
|
|
640
|
+
.map((line) => line.split(/\t|\s{2,}/, 1)[0]);
|
|
641
|
+
}
|
|
642
|
+
const models = candidates
|
|
643
|
+
.map((candidate) => cursorModel(candidate))
|
|
644
|
+
.filter((model) => model !== null);
|
|
645
|
+
const deduplicated = Array.from(new Map(models.map((model) => [model.model, model])).values());
|
|
646
|
+
if (deduplicated.length > 0 &&
|
|
647
|
+
!deduplicated.some((model) => model.isDefault)) {
|
|
648
|
+
deduplicated[0] = { ...deduplicated[0], isDefault: true };
|
|
649
|
+
}
|
|
650
|
+
return deduplicated;
|
|
651
|
+
}
|
|
652
|
+
function captureCliOutput(command, args) {
|
|
653
|
+
return new Promise((resolve, reject) => {
|
|
654
|
+
const child = spawn(command, args, {
|
|
655
|
+
env: subscriptionEnvironment(),
|
|
656
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
657
|
+
});
|
|
658
|
+
let stdout = "";
|
|
659
|
+
let stderr = "";
|
|
660
|
+
const timer = setTimeout(() => child.kill("SIGTERM"), 8_000);
|
|
661
|
+
child.stdout.on("data", (chunk) => {
|
|
662
|
+
stdout = `${stdout}${chunk.toString("utf8")}`.slice(-256_000);
|
|
663
|
+
});
|
|
664
|
+
child.stderr.on("data", (chunk) => {
|
|
665
|
+
stderr = `${stderr}${chunk.toString("utf8")}`.slice(-8_000);
|
|
666
|
+
});
|
|
667
|
+
child.once("error", (error) => {
|
|
668
|
+
clearTimeout(timer);
|
|
669
|
+
reject(error);
|
|
670
|
+
});
|
|
671
|
+
child.once("close", (code) => {
|
|
672
|
+
clearTimeout(timer);
|
|
673
|
+
if (code === 0)
|
|
674
|
+
resolve(stdout);
|
|
675
|
+
else
|
|
676
|
+
reject(new Error(stderr.trim() || `Model discovery exited ${code}`));
|
|
677
|
+
});
|
|
678
|
+
});
|
|
679
|
+
}
|
|
680
|
+
async function subscriptionMetadata(provider, currentModel, currentEffort) {
|
|
681
|
+
let models;
|
|
682
|
+
if (provider === "claude") {
|
|
683
|
+
models = CLAUDE_MODELS;
|
|
684
|
+
}
|
|
685
|
+
else {
|
|
686
|
+
try {
|
|
687
|
+
models = parseCursorModels(await captureCliOutput(PROVIDERS.cursor.binary, ["--list-models"]));
|
|
688
|
+
}
|
|
689
|
+
catch {
|
|
690
|
+
models = [];
|
|
691
|
+
}
|
|
692
|
+
if (models.length === 0) {
|
|
693
|
+
models = [
|
|
694
|
+
{
|
|
695
|
+
model: "auto",
|
|
696
|
+
displayName: "Auto",
|
|
697
|
+
description: "Let Cursor choose the best available model.",
|
|
698
|
+
isDefault: true,
|
|
699
|
+
defaultReasoningEffort: "none",
|
|
700
|
+
supportedReasoningEfforts: [],
|
|
701
|
+
},
|
|
702
|
+
];
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
const fallback = models.find((model) => model.isDefault) ?? models[0];
|
|
706
|
+
const selected = models.find((model) => model.model === currentModel) ?? fallback;
|
|
707
|
+
const effort = selected.supportedReasoningEfforts.some((candidate) => candidate.reasoningEffort === currentEffort)
|
|
708
|
+
? currentEffort
|
|
709
|
+
: selected.defaultReasoningEffort;
|
|
710
|
+
return {
|
|
711
|
+
currentModel: selected.model,
|
|
712
|
+
currentReasoningEffort: effort,
|
|
713
|
+
models,
|
|
714
|
+
planType: "subscription",
|
|
715
|
+
rateLimits: [],
|
|
716
|
+
updatedAt: Date.now(),
|
|
717
|
+
provider: { id: provider, displayName: PROVIDERS[provider].displayName },
|
|
718
|
+
capabilities: {
|
|
719
|
+
modelSelection: true,
|
|
720
|
+
effortSelection: provider === "claude",
|
|
721
|
+
},
|
|
722
|
+
};
|
|
723
|
+
}
|
|
482
724
|
export function companionMcpTransport() {
|
|
483
725
|
const companionFile = fileURLToPath(import.meta.url);
|
|
484
726
|
const runningFromTypeScript = companionFile.endsWith(".ts");
|
|
@@ -528,7 +770,7 @@ function companionClaudeConfig() {
|
|
|
528
770
|
},
|
|
529
771
|
});
|
|
530
772
|
}
|
|
531
|
-
export function companionCliInvocation(provider, prompt, threadId) {
|
|
773
|
+
export function companionCliInvocation(provider, prompt, threadId, model = null, effort = null) {
|
|
532
774
|
const definition = PROVIDERS[provider];
|
|
533
775
|
if (provider === "claude") {
|
|
534
776
|
return {
|
|
@@ -548,6 +790,8 @@ export function companionCliInvocation(provider, prompt, threadId) {
|
|
|
548
790
|
"--mcp-config",
|
|
549
791
|
companionClaudeConfig(),
|
|
550
792
|
"--strict-mcp-config",
|
|
793
|
+
...(model && model !== "default" ? ["--model", model] : []),
|
|
794
|
+
...(effort && effort !== "none" ? ["--effort", effort] : []),
|
|
551
795
|
...(threadId ? ["--resume", threadId] : []),
|
|
552
796
|
],
|
|
553
797
|
stdin: prompt,
|
|
@@ -560,6 +804,7 @@ export function companionCliInvocation(provider, prompt, threadId) {
|
|
|
560
804
|
"--mode=ask",
|
|
561
805
|
"--output-format",
|
|
562
806
|
"stream-json",
|
|
807
|
+
...(model ? ["--model", model] : []),
|
|
563
808
|
...(threadId ? ["--resume", threadId] : []),
|
|
564
809
|
prompt,
|
|
565
810
|
],
|
|
@@ -588,7 +833,9 @@ function cliActivity(provider, event) {
|
|
|
588
833
|
if (!call || typeof call !== "object")
|
|
589
834
|
return "Used a Cursor tool";
|
|
590
835
|
const name = Object.keys(call)[0];
|
|
591
|
-
return name
|
|
836
|
+
return name
|
|
837
|
+
? `Used ${name.replace(/ToolCall$/, "")}`
|
|
838
|
+
: "Used a Cursor tool";
|
|
592
839
|
}
|
|
593
840
|
if (provider === "claude" && event.type === "assistant") {
|
|
594
841
|
const message = event.message;
|
|
@@ -600,9 +847,9 @@ function cliActivity(provider, event) {
|
|
|
600
847
|
}
|
|
601
848
|
return null;
|
|
602
849
|
}
|
|
603
|
-
async function runSubscriptionCliTurn({ provider, prompt, cwd, threadId, signal, send, }) {
|
|
850
|
+
async function runSubscriptionCliTurn({ provider, prompt, cwd, threadId, model, effort, signal, send, }) {
|
|
604
851
|
const definition = PROVIDERS[provider];
|
|
605
|
-
const invocation = companionCliInvocation(provider, prompt, threadId);
|
|
852
|
+
const invocation = companionCliInvocation(provider, prompt, threadId, model, effort);
|
|
606
853
|
send({ type: "agent", event: { type: "turn.started" } });
|
|
607
854
|
return await new Promise((resolve, reject) => {
|
|
608
855
|
const child = spawn(invocation.command, invocation.args, {
|
|
@@ -614,7 +861,10 @@ async function runSubscriptionCliTurn({ provider, prompt, cwd, threadId, signal,
|
|
|
614
861
|
let activeThreadId = threadId;
|
|
615
862
|
let finalText = "";
|
|
616
863
|
let usage;
|
|
617
|
-
const lines = readline.createInterface({
|
|
864
|
+
const lines = readline.createInterface({
|
|
865
|
+
input: child.stdout,
|
|
866
|
+
crlfDelay: Infinity,
|
|
867
|
+
});
|
|
618
868
|
lines.on("line", (line) => {
|
|
619
869
|
if (!line.trim())
|
|
620
870
|
return;
|
|
@@ -709,26 +959,59 @@ function companionThreadOptions(cwd, model, effort) {
|
|
|
709
959
|
modelReasoningEffort: effort,
|
|
710
960
|
};
|
|
711
961
|
}
|
|
962
|
+
const PROVIDER_IDS = Object.keys(PROVIDERS);
|
|
712
963
|
export async function runAgentCompanion(argv) {
|
|
713
964
|
const options = parseArgs(argv);
|
|
714
|
-
const provider = PROVIDERS[options.provider];
|
|
715
965
|
const allowedOrigin = new URL(options.appUrl).origin;
|
|
716
966
|
const token = crypto.randomBytes(32).toString("base64url");
|
|
717
|
-
|
|
718
|
-
const
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
967
|
+
const validation = validatedCompanionProviders();
|
|
968
|
+
const failures = validation.failures;
|
|
969
|
+
const runtimes = new Map();
|
|
970
|
+
for (const id of validation.available) {
|
|
971
|
+
runtimes.set(id, {
|
|
972
|
+
id,
|
|
973
|
+
codex: id === "codex"
|
|
974
|
+
? new Codex({
|
|
975
|
+
config: companionCodexConfig(),
|
|
976
|
+
env: subscriptionEnvironment(),
|
|
977
|
+
})
|
|
978
|
+
: null,
|
|
979
|
+
metadataClient: id === "codex" ? new CodexMetadataClient() : null,
|
|
980
|
+
thread: null,
|
|
981
|
+
cliThreadId: null,
|
|
982
|
+
selectedModel: null,
|
|
983
|
+
selectedEffort: null,
|
|
984
|
+
});
|
|
985
|
+
}
|
|
986
|
+
if (runtimes.size === 0) {
|
|
987
|
+
throw new Error([
|
|
988
|
+
"Nudge Agent could not find an authenticated coding-agent subscription.",
|
|
989
|
+
...PROVIDER_IDS.map((id) => `${PROVIDERS[id].displayName}: ${failures.get(id) ?? "unavailable"}`),
|
|
990
|
+
].join("\n"));
|
|
991
|
+
}
|
|
992
|
+
const defaultProviderId = options.preferredProvider && runtimes.has(options.preferredProvider)
|
|
993
|
+
? options.preferredProvider
|
|
994
|
+
: PROVIDER_IDS.find((id) => runtimes.has(id));
|
|
731
995
|
let activeAbort = null;
|
|
996
|
+
const providerInfo = (id) => ({
|
|
997
|
+
id,
|
|
998
|
+
displayName: PROVIDERS[id].displayName,
|
|
999
|
+
});
|
|
1000
|
+
const resolveRuntime = (value) => {
|
|
1001
|
+
const id = (value ?? defaultProviderId);
|
|
1002
|
+
return PROVIDER_IDS.includes(id) ? (runtimes.get(id) ?? null) : null;
|
|
1003
|
+
};
|
|
1004
|
+
const readMetadata = async (runtime) => {
|
|
1005
|
+
if (runtime.metadataClient) {
|
|
1006
|
+
const metadata = await runtime.metadataClient.metadata(runtime.selectedModel, runtime.selectedEffort);
|
|
1007
|
+
return {
|
|
1008
|
+
...metadata,
|
|
1009
|
+
provider: providerInfo(runtime.id),
|
|
1010
|
+
capabilities: { modelSelection: true, effortSelection: true },
|
|
1011
|
+
};
|
|
1012
|
+
}
|
|
1013
|
+
return subscriptionMetadata(runtime.id, runtime.selectedModel, runtime.selectedEffort);
|
|
1014
|
+
};
|
|
732
1015
|
const corsHeaders = (origin) => ({
|
|
733
1016
|
...(origin === allowedOrigin
|
|
734
1017
|
? { "Access-Control-Allow-Origin": allowedOrigin }
|
|
@@ -744,6 +1027,9 @@ export async function runAgentCompanion(argv) {
|
|
|
744
1027
|
void (async () => {
|
|
745
1028
|
const origin = req.headers.origin;
|
|
746
1029
|
const headers = corsHeaders(origin);
|
|
1030
|
+
const requestUrl = new URL(req.url ?? "/", "http://127.0.0.1");
|
|
1031
|
+
const runtime = resolveRuntime(requestUrl.searchParams.get("provider"));
|
|
1032
|
+
const provider = runtime ? PROVIDERS[runtime.id] : null;
|
|
747
1033
|
if (origin && origin !== allowedOrigin) {
|
|
748
1034
|
writeJson(res, 403, { error: "Origin not allowed" }, headers);
|
|
749
1035
|
return;
|
|
@@ -757,67 +1043,44 @@ export async function runAgentCompanion(argv) {
|
|
|
757
1043
|
writeJson(res, 401, { error: "Invalid companion token" }, headers);
|
|
758
1044
|
return;
|
|
759
1045
|
}
|
|
760
|
-
if (
|
|
1046
|
+
if (!runtime || !provider) {
|
|
1047
|
+
writeJson(res, 400, { error: "That coding-agent subscription is unavailable" }, headers);
|
|
1048
|
+
return;
|
|
1049
|
+
}
|
|
1050
|
+
if (req.method === "GET" && requestUrl.pathname === "/v1/health") {
|
|
761
1051
|
writeJson(res, 200, {
|
|
762
1052
|
ok: true,
|
|
763
1053
|
cwd: options.cwd,
|
|
764
1054
|
running: activeAbort !== null,
|
|
765
|
-
threadId:
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
1055
|
+
threadId: runtime.codex
|
|
1056
|
+
? (runtime.thread?.id ?? null)
|
|
1057
|
+
: runtime.cliThreadId,
|
|
1058
|
+
model: runtime.selectedModel,
|
|
1059
|
+
effort: runtime.selectedEffort,
|
|
1060
|
+
provider: providerInfo(runtime.id),
|
|
1061
|
+
providers: Array.from(runtimes.keys(), providerInfo),
|
|
769
1062
|
}, headers);
|
|
770
1063
|
return;
|
|
771
1064
|
}
|
|
772
|
-
if (req.method === "GET" &&
|
|
773
|
-
if (!metadataClient) {
|
|
774
|
-
writeJson(res, 200, {
|
|
775
|
-
currentModel: options.provider,
|
|
776
|
-
currentReasoningEffort: "none",
|
|
777
|
-
models: [
|
|
778
|
-
{
|
|
779
|
-
model: options.provider,
|
|
780
|
-
displayName: `${provider.displayName} default`,
|
|
781
|
-
description: `Uses the model selected by your ${provider.displayName} CLI account.`,
|
|
782
|
-
isDefault: true,
|
|
783
|
-
defaultReasoningEffort: "none",
|
|
784
|
-
supportedReasoningEfforts: [],
|
|
785
|
-
},
|
|
786
|
-
],
|
|
787
|
-
planType: "subscription",
|
|
788
|
-
rateLimits: [],
|
|
789
|
-
updatedAt: Date.now(),
|
|
790
|
-
provider: { id: options.provider, displayName: provider.displayName },
|
|
791
|
-
capabilities: { modelSelection: false, effortSelection: false },
|
|
792
|
-
}, headers);
|
|
793
|
-
return;
|
|
794
|
-
}
|
|
1065
|
+
if (req.method === "GET" && requestUrl.pathname === "/v1/metadata") {
|
|
795
1066
|
try {
|
|
796
|
-
const metadata = await
|
|
797
|
-
selectedModel = metadata.currentModel;
|
|
798
|
-
selectedEffort = metadata.currentReasoningEffort;
|
|
799
|
-
writeJson(res, 200,
|
|
800
|
-
...metadata,
|
|
801
|
-
provider: { id: options.provider, displayName: provider.displayName },
|
|
802
|
-
capabilities: { modelSelection: true, effortSelection: true },
|
|
803
|
-
}, headers);
|
|
1067
|
+
const metadata = await readMetadata(runtime);
|
|
1068
|
+
runtime.selectedModel = metadata.currentModel;
|
|
1069
|
+
runtime.selectedEffort = metadata.currentReasoningEffort;
|
|
1070
|
+
writeJson(res, 200, metadata, headers);
|
|
804
1071
|
}
|
|
805
1072
|
catch (error) {
|
|
806
1073
|
writeJson(res, 503, {
|
|
807
1074
|
error: error instanceof Error
|
|
808
1075
|
? error.message
|
|
809
|
-
:
|
|
1076
|
+
: `${provider.displayName} metadata is unavailable`,
|
|
810
1077
|
}, headers);
|
|
811
1078
|
}
|
|
812
1079
|
return;
|
|
813
1080
|
}
|
|
814
|
-
if (req.method === "POST" &&
|
|
815
|
-
if (!metadataClient) {
|
|
816
|
-
writeJson(res, 400, { error: `${provider.displayName} controls model selection in its own CLI.` }, headers);
|
|
817
|
-
return;
|
|
818
|
-
}
|
|
1081
|
+
if (req.method === "POST" && requestUrl.pathname === "/v1/model") {
|
|
819
1082
|
if (activeAbort) {
|
|
820
|
-
writeJson(res, 409, { error:
|
|
1083
|
+
writeJson(res, 409, { error: `A ${provider.displayName} turn is still running` }, headers);
|
|
821
1084
|
return;
|
|
822
1085
|
}
|
|
823
1086
|
let parsed;
|
|
@@ -829,25 +1092,29 @@ export async function runAgentCompanion(argv) {
|
|
|
829
1092
|
return;
|
|
830
1093
|
}
|
|
831
1094
|
const model = typeof parsed.model === "string" ? parsed.model.trim() : "";
|
|
832
|
-
const metadata = await
|
|
1095
|
+
const metadata = await readMetadata(runtime);
|
|
833
1096
|
const modelInfo = metadata.models.find((candidate) => candidate.model === model);
|
|
834
1097
|
if (!modelInfo) {
|
|
835
|
-
writeJson(res, 400, { error:
|
|
1098
|
+
writeJson(res, 400, { error: `That ${provider.displayName} model is unavailable` }, headers);
|
|
836
1099
|
return;
|
|
837
1100
|
}
|
|
838
|
-
selectedModel = model;
|
|
839
|
-
selectedEffort = modelInfo.defaultReasoningEffort;
|
|
840
|
-
thread = null;
|
|
841
|
-
|
|
1101
|
+
runtime.selectedModel = model;
|
|
1102
|
+
runtime.selectedEffort = modelInfo.defaultReasoningEffort;
|
|
1103
|
+
runtime.thread = null;
|
|
1104
|
+
runtime.cliThreadId = null;
|
|
1105
|
+
writeJson(res, 200, { ok: true, model, effort: runtime.selectedEffort }, headers);
|
|
842
1106
|
return;
|
|
843
1107
|
}
|
|
844
|
-
if (req.method === "POST" &&
|
|
845
|
-
if (
|
|
846
|
-
writeJson(res,
|
|
1108
|
+
if (req.method === "POST" && requestUrl.pathname === "/v1/effort") {
|
|
1109
|
+
if (activeAbort) {
|
|
1110
|
+
writeJson(res, 409, { error: `A ${provider.displayName} turn is still running` }, headers);
|
|
847
1111
|
return;
|
|
848
1112
|
}
|
|
849
|
-
|
|
850
|
-
|
|
1113
|
+
const metadata = await readMetadata(runtime);
|
|
1114
|
+
if (metadata.capabilities?.effortSelection === false) {
|
|
1115
|
+
writeJson(res, 400, {
|
|
1116
|
+
error: `${provider.displayName} controls reasoning settings in its own CLI.`,
|
|
1117
|
+
}, headers);
|
|
851
1118
|
return;
|
|
852
1119
|
}
|
|
853
1120
|
let parsed;
|
|
@@ -859,33 +1126,32 @@ export async function runAgentCompanion(argv) {
|
|
|
859
1126
|
return;
|
|
860
1127
|
}
|
|
861
1128
|
const effort = reasoningEffort(parsed.effort);
|
|
862
|
-
const metadata = await metadataClient.metadata(selectedModel, selectedEffort);
|
|
863
1129
|
const modelInfo = metadata.models.find((candidate) => candidate.model === metadata.currentModel);
|
|
864
1130
|
if (!effort ||
|
|
865
1131
|
!modelInfo?.supportedReasoningEfforts.some((candidate) => candidate.reasoningEffort === effort)) {
|
|
866
1132
|
writeJson(res, 400, { error: "That reasoning effort is unavailable" }, headers);
|
|
867
1133
|
return;
|
|
868
1134
|
}
|
|
869
|
-
selectedModel = metadata.currentModel;
|
|
870
|
-
selectedEffort = effort;
|
|
871
|
-
thread = null;
|
|
872
|
-
cliThreadId = null;
|
|
873
|
-
writeJson(res, 200, { ok: true, model: selectedModel, effort }, headers);
|
|
1135
|
+
runtime.selectedModel = metadata.currentModel;
|
|
1136
|
+
runtime.selectedEffort = effort;
|
|
1137
|
+
runtime.thread = null;
|
|
1138
|
+
runtime.cliThreadId = null;
|
|
1139
|
+
writeJson(res, 200, { ok: true, model: runtime.selectedModel, effort }, headers);
|
|
874
1140
|
return;
|
|
875
1141
|
}
|
|
876
|
-
if (req.method === "POST" &&
|
|
1142
|
+
if (req.method === "POST" && requestUrl.pathname === "/v1/new") {
|
|
877
1143
|
if (activeAbort) {
|
|
878
|
-
writeJson(res, 409, { error:
|
|
1144
|
+
writeJson(res, 409, { error: `A ${provider.displayName} turn is still running` }, headers);
|
|
879
1145
|
return;
|
|
880
1146
|
}
|
|
881
|
-
thread = null;
|
|
882
|
-
cliThreadId = null;
|
|
1147
|
+
runtime.thread = null;
|
|
1148
|
+
runtime.cliThreadId = null;
|
|
883
1149
|
writeJson(res, 200, { ok: true }, headers);
|
|
884
1150
|
return;
|
|
885
1151
|
}
|
|
886
|
-
if (req.method === "POST" &&
|
|
1152
|
+
if (req.method === "POST" && requestUrl.pathname === "/v1/resume") {
|
|
887
1153
|
if (activeAbort) {
|
|
888
|
-
writeJson(res, 409, { error:
|
|
1154
|
+
writeJson(res, 409, { error: `A ${provider.displayName} turn is still running` }, headers);
|
|
889
1155
|
return;
|
|
890
1156
|
}
|
|
891
1157
|
let parsed;
|
|
@@ -901,22 +1167,50 @@ export async function runAgentCompanion(argv) {
|
|
|
901
1167
|
writeJson(res, 400, { error: "Invalid coding-agent session id" }, headers);
|
|
902
1168
|
return;
|
|
903
1169
|
}
|
|
904
|
-
if (!metadataClient || !codex) {
|
|
905
|
-
|
|
906
|
-
|
|
1170
|
+
if (!runtime.metadataClient || !runtime.codex) {
|
|
1171
|
+
const metadata = await readMetadata(runtime);
|
|
1172
|
+
const requestedModel = typeof parsed.model === "string" ? parsed.model.trim() : "";
|
|
1173
|
+
const requestedEffort = reasoningEffort(parsed.effort);
|
|
1174
|
+
if (requestedModel &&
|
|
1175
|
+
!metadata.models.some((candidate) => candidate.model === requestedModel)) {
|
|
1176
|
+
writeJson(res, 400, { error: `That ${provider.displayName} model is unavailable` }, headers);
|
|
1177
|
+
return;
|
|
1178
|
+
}
|
|
1179
|
+
const nextModel = requestedModel || metadata.currentModel;
|
|
1180
|
+
const modelInfo = metadata.models.find((candidate) => candidate.model === nextModel);
|
|
1181
|
+
const cursorDefaultEffort = metadata.capabilities?.effortSelection === false &&
|
|
1182
|
+
requestedEffort === "none";
|
|
1183
|
+
if (parsed.effort !== undefined &&
|
|
1184
|
+
(!requestedEffort ||
|
|
1185
|
+
(!cursorDefaultEffort &&
|
|
1186
|
+
!modelInfo?.supportedReasoningEfforts.some((candidate) => candidate.reasoningEffort === requestedEffort)))) {
|
|
1187
|
+
writeJson(res, 400, { error: "That reasoning effort is unavailable" }, headers);
|
|
1188
|
+
return;
|
|
1189
|
+
}
|
|
1190
|
+
runtime.selectedModel = nextModel;
|
|
1191
|
+
runtime.selectedEffort =
|
|
1192
|
+
requestedEffort ?? modelInfo?.defaultReasoningEffort ?? "none";
|
|
1193
|
+
runtime.cliThreadId = threadId;
|
|
1194
|
+
writeJson(res, 200, {
|
|
1195
|
+
ok: true,
|
|
1196
|
+
threadId,
|
|
1197
|
+
model: runtime.selectedModel,
|
|
1198
|
+
effort: runtime.selectedEffort,
|
|
1199
|
+
}, headers);
|
|
907
1200
|
return;
|
|
908
1201
|
}
|
|
909
1202
|
const requestedModel = typeof parsed.model === "string" ? parsed.model.trim() : "";
|
|
910
1203
|
const requestedEffort = reasoningEffort(parsed.effort);
|
|
911
|
-
const metadata = await metadataClient.metadata(selectedModel, selectedEffort);
|
|
1204
|
+
const metadata = await runtime.metadataClient.metadata(runtime.selectedModel, runtime.selectedEffort);
|
|
912
1205
|
if (requestedModel) {
|
|
913
1206
|
if (!metadata.models.some((candidate) => candidate.model === requestedModel)) {
|
|
914
1207
|
writeJson(res, 400, { error: "That Codex model is unavailable" }, headers);
|
|
915
1208
|
return;
|
|
916
1209
|
}
|
|
917
|
-
selectedModel = requestedModel;
|
|
1210
|
+
runtime.selectedModel = requestedModel;
|
|
918
1211
|
}
|
|
919
|
-
const modelInfo = metadata.models.find((candidate) => candidate.model ===
|
|
1212
|
+
const modelInfo = metadata.models.find((candidate) => candidate.model ===
|
|
1213
|
+
(runtime.selectedModel ?? metadata.currentModel));
|
|
920
1214
|
if (parsed.effort !== undefined && !requestedEffort) {
|
|
921
1215
|
writeJson(res, 400, { error: "Invalid reasoning effort" }, headers);
|
|
922
1216
|
return;
|
|
@@ -926,17 +1220,23 @@ export async function runAgentCompanion(argv) {
|
|
|
926
1220
|
writeJson(res, 400, { error: "That reasoning effort is unavailable" }, headers);
|
|
927
1221
|
return;
|
|
928
1222
|
}
|
|
929
|
-
selectedEffort =
|
|
930
|
-
|
|
931
|
-
|
|
1223
|
+
runtime.selectedEffort =
|
|
1224
|
+
requestedEffort ?? modelInfo?.defaultReasoningEffort ?? null;
|
|
1225
|
+
runtime.thread = runtime.codex.resumeThread(threadId, companionThreadOptions(options.cwd, runtime.selectedModel, runtime.selectedEffort));
|
|
1226
|
+
writeJson(res, 200, {
|
|
1227
|
+
ok: true,
|
|
1228
|
+
threadId,
|
|
1229
|
+
model: runtime.selectedModel,
|
|
1230
|
+
effort: runtime.selectedEffort,
|
|
1231
|
+
}, headers);
|
|
932
1232
|
return;
|
|
933
1233
|
}
|
|
934
|
-
if (req.method === "POST" &&
|
|
1234
|
+
if (req.method === "POST" && requestUrl.pathname === "/v1/cancel") {
|
|
935
1235
|
activeAbort?.abort();
|
|
936
1236
|
writeJson(res, 200, { ok: true }, headers);
|
|
937
1237
|
return;
|
|
938
1238
|
}
|
|
939
|
-
if (req.method !== "POST" ||
|
|
1239
|
+
if (req.method !== "POST" || requestUrl.pathname !== "/v1/chat") {
|
|
940
1240
|
writeJson(res, 404, { error: "Not found" }, headers);
|
|
941
1241
|
return;
|
|
942
1242
|
}
|
|
@@ -957,8 +1257,8 @@ export async function runAgentCompanion(argv) {
|
|
|
957
1257
|
writeJson(res, 400, { error: "prompt is required" }, headers);
|
|
958
1258
|
return;
|
|
959
1259
|
}
|
|
960
|
-
if (codex) {
|
|
961
|
-
thread ??= codex.startThread(companionThreadOptions(options.cwd, selectedModel, selectedEffort));
|
|
1260
|
+
if (runtime.codex) {
|
|
1261
|
+
runtime.thread ??= runtime.codex.startThread(companionThreadOptions(options.cwd, runtime.selectedModel, runtime.selectedEffort));
|
|
962
1262
|
}
|
|
963
1263
|
const abort = new AbortController();
|
|
964
1264
|
activeAbort = abort;
|
|
@@ -977,11 +1277,17 @@ export async function runAgentCompanion(argv) {
|
|
|
977
1277
|
res.write(`${JSON.stringify(value)}\n`);
|
|
978
1278
|
};
|
|
979
1279
|
try {
|
|
980
|
-
const currentThreadId = codex
|
|
981
|
-
|
|
1280
|
+
const currentThreadId = runtime.codex
|
|
1281
|
+
? (runtime.thread?.id ?? null)
|
|
1282
|
+
: runtime.cliThreadId;
|
|
1283
|
+
send({
|
|
1284
|
+
type: "connected",
|
|
1285
|
+
cwd: options.cwd,
|
|
1286
|
+
threadId: currentThreadId,
|
|
1287
|
+
});
|
|
982
1288
|
const contextualPrompt = buildCompanionPrompt(prompt, parsed.context);
|
|
983
|
-
if (codex && thread) {
|
|
984
|
-
const streamed = await thread.runStreamed(contextualPrompt, {
|
|
1289
|
+
if (runtime.codex && runtime.thread) {
|
|
1290
|
+
const streamed = await runtime.thread.runStreamed(contextualPrompt, {
|
|
985
1291
|
signal: abort.signal,
|
|
986
1292
|
});
|
|
987
1293
|
for await (const event of streamed.events) {
|
|
@@ -989,16 +1295,23 @@ export async function runAgentCompanion(argv) {
|
|
|
989
1295
|
}
|
|
990
1296
|
}
|
|
991
1297
|
else {
|
|
992
|
-
cliThreadId = await runSubscriptionCliTurn({
|
|
993
|
-
provider:
|
|
1298
|
+
runtime.cliThreadId = await runSubscriptionCliTurn({
|
|
1299
|
+
provider: runtime.id,
|
|
994
1300
|
prompt: contextualPrompt,
|
|
995
1301
|
cwd: options.cwd,
|
|
996
|
-
threadId: cliThreadId,
|
|
1302
|
+
threadId: runtime.cliThreadId,
|
|
1303
|
+
model: runtime.selectedModel,
|
|
1304
|
+
effort: runtime.selectedEffort,
|
|
997
1305
|
signal: abort.signal,
|
|
998
1306
|
send,
|
|
999
1307
|
});
|
|
1000
1308
|
}
|
|
1001
|
-
send({
|
|
1309
|
+
send({
|
|
1310
|
+
type: "done",
|
|
1311
|
+
threadId: runtime.codex
|
|
1312
|
+
? (runtime.thread?.id ?? null)
|
|
1313
|
+
: runtime.cliThreadId,
|
|
1314
|
+
});
|
|
1002
1315
|
}
|
|
1003
1316
|
catch (error) {
|
|
1004
1317
|
send({
|
|
@@ -1024,12 +1337,13 @@ export async function runAgentCompanion(argv) {
|
|
|
1024
1337
|
const pairing = Buffer.from(JSON.stringify({
|
|
1025
1338
|
port,
|
|
1026
1339
|
token,
|
|
1027
|
-
provider:
|
|
1340
|
+
provider: providerInfo(defaultProviderId),
|
|
1341
|
+
providers: Array.from(runtimes.keys(), providerInfo),
|
|
1028
1342
|
}), "utf8").toString("base64url");
|
|
1029
1343
|
const url = companionPairingUrl(options.appUrl, pairing);
|
|
1030
1344
|
console.log("");
|
|
1031
1345
|
console.log("Nudge Agent companion is running");
|
|
1032
|
-
console.log(`
|
|
1346
|
+
console.log(` Agents: ${Array.from(runtimes.keys(), (id) => PROVIDERS[id].displayName).join(", ")}`);
|
|
1033
1347
|
console.log(` App: ${options.appUrl}`);
|
|
1034
1348
|
console.log(` Workspace: ${options.cwd}`);
|
|
1035
1349
|
console.log(` Listener: http://127.0.0.1:${port}`);
|
|
@@ -1038,7 +1352,15 @@ export async function runAgentCompanion(argv) {
|
|
|
1038
1352
|
}
|
|
1039
1353
|
console.log(" Files: read-only");
|
|
1040
1354
|
console.log("");
|
|
1041
|
-
|
|
1355
|
+
for (const id of PROVIDER_IDS) {
|
|
1356
|
+
const definition = PROVIDERS[id];
|
|
1357
|
+
if (runtimes.has(id)) {
|
|
1358
|
+
console.log(` ready ${definition.displayName} — reusing \`${definition.loginCommand}\``);
|
|
1359
|
+
}
|
|
1360
|
+
else {
|
|
1361
|
+
console.log(` unavailable ${definition.displayName} — ${failures.get(id) ?? "not found"}`);
|
|
1362
|
+
}
|
|
1363
|
+
}
|
|
1042
1364
|
console.log("Keep this terminal open while using Nudge chat.");
|
|
1043
1365
|
console.log("");
|
|
1044
1366
|
if (options.openBrowser)
|
|
@@ -1048,7 +1370,8 @@ export async function runAgentCompanion(argv) {
|
|
|
1048
1370
|
await new Promise((resolve) => {
|
|
1049
1371
|
const shutdown = () => {
|
|
1050
1372
|
activeAbort?.abort();
|
|
1051
|
-
|
|
1373
|
+
for (const runtime of runtimes.values())
|
|
1374
|
+
runtime.metadataClient?.stop();
|
|
1052
1375
|
server.close(() => resolve());
|
|
1053
1376
|
};
|
|
1054
1377
|
process.once("SIGINT", shutdown);
|