@euqns/nudge-mcp 0.16.0 → 1.1.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 +21 -16
- package/dist/codex-companion.js +531 -127
- package/dist/codex-companion.js.map +1 -1
- package/dist/codex-companion.test.js +40 -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,8 +1,10 @@
|
|
|
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
|
+
import { readdir, realpath, stat } from "node:fs/promises";
|
|
4
5
|
import http from "node:http";
|
|
5
6
|
import { createRequire } from "node:module";
|
|
7
|
+
import os from "node:os";
|
|
6
8
|
import path from "node:path";
|
|
7
9
|
import readline from "node:readline";
|
|
8
10
|
import { fileURLToPath } from "node:url";
|
|
@@ -27,6 +29,62 @@ const PROVIDERS = {
|
|
|
27
29
|
loginCommand: "cursor-agent login",
|
|
28
30
|
},
|
|
29
31
|
};
|
|
32
|
+
const CLAUDE_EFFORTS = [
|
|
33
|
+
{
|
|
34
|
+
reasoningEffort: "low",
|
|
35
|
+
description: "Faster responses with lighter reasoning.",
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
reasoningEffort: "medium",
|
|
39
|
+
description: "Balanced reasoning for everyday work.",
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
reasoningEffort: "high",
|
|
43
|
+
description: "More reasoning for difficult tasks.",
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
reasoningEffort: "xhigh",
|
|
47
|
+
description: "Extended reasoning for complex tasks.",
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
reasoningEffort: "max",
|
|
51
|
+
description: "Claude Code's maximum reasoning effort.",
|
|
52
|
+
},
|
|
53
|
+
];
|
|
54
|
+
const CLAUDE_MODELS = [
|
|
55
|
+
{
|
|
56
|
+
model: "default",
|
|
57
|
+
displayName: "Claude Code default",
|
|
58
|
+
description: "Use the default model from your Claude Code account.",
|
|
59
|
+
isDefault: true,
|
|
60
|
+
defaultReasoningEffort: "medium",
|
|
61
|
+
supportedReasoningEfforts: CLAUDE_EFFORTS,
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
model: "sonnet",
|
|
65
|
+
displayName: "Sonnet",
|
|
66
|
+
description: "Claude Code's balanced model alias.",
|
|
67
|
+
isDefault: false,
|
|
68
|
+
defaultReasoningEffort: "medium",
|
|
69
|
+
supportedReasoningEfforts: CLAUDE_EFFORTS,
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
model: "opus",
|
|
73
|
+
displayName: "Opus",
|
|
74
|
+
description: "Claude Code's most capable model alias.",
|
|
75
|
+
isDefault: false,
|
|
76
|
+
defaultReasoningEffort: "high",
|
|
77
|
+
supportedReasoningEfforts: CLAUDE_EFFORTS,
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
model: "haiku",
|
|
81
|
+
displayName: "Haiku",
|
|
82
|
+
description: "Claude Code's fastest model alias.",
|
|
83
|
+
isDefault: false,
|
|
84
|
+
defaultReasoningEffort: "low",
|
|
85
|
+
supportedReasoningEfforts: CLAUDE_EFFORTS,
|
|
86
|
+
},
|
|
87
|
+
];
|
|
30
88
|
const require = createRequire(import.meta.url);
|
|
31
89
|
const REASONING_EFFORTS = new Set([
|
|
32
90
|
"none",
|
|
@@ -39,7 +97,8 @@ const REASONING_EFFORTS = new Set([
|
|
|
39
97
|
"ultra",
|
|
40
98
|
]);
|
|
41
99
|
function reasoningEffort(value) {
|
|
42
|
-
return typeof value === "string" &&
|
|
100
|
+
return typeof value === "string" &&
|
|
101
|
+
REASONING_EFFORTS.has(value)
|
|
43
102
|
? value
|
|
44
103
|
: null;
|
|
45
104
|
}
|
|
@@ -77,7 +136,10 @@ class CodexMetadataClient {
|
|
|
77
136
|
});
|
|
78
137
|
this.child = child;
|
|
79
138
|
this.stderr = "";
|
|
80
|
-
const lines = readline.createInterface({
|
|
139
|
+
const lines = readline.createInterface({
|
|
140
|
+
input: child.stdout,
|
|
141
|
+
crlfDelay: Infinity,
|
|
142
|
+
});
|
|
81
143
|
lines.on("line", (line) => this.handleLine(line));
|
|
82
144
|
child.stderr.on("data", (chunk) => {
|
|
83
145
|
this.stderr = `${this.stderr}${chunk.toString("utf8")}`.slice(-4_000);
|
|
@@ -164,7 +226,9 @@ class CodexMetadataClient {
|
|
|
164
226
|
.filter((model) => typeof model.model === "string" && model.model.length > 0)
|
|
165
227
|
.map((model) => ({
|
|
166
228
|
model: model.model,
|
|
167
|
-
displayName: typeof model.displayName === "string"
|
|
229
|
+
displayName: typeof model.displayName === "string"
|
|
230
|
+
? model.displayName
|
|
231
|
+
: model.model,
|
|
168
232
|
description: typeof model.description === "string" ? model.description : "",
|
|
169
233
|
isDefault: model.isDefault === true,
|
|
170
234
|
defaultReasoningEffort: reasoningEffort(model.defaultReasoningEffort) ?? "medium",
|
|
@@ -176,10 +240,14 @@ class CodexMetadataClient {
|
|
|
176
240
|
const effort = reasoningEffort(entry.reasoningEffort);
|
|
177
241
|
if (!effort)
|
|
178
242
|
return [];
|
|
179
|
-
return [
|
|
243
|
+
return [
|
|
244
|
+
{
|
|
180
245
|
reasoningEffort: effort,
|
|
181
|
-
description: typeof entry.description === "string"
|
|
182
|
-
|
|
246
|
+
description: typeof entry.description === "string"
|
|
247
|
+
? entry.description
|
|
248
|
+
: "",
|
|
249
|
+
},
|
|
250
|
+
];
|
|
183
251
|
})
|
|
184
252
|
: [],
|
|
185
253
|
}));
|
|
@@ -221,7 +289,7 @@ class CodexMetadataClient {
|
|
|
221
289
|
currentReasoningEffort: selectedEffort,
|
|
222
290
|
models,
|
|
223
291
|
planType: account.account?.type === "chatgpt"
|
|
224
|
-
? account.account.planType ?? null
|
|
292
|
+
? (account.account.planType ?? null)
|
|
225
293
|
: null,
|
|
226
294
|
rateLimits,
|
|
227
295
|
updatedAt: Date.now(),
|
|
@@ -244,7 +312,7 @@ function parseArgs(argv) {
|
|
|
244
312
|
let openBrowserOnStart = true;
|
|
245
313
|
let port = DEFAULT_PORT;
|
|
246
314
|
let portWasExplicit = false;
|
|
247
|
-
let
|
|
315
|
+
let preferredProvider;
|
|
248
316
|
for (let index = 0; index < argv.length; index++) {
|
|
249
317
|
const arg = argv[index];
|
|
250
318
|
const next = argv[index + 1];
|
|
@@ -278,7 +346,7 @@ function parseArgs(argv) {
|
|
|
278
346
|
if (!(next in PROVIDERS)) {
|
|
279
347
|
throw new Error("--provider must be codex, claude, or cursor");
|
|
280
348
|
}
|
|
281
|
-
|
|
349
|
+
preferredProvider = next;
|
|
282
350
|
index++;
|
|
283
351
|
}
|
|
284
352
|
else if (arg.startsWith("--provider=")) {
|
|
@@ -286,7 +354,7 @@ function parseArgs(argv) {
|
|
|
286
354
|
if (!(value in PROVIDERS)) {
|
|
287
355
|
throw new Error("--provider must be codex, claude, or cursor");
|
|
288
356
|
}
|
|
289
|
-
|
|
357
|
+
preferredProvider = value;
|
|
290
358
|
}
|
|
291
359
|
else {
|
|
292
360
|
throw new Error(`Unknown agent companion option: ${arg}`);
|
|
@@ -301,7 +369,7 @@ function parseArgs(argv) {
|
|
|
301
369
|
openBrowser: openBrowserOnStart,
|
|
302
370
|
port,
|
|
303
371
|
portWasExplicit,
|
|
304
|
-
|
|
372
|
+
preferredProvider,
|
|
305
373
|
};
|
|
306
374
|
}
|
|
307
375
|
async function listenOnLoopback(server, preferredPort, allowFallback) {
|
|
@@ -365,8 +433,44 @@ function writeJson(res, status, value, headers) {
|
|
|
365
433
|
res.writeHead(status, { ...headers, "Content-Type": "application/json" });
|
|
366
434
|
res.end(JSON.stringify(value));
|
|
367
435
|
}
|
|
436
|
+
export function resolveCompanionWorkspacePath(value, currentCwd, homeDirectory = os.homedir()) {
|
|
437
|
+
const input = value.trim();
|
|
438
|
+
if (!input || input.includes("\0") || input.length > 4_096) {
|
|
439
|
+
throw new Error("Enter a valid folder path");
|
|
440
|
+
}
|
|
441
|
+
if (input === "~")
|
|
442
|
+
return path.resolve(homeDirectory);
|
|
443
|
+
if (input.startsWith("~/") || input.startsWith("~\\")) {
|
|
444
|
+
return path.resolve(homeDirectory, input.slice(2));
|
|
445
|
+
}
|
|
446
|
+
return path.resolve(currentCwd, input);
|
|
447
|
+
}
|
|
448
|
+
export async function browseCompanionWorkspace(value, currentCwd) {
|
|
449
|
+
const requested = resolveCompanionWorkspacePath(value, currentCwd);
|
|
450
|
+
const info = await stat(requested);
|
|
451
|
+
if (!info.isDirectory())
|
|
452
|
+
throw new Error("That path is not a folder");
|
|
453
|
+
const cwd = await realpath(requested);
|
|
454
|
+
const entries = await readdir(cwd, { withFileTypes: true });
|
|
455
|
+
const directories = entries
|
|
456
|
+
.filter((entry) => entry.isDirectory())
|
|
457
|
+
.map((entry) => ({ name: entry.name, path: path.join(cwd, entry.name) }))
|
|
458
|
+
.sort((left, right) => left.name.localeCompare(right.name, undefined, {
|
|
459
|
+
numeric: true,
|
|
460
|
+
sensitivity: "base",
|
|
461
|
+
}));
|
|
462
|
+
const root = path.parse(cwd).root;
|
|
463
|
+
return {
|
|
464
|
+
cwd,
|
|
465
|
+
parent: cwd === root ? null : path.dirname(cwd),
|
|
466
|
+
directories: directories.slice(0, 500),
|
|
467
|
+
truncated: directories.length > 500,
|
|
468
|
+
};
|
|
469
|
+
}
|
|
368
470
|
export function buildCompanionPrompt(prompt, context) {
|
|
369
|
-
const pathname = typeof context?.pathname === "string"
|
|
471
|
+
const pathname = typeof context?.pathname === "string"
|
|
472
|
+
? context.pathname.slice(0, 500)
|
|
473
|
+
: undefined;
|
|
370
474
|
const routeContext = pathname
|
|
371
475
|
? `The user opened this chat from the Nudge route ${JSON.stringify(pathname)}.`
|
|
372
476
|
: "The user opened this chat from Nudge.";
|
|
@@ -385,11 +489,15 @@ export function buildCompanionPrompt(prompt, context) {
|
|
|
385
489
|
context.boardRole === "viewer"
|
|
386
490
|
? context.boardRole
|
|
387
491
|
: undefined,
|
|
388
|
-
view: typeof context.view === "string"
|
|
492
|
+
view: typeof context.view === "string"
|
|
493
|
+
? context.view.slice(0, 50)
|
|
494
|
+
: undefined,
|
|
389
495
|
canvasId: typeof context.canvasId === "string"
|
|
390
496
|
? context.canvasId.slice(0, 200)
|
|
391
497
|
: undefined,
|
|
392
|
-
cardId: typeof context.cardId === "string"
|
|
498
|
+
cardId: typeof context.cardId === "string"
|
|
499
|
+
? context.cardId.slice(0, 200)
|
|
500
|
+
: undefined,
|
|
393
501
|
cardTitle: typeof context.cardTitle === "string"
|
|
394
502
|
? context.cardTitle.slice(0, 500)
|
|
395
503
|
: undefined,
|
|
@@ -399,7 +507,9 @@ export function buildCompanionPrompt(prompt, context) {
|
|
|
399
507
|
cardHref: typeof context.cardHref === "string"
|
|
400
508
|
? context.cardHref.slice(0, 1_000)
|
|
401
509
|
: undefined,
|
|
402
|
-
listId: typeof context.listId === "string"
|
|
510
|
+
listId: typeof context.listId === "string"
|
|
511
|
+
? context.listId.slice(0, 200)
|
|
512
|
+
: undefined,
|
|
403
513
|
listTitle: typeof context.listTitle === "string"
|
|
404
514
|
? context.listTitle.slice(0, 500)
|
|
405
515
|
: undefined,
|
|
@@ -409,8 +519,12 @@ export function buildCompanionPrompt(prompt, context) {
|
|
|
409
519
|
repository: context.repository && typeof context.repository === "object"
|
|
410
520
|
? context.repository
|
|
411
521
|
: undefined,
|
|
412
|
-
listCount: typeof context.listCount === "number"
|
|
413
|
-
|
|
522
|
+
listCount: typeof context.listCount === "number"
|
|
523
|
+
? context.listCount
|
|
524
|
+
: undefined,
|
|
525
|
+
cardCount: typeof context.cardCount === "number"
|
|
526
|
+
? context.cardCount
|
|
527
|
+
: undefined,
|
|
414
528
|
}),
|
|
415
529
|
"Use this exact board id for Nudge MCP calls unless the user names another board.",
|
|
416
530
|
context.cardId
|
|
@@ -469,6 +583,20 @@ export function assertCompanionProvider(provider) {
|
|
|
469
583
|
throw new Error(`${definition.displayName} is not authenticated. Run \`${definition.loginCommand}\`.${detail ? `\nCurrent status: ${detail}` : ""}`);
|
|
470
584
|
}
|
|
471
585
|
}
|
|
586
|
+
export function validatedCompanionProviders(validate = assertCompanionProvider) {
|
|
587
|
+
const available = [];
|
|
588
|
+
const failures = new Map();
|
|
589
|
+
for (const provider of Object.keys(PROVIDERS)) {
|
|
590
|
+
try {
|
|
591
|
+
validate(provider);
|
|
592
|
+
available.push(provider);
|
|
593
|
+
}
|
|
594
|
+
catch (error) {
|
|
595
|
+
failures.set(provider, error instanceof Error ? error.message : String(error));
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
return { available, failures };
|
|
599
|
+
}
|
|
472
600
|
export function subscriptionEnvironment(environment = process.env) {
|
|
473
601
|
const blocked = new Set([
|
|
474
602
|
"ANTHROPIC_API_KEY",
|
|
@@ -479,6 +607,156 @@ export function subscriptionEnvironment(environment = process.env) {
|
|
|
479
607
|
]);
|
|
480
608
|
return Object.fromEntries(Object.entries(environment).filter((entry) => entry[1] !== undefined && !blocked.has(entry[0])));
|
|
481
609
|
}
|
|
610
|
+
function modelDisplayName(model) {
|
|
611
|
+
if (model === "auto")
|
|
612
|
+
return "Auto";
|
|
613
|
+
return model
|
|
614
|
+
.replace(/[-_]+/g, " ")
|
|
615
|
+
.replace(/\b\w/g, (letter) => letter.toUpperCase());
|
|
616
|
+
}
|
|
617
|
+
function cursorModel(candidate) {
|
|
618
|
+
const record = candidate && typeof candidate === "object"
|
|
619
|
+
? candidate
|
|
620
|
+
: null;
|
|
621
|
+
const rawModel = typeof candidate === "string"
|
|
622
|
+
? candidate
|
|
623
|
+
: typeof record?.model === "string"
|
|
624
|
+
? record.model
|
|
625
|
+
: typeof record?.id === "string"
|
|
626
|
+
? record.id
|
|
627
|
+
: typeof record?.value === "string"
|
|
628
|
+
? record.value
|
|
629
|
+
: "";
|
|
630
|
+
const model = rawModel.trim();
|
|
631
|
+
if (!/^[a-zA-Z0-9][a-zA-Z0-9._:/\[\]=,-]*$/.test(model))
|
|
632
|
+
return null;
|
|
633
|
+
const displayName = typeof record?.displayName === "string"
|
|
634
|
+
? record.displayName
|
|
635
|
+
: typeof record?.name === "string"
|
|
636
|
+
? record.name
|
|
637
|
+
: modelDisplayName(model);
|
|
638
|
+
return {
|
|
639
|
+
model,
|
|
640
|
+
displayName,
|
|
641
|
+
description: "Available to your Cursor account.",
|
|
642
|
+
isDefault: record?.isDefault === true || model === "auto",
|
|
643
|
+
defaultReasoningEffort: "none",
|
|
644
|
+
supportedReasoningEfforts: [],
|
|
645
|
+
};
|
|
646
|
+
}
|
|
647
|
+
/** Parse both current line output and future JSON output from Cursor's model command. */
|
|
648
|
+
export function parseCursorModels(output) {
|
|
649
|
+
const trimmed = output.trim();
|
|
650
|
+
let candidates = [];
|
|
651
|
+
if (trimmed.startsWith("[") || trimmed.startsWith("{")) {
|
|
652
|
+
try {
|
|
653
|
+
const parsed = JSON.parse(trimmed);
|
|
654
|
+
if (Array.isArray(parsed))
|
|
655
|
+
candidates = parsed;
|
|
656
|
+
else if (parsed && typeof parsed === "object") {
|
|
657
|
+
const record = parsed;
|
|
658
|
+
if (Array.isArray(record.models))
|
|
659
|
+
candidates = record.models;
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
catch {
|
|
663
|
+
// Older Cursor releases print one model id per line.
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
if (candidates.length === 0) {
|
|
667
|
+
candidates = output
|
|
668
|
+
.replace(/\u001b\[[0-9;]*m/g, "")
|
|
669
|
+
.split(/\r?\n/)
|
|
670
|
+
.map((line) => line.trim())
|
|
671
|
+
.filter((line) => line.length > 0)
|
|
672
|
+
.filter((line) => !/^(available\s+)?models?:?$/i.test(line))
|
|
673
|
+
.map((line) => line
|
|
674
|
+
.replace(/^[*✓●>•-]\s*/, "")
|
|
675
|
+
.replace(/\s+\((?:default|current)\)$/i, ""))
|
|
676
|
+
.map((line) => line.split(/\t|\s{2,}/, 1)[0]);
|
|
677
|
+
}
|
|
678
|
+
const models = candidates
|
|
679
|
+
.map((candidate) => cursorModel(candidate))
|
|
680
|
+
.filter((model) => model !== null);
|
|
681
|
+
const deduplicated = Array.from(new Map(models.map((model) => [model.model, model])).values());
|
|
682
|
+
if (deduplicated.length > 0 &&
|
|
683
|
+
!deduplicated.some((model) => model.isDefault)) {
|
|
684
|
+
deduplicated[0] = { ...deduplicated[0], isDefault: true };
|
|
685
|
+
}
|
|
686
|
+
return deduplicated;
|
|
687
|
+
}
|
|
688
|
+
function captureCliOutput(command, args) {
|
|
689
|
+
return new Promise((resolve, reject) => {
|
|
690
|
+
const child = spawn(command, args, {
|
|
691
|
+
env: subscriptionEnvironment(),
|
|
692
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
693
|
+
});
|
|
694
|
+
let stdout = "";
|
|
695
|
+
let stderr = "";
|
|
696
|
+
const timer = setTimeout(() => child.kill("SIGTERM"), 8_000);
|
|
697
|
+
child.stdout.on("data", (chunk) => {
|
|
698
|
+
stdout = `${stdout}${chunk.toString("utf8")}`.slice(-256_000);
|
|
699
|
+
});
|
|
700
|
+
child.stderr.on("data", (chunk) => {
|
|
701
|
+
stderr = `${stderr}${chunk.toString("utf8")}`.slice(-8_000);
|
|
702
|
+
});
|
|
703
|
+
child.once("error", (error) => {
|
|
704
|
+
clearTimeout(timer);
|
|
705
|
+
reject(error);
|
|
706
|
+
});
|
|
707
|
+
child.once("close", (code) => {
|
|
708
|
+
clearTimeout(timer);
|
|
709
|
+
if (code === 0)
|
|
710
|
+
resolve(stdout);
|
|
711
|
+
else
|
|
712
|
+
reject(new Error(stderr.trim() || `Model discovery exited ${code}`));
|
|
713
|
+
});
|
|
714
|
+
});
|
|
715
|
+
}
|
|
716
|
+
async function subscriptionMetadata(provider, currentModel, currentEffort) {
|
|
717
|
+
let models;
|
|
718
|
+
if (provider === "claude") {
|
|
719
|
+
models = CLAUDE_MODELS;
|
|
720
|
+
}
|
|
721
|
+
else {
|
|
722
|
+
try {
|
|
723
|
+
models = parseCursorModels(await captureCliOutput(PROVIDERS.cursor.binary, ["--list-models"]));
|
|
724
|
+
}
|
|
725
|
+
catch {
|
|
726
|
+
models = [];
|
|
727
|
+
}
|
|
728
|
+
if (models.length === 0) {
|
|
729
|
+
models = [
|
|
730
|
+
{
|
|
731
|
+
model: "auto",
|
|
732
|
+
displayName: "Auto",
|
|
733
|
+
description: "Let Cursor choose the best available model.",
|
|
734
|
+
isDefault: true,
|
|
735
|
+
defaultReasoningEffort: "none",
|
|
736
|
+
supportedReasoningEfforts: [],
|
|
737
|
+
},
|
|
738
|
+
];
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
const fallback = models.find((model) => model.isDefault) ?? models[0];
|
|
742
|
+
const selected = models.find((model) => model.model === currentModel) ?? fallback;
|
|
743
|
+
const effort = selected.supportedReasoningEfforts.some((candidate) => candidate.reasoningEffort === currentEffort)
|
|
744
|
+
? currentEffort
|
|
745
|
+
: selected.defaultReasoningEffort;
|
|
746
|
+
return {
|
|
747
|
+
currentModel: selected.model,
|
|
748
|
+
currentReasoningEffort: effort,
|
|
749
|
+
models,
|
|
750
|
+
planType: "subscription",
|
|
751
|
+
rateLimits: [],
|
|
752
|
+
updatedAt: Date.now(),
|
|
753
|
+
provider: { id: provider, displayName: PROVIDERS[provider].displayName },
|
|
754
|
+
capabilities: {
|
|
755
|
+
modelSelection: true,
|
|
756
|
+
effortSelection: provider === "claude",
|
|
757
|
+
},
|
|
758
|
+
};
|
|
759
|
+
}
|
|
482
760
|
export function companionMcpTransport() {
|
|
483
761
|
const companionFile = fileURLToPath(import.meta.url);
|
|
484
762
|
const runningFromTypeScript = companionFile.endsWith(".ts");
|
|
@@ -528,7 +806,7 @@ function companionClaudeConfig() {
|
|
|
528
806
|
},
|
|
529
807
|
});
|
|
530
808
|
}
|
|
531
|
-
export function companionCliInvocation(provider, prompt, threadId) {
|
|
809
|
+
export function companionCliInvocation(provider, prompt, threadId, model = null, effort = null) {
|
|
532
810
|
const definition = PROVIDERS[provider];
|
|
533
811
|
if (provider === "claude") {
|
|
534
812
|
return {
|
|
@@ -548,6 +826,8 @@ export function companionCliInvocation(provider, prompt, threadId) {
|
|
|
548
826
|
"--mcp-config",
|
|
549
827
|
companionClaudeConfig(),
|
|
550
828
|
"--strict-mcp-config",
|
|
829
|
+
...(model && model !== "default" ? ["--model", model] : []),
|
|
830
|
+
...(effort && effort !== "none" ? ["--effort", effort] : []),
|
|
551
831
|
...(threadId ? ["--resume", threadId] : []),
|
|
552
832
|
],
|
|
553
833
|
stdin: prompt,
|
|
@@ -560,6 +840,7 @@ export function companionCliInvocation(provider, prompt, threadId) {
|
|
|
560
840
|
"--mode=ask",
|
|
561
841
|
"--output-format",
|
|
562
842
|
"stream-json",
|
|
843
|
+
...(model ? ["--model", model] : []),
|
|
563
844
|
...(threadId ? ["--resume", threadId] : []),
|
|
564
845
|
prompt,
|
|
565
846
|
],
|
|
@@ -588,7 +869,9 @@ function cliActivity(provider, event) {
|
|
|
588
869
|
if (!call || typeof call !== "object")
|
|
589
870
|
return "Used a Cursor tool";
|
|
590
871
|
const name = Object.keys(call)[0];
|
|
591
|
-
return name
|
|
872
|
+
return name
|
|
873
|
+
? `Used ${name.replace(/ToolCall$/, "")}`
|
|
874
|
+
: "Used a Cursor tool";
|
|
592
875
|
}
|
|
593
876
|
if (provider === "claude" && event.type === "assistant") {
|
|
594
877
|
const message = event.message;
|
|
@@ -600,9 +883,9 @@ function cliActivity(provider, event) {
|
|
|
600
883
|
}
|
|
601
884
|
return null;
|
|
602
885
|
}
|
|
603
|
-
async function runSubscriptionCliTurn({ provider, prompt, cwd, threadId, signal, send, }) {
|
|
886
|
+
async function runSubscriptionCliTurn({ provider, prompt, cwd, threadId, model, effort, signal, send, }) {
|
|
604
887
|
const definition = PROVIDERS[provider];
|
|
605
|
-
const invocation = companionCliInvocation(provider, prompt, threadId);
|
|
888
|
+
const invocation = companionCliInvocation(provider, prompt, threadId, model, effort);
|
|
606
889
|
send({ type: "agent", event: { type: "turn.started" } });
|
|
607
890
|
return await new Promise((resolve, reject) => {
|
|
608
891
|
const child = spawn(invocation.command, invocation.args, {
|
|
@@ -614,7 +897,10 @@ async function runSubscriptionCliTurn({ provider, prompt, cwd, threadId, signal,
|
|
|
614
897
|
let activeThreadId = threadId;
|
|
615
898
|
let finalText = "";
|
|
616
899
|
let usage;
|
|
617
|
-
const lines = readline.createInterface({
|
|
900
|
+
const lines = readline.createInterface({
|
|
901
|
+
input: child.stdout,
|
|
902
|
+
crlfDelay: Infinity,
|
|
903
|
+
});
|
|
618
904
|
lines.on("line", (line) => {
|
|
619
905
|
if (!line.trim())
|
|
620
906
|
return;
|
|
@@ -709,26 +995,66 @@ function companionThreadOptions(cwd, model, effort) {
|
|
|
709
995
|
modelReasoningEffort: effort,
|
|
710
996
|
};
|
|
711
997
|
}
|
|
998
|
+
const PROVIDER_IDS = Object.keys(PROVIDERS);
|
|
712
999
|
export async function runAgentCompanion(argv) {
|
|
713
1000
|
const options = parseArgs(argv);
|
|
714
|
-
|
|
1001
|
+
let workspaceCwd = options.cwd;
|
|
715
1002
|
const allowedOrigin = new URL(options.appUrl).origin;
|
|
716
1003
|
const token = crypto.randomBytes(32).toString("base64url");
|
|
717
|
-
|
|
718
|
-
const
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
1004
|
+
const validation = validatedCompanionProviders();
|
|
1005
|
+
const failures = validation.failures;
|
|
1006
|
+
const runtimes = new Map();
|
|
1007
|
+
for (const id of validation.available) {
|
|
1008
|
+
runtimes.set(id, {
|
|
1009
|
+
id,
|
|
1010
|
+
codex: id === "codex"
|
|
1011
|
+
? new Codex({
|
|
1012
|
+
config: companionCodexConfig(),
|
|
1013
|
+
env: subscriptionEnvironment(),
|
|
1014
|
+
})
|
|
1015
|
+
: null,
|
|
1016
|
+
metadataClient: id === "codex" ? new CodexMetadataClient() : null,
|
|
1017
|
+
thread: null,
|
|
1018
|
+
cliThreadId: null,
|
|
1019
|
+
selectedModel: null,
|
|
1020
|
+
selectedEffort: null,
|
|
1021
|
+
});
|
|
1022
|
+
}
|
|
1023
|
+
if (runtimes.size === 0) {
|
|
1024
|
+
throw new Error([
|
|
1025
|
+
"Nudge Agent could not find an authenticated coding-agent subscription.",
|
|
1026
|
+
...PROVIDER_IDS.map((id) => `${PROVIDERS[id].displayName}: ${failures.get(id) ?? "unavailable"}`),
|
|
1027
|
+
].join("\n"));
|
|
1028
|
+
}
|
|
1029
|
+
const defaultProviderId = options.preferredProvider && runtimes.has(options.preferredProvider)
|
|
1030
|
+
? options.preferredProvider
|
|
1031
|
+
: PROVIDER_IDS.find((id) => runtimes.has(id));
|
|
731
1032
|
let activeAbort = null;
|
|
1033
|
+
const providerInfo = (id) => ({
|
|
1034
|
+
id,
|
|
1035
|
+
displayName: PROVIDERS[id].displayName,
|
|
1036
|
+
});
|
|
1037
|
+
const resolveRuntime = (value) => {
|
|
1038
|
+
const id = (value ?? defaultProviderId);
|
|
1039
|
+
return PROVIDER_IDS.includes(id) ? (runtimes.get(id) ?? null) : null;
|
|
1040
|
+
};
|
|
1041
|
+
const resetThreads = () => {
|
|
1042
|
+
for (const candidate of runtimes.values()) {
|
|
1043
|
+
candidate.thread = null;
|
|
1044
|
+
candidate.cliThreadId = null;
|
|
1045
|
+
}
|
|
1046
|
+
};
|
|
1047
|
+
const readMetadata = async (runtime) => {
|
|
1048
|
+
if (runtime.metadataClient) {
|
|
1049
|
+
const metadata = await runtime.metadataClient.metadata(runtime.selectedModel, runtime.selectedEffort);
|
|
1050
|
+
return {
|
|
1051
|
+
...metadata,
|
|
1052
|
+
provider: providerInfo(runtime.id),
|
|
1053
|
+
capabilities: { modelSelection: true, effortSelection: true },
|
|
1054
|
+
};
|
|
1055
|
+
}
|
|
1056
|
+
return subscriptionMetadata(runtime.id, runtime.selectedModel, runtime.selectedEffort);
|
|
1057
|
+
};
|
|
732
1058
|
const corsHeaders = (origin) => ({
|
|
733
1059
|
...(origin === allowedOrigin
|
|
734
1060
|
? { "Access-Control-Allow-Origin": allowedOrigin }
|
|
@@ -744,6 +1070,9 @@ export async function runAgentCompanion(argv) {
|
|
|
744
1070
|
void (async () => {
|
|
745
1071
|
const origin = req.headers.origin;
|
|
746
1072
|
const headers = corsHeaders(origin);
|
|
1073
|
+
const requestUrl = new URL(req.url ?? "/", "http://127.0.0.1");
|
|
1074
|
+
const runtime = resolveRuntime(requestUrl.searchParams.get("provider"));
|
|
1075
|
+
const provider = runtime ? PROVIDERS[runtime.id] : null;
|
|
747
1076
|
if (origin && origin !== allowedOrigin) {
|
|
748
1077
|
writeJson(res, 403, { error: "Origin not allowed" }, headers);
|
|
749
1078
|
return;
|
|
@@ -757,67 +1086,82 @@ export async function runAgentCompanion(argv) {
|
|
|
757
1086
|
writeJson(res, 401, { error: "Invalid companion token" }, headers);
|
|
758
1087
|
return;
|
|
759
1088
|
}
|
|
760
|
-
if (
|
|
1089
|
+
if (!runtime || !provider) {
|
|
1090
|
+
writeJson(res, 400, { error: "That coding-agent subscription is unavailable" }, headers);
|
|
1091
|
+
return;
|
|
1092
|
+
}
|
|
1093
|
+
if (req.method === "GET" && requestUrl.pathname === "/v1/health") {
|
|
761
1094
|
writeJson(res, 200, {
|
|
762
1095
|
ok: true,
|
|
763
|
-
cwd:
|
|
1096
|
+
cwd: workspaceCwd,
|
|
764
1097
|
running: activeAbort !== null,
|
|
765
|
-
threadId:
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
1098
|
+
threadId: runtime.codex
|
|
1099
|
+
? (runtime.thread?.id ?? null)
|
|
1100
|
+
: runtime.cliThreadId,
|
|
1101
|
+
model: runtime.selectedModel,
|
|
1102
|
+
effort: runtime.selectedEffort,
|
|
1103
|
+
provider: providerInfo(runtime.id),
|
|
1104
|
+
providers: Array.from(runtimes.keys(), providerInfo),
|
|
769
1105
|
}, headers);
|
|
770
1106
|
return;
|
|
771
1107
|
}
|
|
772
|
-
if (req.method === "GET" &&
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
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 },
|
|
1108
|
+
if (req.method === "GET" && requestUrl.pathname === "/v1/workspace") {
|
|
1109
|
+
try {
|
|
1110
|
+
const requestedPath = requestUrl.searchParams.get("path") ?? workspaceCwd;
|
|
1111
|
+
const listing = await browseCompanionWorkspace(requestedPath, workspaceCwd);
|
|
1112
|
+
writeJson(res, 200, listing, headers);
|
|
1113
|
+
}
|
|
1114
|
+
catch (error) {
|
|
1115
|
+
writeJson(res, 400, {
|
|
1116
|
+
error: error instanceof Error
|
|
1117
|
+
? error.message
|
|
1118
|
+
: "That folder is unavailable",
|
|
792
1119
|
}, headers);
|
|
1120
|
+
}
|
|
1121
|
+
return;
|
|
1122
|
+
}
|
|
1123
|
+
if (req.method === "POST" && requestUrl.pathname === "/v1/workspace") {
|
|
1124
|
+
if (activeAbort) {
|
|
1125
|
+
writeJson(res, 409, { error: `A ${provider.displayName} turn is still running` }, headers);
|
|
793
1126
|
return;
|
|
794
1127
|
}
|
|
1128
|
+
let parsed;
|
|
795
1129
|
try {
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
1130
|
+
parsed = JSON.parse(await readBody(req));
|
|
1131
|
+
const requestedPath = typeof parsed.cwd === "string" ? parsed.cwd : "";
|
|
1132
|
+
const listing = await browseCompanionWorkspace(requestedPath, workspaceCwd);
|
|
1133
|
+
workspaceCwd = listing.cwd;
|
|
1134
|
+
resetThreads();
|
|
1135
|
+
writeJson(res, 200, { ok: true, cwd: workspaceCwd }, headers);
|
|
1136
|
+
}
|
|
1137
|
+
catch (error) {
|
|
1138
|
+
writeJson(res, 400, {
|
|
1139
|
+
error: error instanceof Error
|
|
1140
|
+
? error.message
|
|
1141
|
+
: "That folder is unavailable",
|
|
803
1142
|
}, headers);
|
|
804
1143
|
}
|
|
1144
|
+
return;
|
|
1145
|
+
}
|
|
1146
|
+
if (req.method === "GET" && requestUrl.pathname === "/v1/metadata") {
|
|
1147
|
+
try {
|
|
1148
|
+
const metadata = await readMetadata(runtime);
|
|
1149
|
+
runtime.selectedModel = metadata.currentModel;
|
|
1150
|
+
runtime.selectedEffort = metadata.currentReasoningEffort;
|
|
1151
|
+
writeJson(res, 200, metadata, headers);
|
|
1152
|
+
}
|
|
805
1153
|
catch (error) {
|
|
806
1154
|
writeJson(res, 503, {
|
|
807
1155
|
error: error instanceof Error
|
|
808
1156
|
? error.message
|
|
809
|
-
:
|
|
1157
|
+
: `${provider.displayName} metadata is unavailable`,
|
|
810
1158
|
}, headers);
|
|
811
1159
|
}
|
|
812
1160
|
return;
|
|
813
1161
|
}
|
|
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
|
-
}
|
|
1162
|
+
if (req.method === "POST" && requestUrl.pathname === "/v1/model") {
|
|
819
1163
|
if (activeAbort) {
|
|
820
|
-
writeJson(res, 409, { error:
|
|
1164
|
+
writeJson(res, 409, { error: `A ${provider.displayName} turn is still running` }, headers);
|
|
821
1165
|
return;
|
|
822
1166
|
}
|
|
823
1167
|
let parsed;
|
|
@@ -829,25 +1173,29 @@ export async function runAgentCompanion(argv) {
|
|
|
829
1173
|
return;
|
|
830
1174
|
}
|
|
831
1175
|
const model = typeof parsed.model === "string" ? parsed.model.trim() : "";
|
|
832
|
-
const metadata = await
|
|
1176
|
+
const metadata = await readMetadata(runtime);
|
|
833
1177
|
const modelInfo = metadata.models.find((candidate) => candidate.model === model);
|
|
834
1178
|
if (!modelInfo) {
|
|
835
|
-
writeJson(res, 400, { error:
|
|
1179
|
+
writeJson(res, 400, { error: `That ${provider.displayName} model is unavailable` }, headers);
|
|
836
1180
|
return;
|
|
837
1181
|
}
|
|
838
|
-
selectedModel = model;
|
|
839
|
-
selectedEffort = modelInfo.defaultReasoningEffort;
|
|
840
|
-
thread = null;
|
|
841
|
-
|
|
1182
|
+
runtime.selectedModel = model;
|
|
1183
|
+
runtime.selectedEffort = modelInfo.defaultReasoningEffort;
|
|
1184
|
+
runtime.thread = null;
|
|
1185
|
+
runtime.cliThreadId = null;
|
|
1186
|
+
writeJson(res, 200, { ok: true, model, effort: runtime.selectedEffort }, headers);
|
|
842
1187
|
return;
|
|
843
1188
|
}
|
|
844
|
-
if (req.method === "POST" &&
|
|
845
|
-
if (
|
|
846
|
-
writeJson(res,
|
|
1189
|
+
if (req.method === "POST" && requestUrl.pathname === "/v1/effort") {
|
|
1190
|
+
if (activeAbort) {
|
|
1191
|
+
writeJson(res, 409, { error: `A ${provider.displayName} turn is still running` }, headers);
|
|
847
1192
|
return;
|
|
848
1193
|
}
|
|
849
|
-
|
|
850
|
-
|
|
1194
|
+
const metadata = await readMetadata(runtime);
|
|
1195
|
+
if (metadata.capabilities?.effortSelection === false) {
|
|
1196
|
+
writeJson(res, 400, {
|
|
1197
|
+
error: `${provider.displayName} controls reasoning settings in its own CLI.`,
|
|
1198
|
+
}, headers);
|
|
851
1199
|
return;
|
|
852
1200
|
}
|
|
853
1201
|
let parsed;
|
|
@@ -859,33 +1207,32 @@ export async function runAgentCompanion(argv) {
|
|
|
859
1207
|
return;
|
|
860
1208
|
}
|
|
861
1209
|
const effort = reasoningEffort(parsed.effort);
|
|
862
|
-
const metadata = await metadataClient.metadata(selectedModel, selectedEffort);
|
|
863
1210
|
const modelInfo = metadata.models.find((candidate) => candidate.model === metadata.currentModel);
|
|
864
1211
|
if (!effort ||
|
|
865
1212
|
!modelInfo?.supportedReasoningEfforts.some((candidate) => candidate.reasoningEffort === effort)) {
|
|
866
1213
|
writeJson(res, 400, { error: "That reasoning effort is unavailable" }, headers);
|
|
867
1214
|
return;
|
|
868
1215
|
}
|
|
869
|
-
selectedModel = metadata.currentModel;
|
|
870
|
-
selectedEffort = effort;
|
|
871
|
-
thread = null;
|
|
872
|
-
cliThreadId = null;
|
|
873
|
-
writeJson(res, 200, { ok: true, model: selectedModel, effort }, headers);
|
|
1216
|
+
runtime.selectedModel = metadata.currentModel;
|
|
1217
|
+
runtime.selectedEffort = effort;
|
|
1218
|
+
runtime.thread = null;
|
|
1219
|
+
runtime.cliThreadId = null;
|
|
1220
|
+
writeJson(res, 200, { ok: true, model: runtime.selectedModel, effort }, headers);
|
|
874
1221
|
return;
|
|
875
1222
|
}
|
|
876
|
-
if (req.method === "POST" &&
|
|
1223
|
+
if (req.method === "POST" && requestUrl.pathname === "/v1/new") {
|
|
877
1224
|
if (activeAbort) {
|
|
878
|
-
writeJson(res, 409, { error:
|
|
1225
|
+
writeJson(res, 409, { error: `A ${provider.displayName} turn is still running` }, headers);
|
|
879
1226
|
return;
|
|
880
1227
|
}
|
|
881
|
-
thread = null;
|
|
882
|
-
cliThreadId = null;
|
|
1228
|
+
runtime.thread = null;
|
|
1229
|
+
runtime.cliThreadId = null;
|
|
883
1230
|
writeJson(res, 200, { ok: true }, headers);
|
|
884
1231
|
return;
|
|
885
1232
|
}
|
|
886
|
-
if (req.method === "POST" &&
|
|
1233
|
+
if (req.method === "POST" && requestUrl.pathname === "/v1/resume") {
|
|
887
1234
|
if (activeAbort) {
|
|
888
|
-
writeJson(res, 409, { error:
|
|
1235
|
+
writeJson(res, 409, { error: `A ${provider.displayName} turn is still running` }, headers);
|
|
889
1236
|
return;
|
|
890
1237
|
}
|
|
891
1238
|
let parsed;
|
|
@@ -901,22 +1248,50 @@ export async function runAgentCompanion(argv) {
|
|
|
901
1248
|
writeJson(res, 400, { error: "Invalid coding-agent session id" }, headers);
|
|
902
1249
|
return;
|
|
903
1250
|
}
|
|
904
|
-
if (!metadataClient || !codex) {
|
|
905
|
-
|
|
906
|
-
|
|
1251
|
+
if (!runtime.metadataClient || !runtime.codex) {
|
|
1252
|
+
const metadata = await readMetadata(runtime);
|
|
1253
|
+
const requestedModel = typeof parsed.model === "string" ? parsed.model.trim() : "";
|
|
1254
|
+
const requestedEffort = reasoningEffort(parsed.effort);
|
|
1255
|
+
if (requestedModel &&
|
|
1256
|
+
!metadata.models.some((candidate) => candidate.model === requestedModel)) {
|
|
1257
|
+
writeJson(res, 400, { error: `That ${provider.displayName} model is unavailable` }, headers);
|
|
1258
|
+
return;
|
|
1259
|
+
}
|
|
1260
|
+
const nextModel = requestedModel || metadata.currentModel;
|
|
1261
|
+
const modelInfo = metadata.models.find((candidate) => candidate.model === nextModel);
|
|
1262
|
+
const cursorDefaultEffort = metadata.capabilities?.effortSelection === false &&
|
|
1263
|
+
requestedEffort === "none";
|
|
1264
|
+
if (parsed.effort !== undefined &&
|
|
1265
|
+
(!requestedEffort ||
|
|
1266
|
+
(!cursorDefaultEffort &&
|
|
1267
|
+
!modelInfo?.supportedReasoningEfforts.some((candidate) => candidate.reasoningEffort === requestedEffort)))) {
|
|
1268
|
+
writeJson(res, 400, { error: "That reasoning effort is unavailable" }, headers);
|
|
1269
|
+
return;
|
|
1270
|
+
}
|
|
1271
|
+
runtime.selectedModel = nextModel;
|
|
1272
|
+
runtime.selectedEffort =
|
|
1273
|
+
requestedEffort ?? modelInfo?.defaultReasoningEffort ?? "none";
|
|
1274
|
+
runtime.cliThreadId = threadId;
|
|
1275
|
+
writeJson(res, 200, {
|
|
1276
|
+
ok: true,
|
|
1277
|
+
threadId,
|
|
1278
|
+
model: runtime.selectedModel,
|
|
1279
|
+
effort: runtime.selectedEffort,
|
|
1280
|
+
}, headers);
|
|
907
1281
|
return;
|
|
908
1282
|
}
|
|
909
1283
|
const requestedModel = typeof parsed.model === "string" ? parsed.model.trim() : "";
|
|
910
1284
|
const requestedEffort = reasoningEffort(parsed.effort);
|
|
911
|
-
const metadata = await metadataClient.metadata(selectedModel, selectedEffort);
|
|
1285
|
+
const metadata = await runtime.metadataClient.metadata(runtime.selectedModel, runtime.selectedEffort);
|
|
912
1286
|
if (requestedModel) {
|
|
913
1287
|
if (!metadata.models.some((candidate) => candidate.model === requestedModel)) {
|
|
914
1288
|
writeJson(res, 400, { error: "That Codex model is unavailable" }, headers);
|
|
915
1289
|
return;
|
|
916
1290
|
}
|
|
917
|
-
selectedModel = requestedModel;
|
|
1291
|
+
runtime.selectedModel = requestedModel;
|
|
918
1292
|
}
|
|
919
|
-
const modelInfo = metadata.models.find((candidate) => candidate.model ===
|
|
1293
|
+
const modelInfo = metadata.models.find((candidate) => candidate.model ===
|
|
1294
|
+
(runtime.selectedModel ?? metadata.currentModel));
|
|
920
1295
|
if (parsed.effort !== undefined && !requestedEffort) {
|
|
921
1296
|
writeJson(res, 400, { error: "Invalid reasoning effort" }, headers);
|
|
922
1297
|
return;
|
|
@@ -926,17 +1301,23 @@ export async function runAgentCompanion(argv) {
|
|
|
926
1301
|
writeJson(res, 400, { error: "That reasoning effort is unavailable" }, headers);
|
|
927
1302
|
return;
|
|
928
1303
|
}
|
|
929
|
-
selectedEffort =
|
|
930
|
-
|
|
931
|
-
|
|
1304
|
+
runtime.selectedEffort =
|
|
1305
|
+
requestedEffort ?? modelInfo?.defaultReasoningEffort ?? null;
|
|
1306
|
+
runtime.thread = runtime.codex.resumeThread(threadId, companionThreadOptions(workspaceCwd, runtime.selectedModel, runtime.selectedEffort));
|
|
1307
|
+
writeJson(res, 200, {
|
|
1308
|
+
ok: true,
|
|
1309
|
+
threadId,
|
|
1310
|
+
model: runtime.selectedModel,
|
|
1311
|
+
effort: runtime.selectedEffort,
|
|
1312
|
+
}, headers);
|
|
932
1313
|
return;
|
|
933
1314
|
}
|
|
934
|
-
if (req.method === "POST" &&
|
|
1315
|
+
if (req.method === "POST" && requestUrl.pathname === "/v1/cancel") {
|
|
935
1316
|
activeAbort?.abort();
|
|
936
1317
|
writeJson(res, 200, { ok: true }, headers);
|
|
937
1318
|
return;
|
|
938
1319
|
}
|
|
939
|
-
if (req.method !== "POST" ||
|
|
1320
|
+
if (req.method !== "POST" || requestUrl.pathname !== "/v1/chat") {
|
|
940
1321
|
writeJson(res, 404, { error: "Not found" }, headers);
|
|
941
1322
|
return;
|
|
942
1323
|
}
|
|
@@ -957,8 +1338,8 @@ export async function runAgentCompanion(argv) {
|
|
|
957
1338
|
writeJson(res, 400, { error: "prompt is required" }, headers);
|
|
958
1339
|
return;
|
|
959
1340
|
}
|
|
960
|
-
if (codex) {
|
|
961
|
-
thread ??= codex.startThread(companionThreadOptions(
|
|
1341
|
+
if (runtime.codex) {
|
|
1342
|
+
runtime.thread ??= runtime.codex.startThread(companionThreadOptions(workspaceCwd, runtime.selectedModel, runtime.selectedEffort));
|
|
962
1343
|
}
|
|
963
1344
|
const abort = new AbortController();
|
|
964
1345
|
activeAbort = abort;
|
|
@@ -977,11 +1358,17 @@ export async function runAgentCompanion(argv) {
|
|
|
977
1358
|
res.write(`${JSON.stringify(value)}\n`);
|
|
978
1359
|
};
|
|
979
1360
|
try {
|
|
980
|
-
const currentThreadId = codex
|
|
981
|
-
|
|
1361
|
+
const currentThreadId = runtime.codex
|
|
1362
|
+
? (runtime.thread?.id ?? null)
|
|
1363
|
+
: runtime.cliThreadId;
|
|
1364
|
+
send({
|
|
1365
|
+
type: "connected",
|
|
1366
|
+
cwd: workspaceCwd,
|
|
1367
|
+
threadId: currentThreadId,
|
|
1368
|
+
});
|
|
982
1369
|
const contextualPrompt = buildCompanionPrompt(prompt, parsed.context);
|
|
983
|
-
if (codex && thread) {
|
|
984
|
-
const streamed = await thread.runStreamed(contextualPrompt, {
|
|
1370
|
+
if (runtime.codex && runtime.thread) {
|
|
1371
|
+
const streamed = await runtime.thread.runStreamed(contextualPrompt, {
|
|
985
1372
|
signal: abort.signal,
|
|
986
1373
|
});
|
|
987
1374
|
for await (const event of streamed.events) {
|
|
@@ -989,16 +1376,23 @@ export async function runAgentCompanion(argv) {
|
|
|
989
1376
|
}
|
|
990
1377
|
}
|
|
991
1378
|
else {
|
|
992
|
-
cliThreadId = await runSubscriptionCliTurn({
|
|
993
|
-
provider:
|
|
1379
|
+
runtime.cliThreadId = await runSubscriptionCliTurn({
|
|
1380
|
+
provider: runtime.id,
|
|
994
1381
|
prompt: contextualPrompt,
|
|
995
|
-
cwd:
|
|
996
|
-
threadId: cliThreadId,
|
|
1382
|
+
cwd: workspaceCwd,
|
|
1383
|
+
threadId: runtime.cliThreadId,
|
|
1384
|
+
model: runtime.selectedModel,
|
|
1385
|
+
effort: runtime.selectedEffort,
|
|
997
1386
|
signal: abort.signal,
|
|
998
1387
|
send,
|
|
999
1388
|
});
|
|
1000
1389
|
}
|
|
1001
|
-
send({
|
|
1390
|
+
send({
|
|
1391
|
+
type: "done",
|
|
1392
|
+
threadId: runtime.codex
|
|
1393
|
+
? (runtime.thread?.id ?? null)
|
|
1394
|
+
: runtime.cliThreadId,
|
|
1395
|
+
});
|
|
1002
1396
|
}
|
|
1003
1397
|
catch (error) {
|
|
1004
1398
|
send({
|
|
@@ -1024,12 +1418,13 @@ export async function runAgentCompanion(argv) {
|
|
|
1024
1418
|
const pairing = Buffer.from(JSON.stringify({
|
|
1025
1419
|
port,
|
|
1026
1420
|
token,
|
|
1027
|
-
provider:
|
|
1421
|
+
provider: providerInfo(defaultProviderId),
|
|
1422
|
+
providers: Array.from(runtimes.keys(), providerInfo),
|
|
1028
1423
|
}), "utf8").toString("base64url");
|
|
1029
1424
|
const url = companionPairingUrl(options.appUrl, pairing);
|
|
1030
1425
|
console.log("");
|
|
1031
1426
|
console.log("Nudge Agent companion is running");
|
|
1032
|
-
console.log(`
|
|
1427
|
+
console.log(` Agents: ${Array.from(runtimes.keys(), (id) => PROVIDERS[id].displayName).join(", ")}`);
|
|
1033
1428
|
console.log(` App: ${options.appUrl}`);
|
|
1034
1429
|
console.log(` Workspace: ${options.cwd}`);
|
|
1035
1430
|
console.log(` Listener: http://127.0.0.1:${port}`);
|
|
@@ -1038,7 +1433,15 @@ export async function runAgentCompanion(argv) {
|
|
|
1038
1433
|
}
|
|
1039
1434
|
console.log(" Files: read-only");
|
|
1040
1435
|
console.log("");
|
|
1041
|
-
|
|
1436
|
+
for (const id of PROVIDER_IDS) {
|
|
1437
|
+
const definition = PROVIDERS[id];
|
|
1438
|
+
if (runtimes.has(id)) {
|
|
1439
|
+
console.log(` ready ${definition.displayName} — reusing \`${definition.loginCommand}\``);
|
|
1440
|
+
}
|
|
1441
|
+
else {
|
|
1442
|
+
console.log(` unavailable ${definition.displayName} — ${failures.get(id) ?? "not found"}`);
|
|
1443
|
+
}
|
|
1444
|
+
}
|
|
1042
1445
|
console.log("Keep this terminal open while using Nudge chat.");
|
|
1043
1446
|
console.log("");
|
|
1044
1447
|
if (options.openBrowser)
|
|
@@ -1048,7 +1451,8 @@ export async function runAgentCompanion(argv) {
|
|
|
1048
1451
|
await new Promise((resolve) => {
|
|
1049
1452
|
const shutdown = () => {
|
|
1050
1453
|
activeAbort?.abort();
|
|
1051
|
-
|
|
1454
|
+
for (const runtime of runtimes.values())
|
|
1455
|
+
runtime.metadataClient?.stop();
|
|
1052
1456
|
server.close(() => resolve());
|
|
1053
1457
|
};
|
|
1054
1458
|
process.once("SIGINT", shutdown);
|