@euqns/nudge-mcp 0.15.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 +36 -24
- package/dist/ci-workflows.js +19 -6
- package/dist/ci-workflows.js.map +1 -1
- package/dist/ci-workflows.test.js +20 -1
- package/dist/ci-workflows.test.js.map +1 -1
- package/dist/codex-companion.js +740 -79
- package/dist/codex-companion.js.map +1 -1
- package/dist/codex-companion.test.js +77 -1
- package/dist/codex-companion.test.js.map +1 -1
- package/dist/index.js +21 -6
- package/dist/index.js.map +1 -1
- package/dist/runner.js +4 -1
- package/dist/runner.js.map +1 -1
- package/dist/runner.test.js +7 -1
- package/dist/runner.test.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";
|
|
@@ -10,6 +10,79 @@ import { NUDGE_MCP_VERSION } from "./version.js";
|
|
|
10
10
|
const DEFAULT_APP_URL = "https://funjitsu-nudge.vercel.app";
|
|
11
11
|
const DEFAULT_PORT = 47653;
|
|
12
12
|
const MAX_BODY_BYTES = 64 * 1024;
|
|
13
|
+
const PROVIDERS = {
|
|
14
|
+
codex: {
|
|
15
|
+
displayName: "Codex",
|
|
16
|
+
binary: process.platform === "win32" ? "codex.cmd" : "codex",
|
|
17
|
+
loginCommand: "codex login",
|
|
18
|
+
},
|
|
19
|
+
claude: {
|
|
20
|
+
displayName: "Claude Code",
|
|
21
|
+
binary: process.platform === "win32" ? "claude.cmd" : "claude",
|
|
22
|
+
loginCommand: "claude auth login",
|
|
23
|
+
},
|
|
24
|
+
cursor: {
|
|
25
|
+
displayName: "Cursor",
|
|
26
|
+
binary: process.platform === "win32" ? "cursor-agent.cmd" : "cursor-agent",
|
|
27
|
+
loginCommand: "cursor-agent login",
|
|
28
|
+
},
|
|
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
|
+
];
|
|
13
86
|
const require = createRequire(import.meta.url);
|
|
14
87
|
const REASONING_EFFORTS = new Set([
|
|
15
88
|
"none",
|
|
@@ -22,7 +95,8 @@ const REASONING_EFFORTS = new Set([
|
|
|
22
95
|
"ultra",
|
|
23
96
|
]);
|
|
24
97
|
function reasoningEffort(value) {
|
|
25
|
-
return typeof value === "string" &&
|
|
98
|
+
return typeof value === "string" &&
|
|
99
|
+
REASONING_EFFORTS.has(value)
|
|
26
100
|
? value
|
|
27
101
|
: null;
|
|
28
102
|
}
|
|
@@ -60,7 +134,10 @@ class CodexMetadataClient {
|
|
|
60
134
|
});
|
|
61
135
|
this.child = child;
|
|
62
136
|
this.stderr = "";
|
|
63
|
-
const lines = readline.createInterface({
|
|
137
|
+
const lines = readline.createInterface({
|
|
138
|
+
input: child.stdout,
|
|
139
|
+
crlfDelay: Infinity,
|
|
140
|
+
});
|
|
64
141
|
lines.on("line", (line) => this.handleLine(line));
|
|
65
142
|
child.stderr.on("data", (chunk) => {
|
|
66
143
|
this.stderr = `${this.stderr}${chunk.toString("utf8")}`.slice(-4_000);
|
|
@@ -73,7 +150,7 @@ class CodexMetadataClient {
|
|
|
73
150
|
await this.rawRequest("initialize", {
|
|
74
151
|
clientInfo: {
|
|
75
152
|
name: "nudge-companion",
|
|
76
|
-
title: "Nudge
|
|
153
|
+
title: "Nudge Agent",
|
|
77
154
|
version: NUDGE_MCP_VERSION,
|
|
78
155
|
},
|
|
79
156
|
capabilities: {
|
|
@@ -147,7 +224,9 @@ class CodexMetadataClient {
|
|
|
147
224
|
.filter((model) => typeof model.model === "string" && model.model.length > 0)
|
|
148
225
|
.map((model) => ({
|
|
149
226
|
model: model.model,
|
|
150
|
-
displayName: typeof model.displayName === "string"
|
|
227
|
+
displayName: typeof model.displayName === "string"
|
|
228
|
+
? model.displayName
|
|
229
|
+
: model.model,
|
|
151
230
|
description: typeof model.description === "string" ? model.description : "",
|
|
152
231
|
isDefault: model.isDefault === true,
|
|
153
232
|
defaultReasoningEffort: reasoningEffort(model.defaultReasoningEffort) ?? "medium",
|
|
@@ -159,10 +238,14 @@ class CodexMetadataClient {
|
|
|
159
238
|
const effort = reasoningEffort(entry.reasoningEffort);
|
|
160
239
|
if (!effort)
|
|
161
240
|
return [];
|
|
162
|
-
return [
|
|
241
|
+
return [
|
|
242
|
+
{
|
|
163
243
|
reasoningEffort: effort,
|
|
164
|
-
description: typeof entry.description === "string"
|
|
165
|
-
|
|
244
|
+
description: typeof entry.description === "string"
|
|
245
|
+
? entry.description
|
|
246
|
+
: "",
|
|
247
|
+
},
|
|
248
|
+
];
|
|
166
249
|
})
|
|
167
250
|
: [],
|
|
168
251
|
}));
|
|
@@ -204,7 +287,7 @@ class CodexMetadataClient {
|
|
|
204
287
|
currentReasoningEffort: selectedEffort,
|
|
205
288
|
models,
|
|
206
289
|
planType: account.account?.type === "chatgpt"
|
|
207
|
-
? account.account.planType ?? null
|
|
290
|
+
? (account.account.planType ?? null)
|
|
208
291
|
: null,
|
|
209
292
|
rateLimits,
|
|
210
293
|
updatedAt: Date.now(),
|
|
@@ -227,6 +310,7 @@ function parseArgs(argv) {
|
|
|
227
310
|
let openBrowserOnStart = true;
|
|
228
311
|
let port = DEFAULT_PORT;
|
|
229
312
|
let portWasExplicit = false;
|
|
313
|
+
let preferredProvider;
|
|
230
314
|
for (let index = 0; index < argv.length; index++) {
|
|
231
315
|
const arg = argv[index];
|
|
232
316
|
const next = argv[index + 1];
|
|
@@ -256,8 +340,22 @@ function parseArgs(argv) {
|
|
|
256
340
|
else if (arg === "--no-open") {
|
|
257
341
|
openBrowserOnStart = false;
|
|
258
342
|
}
|
|
343
|
+
else if (arg === "--provider" && next) {
|
|
344
|
+
if (!(next in PROVIDERS)) {
|
|
345
|
+
throw new Error("--provider must be codex, claude, or cursor");
|
|
346
|
+
}
|
|
347
|
+
preferredProvider = next;
|
|
348
|
+
index++;
|
|
349
|
+
}
|
|
350
|
+
else if (arg.startsWith("--provider=")) {
|
|
351
|
+
const value = arg.slice("--provider=".length);
|
|
352
|
+
if (!(value in PROVIDERS)) {
|
|
353
|
+
throw new Error("--provider must be codex, claude, or cursor");
|
|
354
|
+
}
|
|
355
|
+
preferredProvider = value;
|
|
356
|
+
}
|
|
259
357
|
else {
|
|
260
|
-
throw new Error(`Unknown
|
|
358
|
+
throw new Error(`Unknown agent companion option: ${arg}`);
|
|
261
359
|
}
|
|
262
360
|
}
|
|
263
361
|
if (!Number.isInteger(port) || port < 1024 || port > 65535) {
|
|
@@ -269,6 +367,7 @@ function parseArgs(argv) {
|
|
|
269
367
|
openBrowser: openBrowserOnStart,
|
|
270
368
|
port,
|
|
271
369
|
portWasExplicit,
|
|
370
|
+
preferredProvider,
|
|
272
371
|
};
|
|
273
372
|
}
|
|
274
373
|
async function listenOnLoopback(server, preferredPort, allowFallback) {
|
|
@@ -332,8 +431,10 @@ function writeJson(res, status, value, headers) {
|
|
|
332
431
|
res.writeHead(status, { ...headers, "Content-Type": "application/json" });
|
|
333
432
|
res.end(JSON.stringify(value));
|
|
334
433
|
}
|
|
335
|
-
function
|
|
336
|
-
const pathname = typeof context?.pathname === "string"
|
|
434
|
+
export function buildCompanionPrompt(prompt, context) {
|
|
435
|
+
const pathname = typeof context?.pathname === "string"
|
|
436
|
+
? context.pathname.slice(0, 500)
|
|
437
|
+
: undefined;
|
|
337
438
|
const routeContext = pathname
|
|
338
439
|
? `The user opened this chat from the Nudge route ${JSON.stringify(pathname)}.`
|
|
339
440
|
: "The user opened this chat from Nudge.";
|
|
@@ -347,21 +448,62 @@ function buildPrompt(prompt, context) {
|
|
|
347
448
|
source: context.boardSource === "cloud" || context.boardSource === "local"
|
|
348
449
|
? context.boardSource
|
|
349
450
|
: undefined,
|
|
350
|
-
|
|
451
|
+
role: context.boardRole === "owner" ||
|
|
452
|
+
context.boardRole === "editor" ||
|
|
453
|
+
context.boardRole === "viewer"
|
|
454
|
+
? context.boardRole
|
|
455
|
+
: undefined,
|
|
456
|
+
view: typeof context.view === "string"
|
|
457
|
+
? context.view.slice(0, 50)
|
|
458
|
+
: undefined,
|
|
351
459
|
canvasId: typeof context.canvasId === "string"
|
|
352
460
|
? context.canvasId.slice(0, 200)
|
|
353
461
|
: undefined,
|
|
354
|
-
cardId: typeof context.cardId === "string"
|
|
355
|
-
|
|
356
|
-
|
|
462
|
+
cardId: typeof context.cardId === "string"
|
|
463
|
+
? context.cardId.slice(0, 200)
|
|
464
|
+
: undefined,
|
|
465
|
+
cardTitle: typeof context.cardTitle === "string"
|
|
466
|
+
? context.cardTitle.slice(0, 500)
|
|
467
|
+
: undefined,
|
|
468
|
+
cardDescription: typeof context.cardDescription === "string"
|
|
469
|
+
? context.cardDescription.slice(0, 4_000)
|
|
470
|
+
: undefined,
|
|
471
|
+
cardHref: typeof context.cardHref === "string"
|
|
472
|
+
? context.cardHref.slice(0, 1_000)
|
|
473
|
+
: undefined,
|
|
474
|
+
listId: typeof context.listId === "string"
|
|
475
|
+
? context.listId.slice(0, 200)
|
|
476
|
+
: undefined,
|
|
477
|
+
listTitle: typeof context.listTitle === "string"
|
|
478
|
+
? context.listTitle.slice(0, 500)
|
|
479
|
+
: undefined,
|
|
480
|
+
selectionSource: typeof context.selectionSource === "string"
|
|
481
|
+
? context.selectionSource.slice(0, 50)
|
|
482
|
+
: undefined,
|
|
483
|
+
repository: context.repository && typeof context.repository === "object"
|
|
484
|
+
? context.repository
|
|
485
|
+
: undefined,
|
|
486
|
+
listCount: typeof context.listCount === "number"
|
|
487
|
+
? context.listCount
|
|
488
|
+
: undefined,
|
|
489
|
+
cardCount: typeof context.cardCount === "number"
|
|
490
|
+
? context.cardCount
|
|
491
|
+
: undefined,
|
|
357
492
|
}),
|
|
358
493
|
"Use this exact board id for Nudge MCP calls unless the user names another board.",
|
|
494
|
+
context.cardId
|
|
495
|
+
? "Treat the selected card as the subject when the user says this task, this card, or it."
|
|
496
|
+
: "No card is selected; do not guess which card the user means.",
|
|
359
497
|
]
|
|
360
498
|
: [];
|
|
361
499
|
return [
|
|
362
500
|
routeContext,
|
|
363
501
|
...boardContext,
|
|
364
502
|
"Use the configured Nudge MCP tools when the request needs board or card data.",
|
|
503
|
+
"Board mutations use a two-step confirmation workflow. First inspect enough data to show a concise preview naming every item and field that will change. Do not call a write tool until a later user message explicitly confirms that preview. A request such as 'create this' starts the preview; it is not confirmation. After execution, report success and include the returned Nudge link so the result opens in chat.",
|
|
504
|
+
"Call out deletes, replacement descriptions, membership changes, and terminal status moves as irreversible or potentially disruptive before confirmation. If permissions reject an operation, explain the required board role and offer a retry after access changes.",
|
|
505
|
+
"Local boards cannot be changed through Nudge MCP, and viewer-role cloud boards are read-only. Explain that permission state before proposing a write and do not attempt the mutation.",
|
|
506
|
+
"For a coding-agent handoff, use the nudge.agent-handoff/v1 sections: task brief, acceptance criteria, relevant links, repository context, and return path. The format must work for Claude Code, Codex, and Cursor. Runner output should be summarized back to the originating card with add_comment, after the same write preview and confirmation.",
|
|
365
507
|
"Be concise about progress and return a clear final answer for the Nudge chat UI.",
|
|
366
508
|
"The local filesystem is read-only. Nudge MCP tools may update boards when requested; do not claim that local files were changed.",
|
|
367
509
|
"",
|
|
@@ -386,13 +528,198 @@ export function assertChatGptLogin() {
|
|
|
386
528
|
].join("\n"));
|
|
387
529
|
}
|
|
388
530
|
}
|
|
389
|
-
export function
|
|
531
|
+
export function assertCompanionProvider(provider) {
|
|
532
|
+
if (provider === "codex") {
|
|
533
|
+
assertChatGptLogin();
|
|
534
|
+
return;
|
|
535
|
+
}
|
|
536
|
+
const definition = PROVIDERS[provider];
|
|
537
|
+
const args = provider === "claude" ? ["auth", "status"] : ["status"];
|
|
538
|
+
const result = spawnSync(definition.binary, args, {
|
|
539
|
+
encoding: "utf8",
|
|
540
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
541
|
+
});
|
|
542
|
+
if (result.error) {
|
|
543
|
+
throw new Error(`${definition.displayName} CLI was not found. Install it, then run \`${definition.loginCommand}\`.`);
|
|
544
|
+
}
|
|
545
|
+
if (result.status !== 0) {
|
|
546
|
+
const detail = `${result.stdout ?? ""}\n${result.stderr ?? ""}`.trim();
|
|
547
|
+
throw new Error(`${definition.displayName} is not authenticated. Run \`${definition.loginCommand}\`.${detail ? `\nCurrent status: ${detail}` : ""}`);
|
|
548
|
+
}
|
|
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
|
+
}
|
|
564
|
+
export function subscriptionEnvironment(environment = process.env) {
|
|
390
565
|
const blocked = new Set([
|
|
566
|
+
"ANTHROPIC_API_KEY",
|
|
567
|
+
"CURSOR_API_KEY",
|
|
391
568
|
"CODEX_ACCESS_TOKEN",
|
|
392
569
|
"CODEX_API_KEY",
|
|
393
570
|
"OPENAI_API_KEY",
|
|
394
571
|
]);
|
|
395
|
-
return Object.fromEntries(Object.entries(
|
|
572
|
+
return Object.fromEntries(Object.entries(environment).filter((entry) => entry[1] !== undefined && !blocked.has(entry[0])));
|
|
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
|
+
};
|
|
396
723
|
}
|
|
397
724
|
export function companionMcpTransport() {
|
|
398
725
|
const companionFile = fileURLToPath(import.meta.url);
|
|
@@ -432,6 +759,194 @@ export function companionCodexConfig(env) {
|
|
|
432
759
|
},
|
|
433
760
|
};
|
|
434
761
|
}
|
|
762
|
+
function companionClaudeConfig() {
|
|
763
|
+
const transport = companionMcpTransport();
|
|
764
|
+
return JSON.stringify({
|
|
765
|
+
mcpServers: {
|
|
766
|
+
nudge: {
|
|
767
|
+
command: transport.command,
|
|
768
|
+
args: transport.args,
|
|
769
|
+
},
|
|
770
|
+
},
|
|
771
|
+
});
|
|
772
|
+
}
|
|
773
|
+
export function companionCliInvocation(provider, prompt, threadId, model = null, effort = null) {
|
|
774
|
+
const definition = PROVIDERS[provider];
|
|
775
|
+
if (provider === "claude") {
|
|
776
|
+
return {
|
|
777
|
+
command: definition.binary,
|
|
778
|
+
args: [
|
|
779
|
+
"-p",
|
|
780
|
+
"--output-format",
|
|
781
|
+
"stream-json",
|
|
782
|
+
"--verbose",
|
|
783
|
+
"--permission-mode",
|
|
784
|
+
"bypassPermissions",
|
|
785
|
+
"--disallowedTools",
|
|
786
|
+
"Edit",
|
|
787
|
+
"Write",
|
|
788
|
+
"NotebookEdit",
|
|
789
|
+
"Bash",
|
|
790
|
+
"--mcp-config",
|
|
791
|
+
companionClaudeConfig(),
|
|
792
|
+
"--strict-mcp-config",
|
|
793
|
+
...(model && model !== "default" ? ["--model", model] : []),
|
|
794
|
+
...(effort && effort !== "none" ? ["--effort", effort] : []),
|
|
795
|
+
...(threadId ? ["--resume", threadId] : []),
|
|
796
|
+
],
|
|
797
|
+
stdin: prompt,
|
|
798
|
+
};
|
|
799
|
+
}
|
|
800
|
+
return {
|
|
801
|
+
command: definition.binary,
|
|
802
|
+
args: [
|
|
803
|
+
"-p",
|
|
804
|
+
"--mode=ask",
|
|
805
|
+
"--output-format",
|
|
806
|
+
"stream-json",
|
|
807
|
+
...(model ? ["--model", model] : []),
|
|
808
|
+
...(threadId ? ["--resume", threadId] : []),
|
|
809
|
+
prompt,
|
|
810
|
+
],
|
|
811
|
+
stdin: null,
|
|
812
|
+
};
|
|
813
|
+
}
|
|
814
|
+
export function companionPairingUrl(appUrl, pairing) {
|
|
815
|
+
return `${appUrl}/#nudge-agent=${pairing}`;
|
|
816
|
+
}
|
|
817
|
+
function syntheticUsage(usage) {
|
|
818
|
+
return {
|
|
819
|
+
input_tokens: typeof usage?.input_tokens === "number" ? usage.input_tokens : 0,
|
|
820
|
+
cached_input_tokens: typeof usage?.cache_read_input_tokens === "number"
|
|
821
|
+
? usage.cache_read_input_tokens
|
|
822
|
+
: 0,
|
|
823
|
+
cache_write_input_tokens: typeof usage?.cache_creation_input_tokens === "number"
|
|
824
|
+
? usage.cache_creation_input_tokens
|
|
825
|
+
: 0,
|
|
826
|
+
output_tokens: typeof usage?.output_tokens === "number" ? usage.output_tokens : 0,
|
|
827
|
+
reasoning_output_tokens: 0,
|
|
828
|
+
};
|
|
829
|
+
}
|
|
830
|
+
function cliActivity(provider, event) {
|
|
831
|
+
if (provider === "cursor" && event.type === "tool_call") {
|
|
832
|
+
const call = event.tool_call;
|
|
833
|
+
if (!call || typeof call !== "object")
|
|
834
|
+
return "Used a Cursor tool";
|
|
835
|
+
const name = Object.keys(call)[0];
|
|
836
|
+
return name
|
|
837
|
+
? `Used ${name.replace(/ToolCall$/, "")}`
|
|
838
|
+
: "Used a Cursor tool";
|
|
839
|
+
}
|
|
840
|
+
if (provider === "claude" && event.type === "assistant") {
|
|
841
|
+
const message = event.message;
|
|
842
|
+
const blocks = Array.isArray(message?.content) ? message.content : [];
|
|
843
|
+
const tool = blocks.find((block) => !!block &&
|
|
844
|
+
typeof block === "object" &&
|
|
845
|
+
block.type === "tool_use");
|
|
846
|
+
return tool?.name ? `Used ${String(tool.name)}` : null;
|
|
847
|
+
}
|
|
848
|
+
return null;
|
|
849
|
+
}
|
|
850
|
+
async function runSubscriptionCliTurn({ provider, prompt, cwd, threadId, model, effort, signal, send, }) {
|
|
851
|
+
const definition = PROVIDERS[provider];
|
|
852
|
+
const invocation = companionCliInvocation(provider, prompt, threadId, model, effort);
|
|
853
|
+
send({ type: "agent", event: { type: "turn.started" } });
|
|
854
|
+
return await new Promise((resolve, reject) => {
|
|
855
|
+
const child = spawn(invocation.command, invocation.args, {
|
|
856
|
+
cwd,
|
|
857
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
858
|
+
env: subscriptionEnvironment(),
|
|
859
|
+
});
|
|
860
|
+
let stderr = "";
|
|
861
|
+
let activeThreadId = threadId;
|
|
862
|
+
let finalText = "";
|
|
863
|
+
let usage;
|
|
864
|
+
const lines = readline.createInterface({
|
|
865
|
+
input: child.stdout,
|
|
866
|
+
crlfDelay: Infinity,
|
|
867
|
+
});
|
|
868
|
+
lines.on("line", (line) => {
|
|
869
|
+
if (!line.trim())
|
|
870
|
+
return;
|
|
871
|
+
let event;
|
|
872
|
+
try {
|
|
873
|
+
event = JSON.parse(line);
|
|
874
|
+
}
|
|
875
|
+
catch {
|
|
876
|
+
return;
|
|
877
|
+
}
|
|
878
|
+
if (typeof event.session_id === "string") {
|
|
879
|
+
activeThreadId = event.session_id;
|
|
880
|
+
if (event.type === "system") {
|
|
881
|
+
send({
|
|
882
|
+
type: "agent",
|
|
883
|
+
event: { type: "thread.started", thread_id: activeThreadId },
|
|
884
|
+
});
|
|
885
|
+
}
|
|
886
|
+
}
|
|
887
|
+
const activity = cliActivity(provider, event);
|
|
888
|
+
if (activity) {
|
|
889
|
+
const id = typeof event.call_id === "string"
|
|
890
|
+
? event.call_id
|
|
891
|
+
: `tool-${Date.now().toString(36)}`;
|
|
892
|
+
send({
|
|
893
|
+
type: "agent",
|
|
894
|
+
event: {
|
|
895
|
+
type: event.subtype === "completed" ? "item.completed" : "item.started",
|
|
896
|
+
item: { id, type: "reasoning", text: activity },
|
|
897
|
+
},
|
|
898
|
+
});
|
|
899
|
+
}
|
|
900
|
+
if (event.type === "result") {
|
|
901
|
+
if (typeof event.result === "string")
|
|
902
|
+
finalText = event.result;
|
|
903
|
+
if (event.usage && typeof event.usage === "object") {
|
|
904
|
+
usage = event.usage;
|
|
905
|
+
}
|
|
906
|
+
}
|
|
907
|
+
});
|
|
908
|
+
child.stderr.on("data", (chunk) => {
|
|
909
|
+
stderr = `${stderr}${chunk.toString("utf8")}`.slice(-8_000);
|
|
910
|
+
});
|
|
911
|
+
const onAbort = () => child.kill("SIGTERM");
|
|
912
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
913
|
+
child.once("error", (error) => {
|
|
914
|
+
signal.removeEventListener("abort", onAbort);
|
|
915
|
+
reject(error);
|
|
916
|
+
});
|
|
917
|
+
child.once("close", (code) => {
|
|
918
|
+
signal.removeEventListener("abort", onAbort);
|
|
919
|
+
if (signal.aborted) {
|
|
920
|
+
resolve(activeThreadId);
|
|
921
|
+
return;
|
|
922
|
+
}
|
|
923
|
+
if (code !== 0) {
|
|
924
|
+
reject(new Error(`${definition.displayName} exited ${code}: ${stderr.trim().slice(-800) || "no error detail"}`));
|
|
925
|
+
return;
|
|
926
|
+
}
|
|
927
|
+
send({
|
|
928
|
+
type: "agent",
|
|
929
|
+
event: {
|
|
930
|
+
type: "item.completed",
|
|
931
|
+
item: {
|
|
932
|
+
id: `message-${Date.now().toString(36)}`,
|
|
933
|
+
type: "agent_message",
|
|
934
|
+
text: finalText,
|
|
935
|
+
},
|
|
936
|
+
},
|
|
937
|
+
});
|
|
938
|
+
send({
|
|
939
|
+
type: "agent",
|
|
940
|
+
event: { type: "turn.completed", usage: syntheticUsage(usage) },
|
|
941
|
+
});
|
|
942
|
+
resolve(activeThreadId);
|
|
943
|
+
});
|
|
944
|
+
if (invocation.stdin !== null) {
|
|
945
|
+
child.stdin.write(invocation.stdin);
|
|
946
|
+
}
|
|
947
|
+
child.stdin.end();
|
|
948
|
+
});
|
|
949
|
+
}
|
|
435
950
|
function companionThreadOptions(cwd, model, effort) {
|
|
436
951
|
return {
|
|
437
952
|
approvalPolicy: "never",
|
|
@@ -444,20 +959,59 @@ function companionThreadOptions(cwd, model, effort) {
|
|
|
444
959
|
modelReasoningEffort: effort,
|
|
445
960
|
};
|
|
446
961
|
}
|
|
447
|
-
|
|
962
|
+
const PROVIDER_IDS = Object.keys(PROVIDERS);
|
|
963
|
+
export async function runAgentCompanion(argv) {
|
|
448
964
|
const options = parseArgs(argv);
|
|
449
965
|
const allowedOrigin = new URL(options.appUrl).origin;
|
|
450
966
|
const token = crypto.randomBytes(32).toString("base64url");
|
|
451
|
-
|
|
452
|
-
const
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
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));
|
|
460
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
|
+
};
|
|
461
1015
|
const corsHeaders = (origin) => ({
|
|
462
1016
|
...(origin === allowedOrigin
|
|
463
1017
|
? { "Access-Control-Allow-Origin": allowedOrigin }
|
|
@@ -473,6 +1027,9 @@ export async function runCodexCompanion(argv) {
|
|
|
473
1027
|
void (async () => {
|
|
474
1028
|
const origin = req.headers.origin;
|
|
475
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;
|
|
476
1033
|
if (origin && origin !== allowedOrigin) {
|
|
477
1034
|
writeJson(res, 403, { error: "Origin not allowed" }, headers);
|
|
478
1035
|
return;
|
|
@@ -486,36 +1043,44 @@ export async function runCodexCompanion(argv) {
|
|
|
486
1043
|
writeJson(res, 401, { error: "Invalid companion token" }, headers);
|
|
487
1044
|
return;
|
|
488
1045
|
}
|
|
489
|
-
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") {
|
|
490
1051
|
writeJson(res, 200, {
|
|
491
1052
|
ok: true,
|
|
492
1053
|
cwd: options.cwd,
|
|
493
1054
|
running: activeAbort !== null,
|
|
494
|
-
threadId:
|
|
495
|
-
|
|
496
|
-
|
|
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),
|
|
497
1062
|
}, headers);
|
|
498
1063
|
return;
|
|
499
1064
|
}
|
|
500
|
-
if (req.method === "GET" &&
|
|
1065
|
+
if (req.method === "GET" && requestUrl.pathname === "/v1/metadata") {
|
|
501
1066
|
try {
|
|
502
|
-
const metadata = await
|
|
503
|
-
selectedModel = metadata.currentModel;
|
|
504
|
-
selectedEffort = metadata.currentReasoningEffort;
|
|
1067
|
+
const metadata = await readMetadata(runtime);
|
|
1068
|
+
runtime.selectedModel = metadata.currentModel;
|
|
1069
|
+
runtime.selectedEffort = metadata.currentReasoningEffort;
|
|
505
1070
|
writeJson(res, 200, metadata, headers);
|
|
506
1071
|
}
|
|
507
1072
|
catch (error) {
|
|
508
1073
|
writeJson(res, 503, {
|
|
509
1074
|
error: error instanceof Error
|
|
510
1075
|
? error.message
|
|
511
|
-
:
|
|
1076
|
+
: `${provider.displayName} metadata is unavailable`,
|
|
512
1077
|
}, headers);
|
|
513
1078
|
}
|
|
514
1079
|
return;
|
|
515
1080
|
}
|
|
516
|
-
if (req.method === "POST" &&
|
|
1081
|
+
if (req.method === "POST" && requestUrl.pathname === "/v1/model") {
|
|
517
1082
|
if (activeAbort) {
|
|
518
|
-
writeJson(res, 409, { error:
|
|
1083
|
+
writeJson(res, 409, { error: `A ${provider.displayName} turn is still running` }, headers);
|
|
519
1084
|
return;
|
|
520
1085
|
}
|
|
521
1086
|
let parsed;
|
|
@@ -527,21 +1092,29 @@ export async function runCodexCompanion(argv) {
|
|
|
527
1092
|
return;
|
|
528
1093
|
}
|
|
529
1094
|
const model = typeof parsed.model === "string" ? parsed.model.trim() : "";
|
|
530
|
-
const metadata = await
|
|
1095
|
+
const metadata = await readMetadata(runtime);
|
|
531
1096
|
const modelInfo = metadata.models.find((candidate) => candidate.model === model);
|
|
532
1097
|
if (!modelInfo) {
|
|
533
|
-
writeJson(res, 400, { error:
|
|
1098
|
+
writeJson(res, 400, { error: `That ${provider.displayName} model is unavailable` }, headers);
|
|
534
1099
|
return;
|
|
535
1100
|
}
|
|
536
|
-
selectedModel = model;
|
|
537
|
-
selectedEffort = modelInfo.defaultReasoningEffort;
|
|
538
|
-
thread = null;
|
|
539
|
-
|
|
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);
|
|
540
1106
|
return;
|
|
541
1107
|
}
|
|
542
|
-
if (req.method === "POST" &&
|
|
1108
|
+
if (req.method === "POST" && requestUrl.pathname === "/v1/effort") {
|
|
543
1109
|
if (activeAbort) {
|
|
544
|
-
writeJson(res, 409, { error:
|
|
1110
|
+
writeJson(res, 409, { error: `A ${provider.displayName} turn is still running` }, headers);
|
|
1111
|
+
return;
|
|
1112
|
+
}
|
|
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);
|
|
545
1118
|
return;
|
|
546
1119
|
}
|
|
547
1120
|
let parsed;
|
|
@@ -553,31 +1126,32 @@ export async function runCodexCompanion(argv) {
|
|
|
553
1126
|
return;
|
|
554
1127
|
}
|
|
555
1128
|
const effort = reasoningEffort(parsed.effort);
|
|
556
|
-
const metadata = await metadataClient.metadata(selectedModel, selectedEffort);
|
|
557
1129
|
const modelInfo = metadata.models.find((candidate) => candidate.model === metadata.currentModel);
|
|
558
1130
|
if (!effort ||
|
|
559
1131
|
!modelInfo?.supportedReasoningEfforts.some((candidate) => candidate.reasoningEffort === effort)) {
|
|
560
1132
|
writeJson(res, 400, { error: "That reasoning effort is unavailable" }, headers);
|
|
561
1133
|
return;
|
|
562
1134
|
}
|
|
563
|
-
selectedModel = metadata.currentModel;
|
|
564
|
-
selectedEffort = effort;
|
|
565
|
-
thread = null;
|
|
566
|
-
|
|
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);
|
|
567
1140
|
return;
|
|
568
1141
|
}
|
|
569
|
-
if (req.method === "POST" &&
|
|
1142
|
+
if (req.method === "POST" && requestUrl.pathname === "/v1/new") {
|
|
570
1143
|
if (activeAbort) {
|
|
571
|
-
writeJson(res, 409, { error:
|
|
1144
|
+
writeJson(res, 409, { error: `A ${provider.displayName} turn is still running` }, headers);
|
|
572
1145
|
return;
|
|
573
1146
|
}
|
|
574
|
-
thread = null;
|
|
1147
|
+
runtime.thread = null;
|
|
1148
|
+
runtime.cliThreadId = null;
|
|
575
1149
|
writeJson(res, 200, { ok: true }, headers);
|
|
576
1150
|
return;
|
|
577
1151
|
}
|
|
578
|
-
if (req.method === "POST" &&
|
|
1152
|
+
if (req.method === "POST" && requestUrl.pathname === "/v1/resume") {
|
|
579
1153
|
if (activeAbort) {
|
|
580
|
-
writeJson(res, 409, { error:
|
|
1154
|
+
writeJson(res, 409, { error: `A ${provider.displayName} turn is still running` }, headers);
|
|
581
1155
|
return;
|
|
582
1156
|
}
|
|
583
1157
|
let parsed;
|
|
@@ -590,20 +1164,53 @@ export async function runCodexCompanion(argv) {
|
|
|
590
1164
|
}
|
|
591
1165
|
const threadId = typeof parsed.threadId === "string" ? parsed.threadId.trim() : "";
|
|
592
1166
|
if (!/^[a-zA-Z0-9-]{10,100}$/.test(threadId)) {
|
|
593
|
-
writeJson(res, 400, { error: "Invalid
|
|
1167
|
+
writeJson(res, 400, { error: "Invalid coding-agent session id" }, headers);
|
|
1168
|
+
return;
|
|
1169
|
+
}
|
|
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);
|
|
594
1200
|
return;
|
|
595
1201
|
}
|
|
596
1202
|
const requestedModel = typeof parsed.model === "string" ? parsed.model.trim() : "";
|
|
597
1203
|
const requestedEffort = reasoningEffort(parsed.effort);
|
|
598
|
-
const metadata = await metadataClient.metadata(selectedModel, selectedEffort);
|
|
1204
|
+
const metadata = await runtime.metadataClient.metadata(runtime.selectedModel, runtime.selectedEffort);
|
|
599
1205
|
if (requestedModel) {
|
|
600
1206
|
if (!metadata.models.some((candidate) => candidate.model === requestedModel)) {
|
|
601
1207
|
writeJson(res, 400, { error: "That Codex model is unavailable" }, headers);
|
|
602
1208
|
return;
|
|
603
1209
|
}
|
|
604
|
-
selectedModel = requestedModel;
|
|
1210
|
+
runtime.selectedModel = requestedModel;
|
|
605
1211
|
}
|
|
606
|
-
const modelInfo = metadata.models.find((candidate) => candidate.model ===
|
|
1212
|
+
const modelInfo = metadata.models.find((candidate) => candidate.model ===
|
|
1213
|
+
(runtime.selectedModel ?? metadata.currentModel));
|
|
607
1214
|
if (parsed.effort !== undefined && !requestedEffort) {
|
|
608
1215
|
writeJson(res, 400, { error: "Invalid reasoning effort" }, headers);
|
|
609
1216
|
return;
|
|
@@ -613,22 +1220,28 @@ export async function runCodexCompanion(argv) {
|
|
|
613
1220
|
writeJson(res, 400, { error: "That reasoning effort is unavailable" }, headers);
|
|
614
1221
|
return;
|
|
615
1222
|
}
|
|
616
|
-
selectedEffort =
|
|
617
|
-
|
|
618
|
-
|
|
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);
|
|
619
1232
|
return;
|
|
620
1233
|
}
|
|
621
|
-
if (req.method === "POST" &&
|
|
1234
|
+
if (req.method === "POST" && requestUrl.pathname === "/v1/cancel") {
|
|
622
1235
|
activeAbort?.abort();
|
|
623
1236
|
writeJson(res, 200, { ok: true }, headers);
|
|
624
1237
|
return;
|
|
625
1238
|
}
|
|
626
|
-
if (req.method !== "POST" ||
|
|
1239
|
+
if (req.method !== "POST" || requestUrl.pathname !== "/v1/chat") {
|
|
627
1240
|
writeJson(res, 404, { error: "Not found" }, headers);
|
|
628
1241
|
return;
|
|
629
1242
|
}
|
|
630
1243
|
if (activeAbort) {
|
|
631
|
-
writeJson(res, 409, { error:
|
|
1244
|
+
writeJson(res, 409, { error: `A ${provider.displayName} turn is already running` }, headers);
|
|
632
1245
|
return;
|
|
633
1246
|
}
|
|
634
1247
|
let parsed;
|
|
@@ -644,7 +1257,9 @@ export async function runCodexCompanion(argv) {
|
|
|
644
1257
|
writeJson(res, 400, { error: "prompt is required" }, headers);
|
|
645
1258
|
return;
|
|
646
1259
|
}
|
|
647
|
-
|
|
1260
|
+
if (runtime.codex) {
|
|
1261
|
+
runtime.thread ??= runtime.codex.startThread(companionThreadOptions(options.cwd, runtime.selectedModel, runtime.selectedEffort));
|
|
1262
|
+
}
|
|
648
1263
|
const abort = new AbortController();
|
|
649
1264
|
activeAbort = abort;
|
|
650
1265
|
let responseFinished = false;
|
|
@@ -662,14 +1277,41 @@ export async function runCodexCompanion(argv) {
|
|
|
662
1277
|
res.write(`${JSON.stringify(value)}\n`);
|
|
663
1278
|
};
|
|
664
1279
|
try {
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
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,
|
|
668
1287
|
});
|
|
669
|
-
|
|
670
|
-
|
|
1288
|
+
const contextualPrompt = buildCompanionPrompt(prompt, parsed.context);
|
|
1289
|
+
if (runtime.codex && runtime.thread) {
|
|
1290
|
+
const streamed = await runtime.thread.runStreamed(contextualPrompt, {
|
|
1291
|
+
signal: abort.signal,
|
|
1292
|
+
});
|
|
1293
|
+
for await (const event of streamed.events) {
|
|
1294
|
+
send({ type: "agent", event: event });
|
|
1295
|
+
}
|
|
671
1296
|
}
|
|
672
|
-
|
|
1297
|
+
else {
|
|
1298
|
+
runtime.cliThreadId = await runSubscriptionCliTurn({
|
|
1299
|
+
provider: runtime.id,
|
|
1300
|
+
prompt: contextualPrompt,
|
|
1301
|
+
cwd: options.cwd,
|
|
1302
|
+
threadId: runtime.cliThreadId,
|
|
1303
|
+
model: runtime.selectedModel,
|
|
1304
|
+
effort: runtime.selectedEffort,
|
|
1305
|
+
signal: abort.signal,
|
|
1306
|
+
send,
|
|
1307
|
+
});
|
|
1308
|
+
}
|
|
1309
|
+
send({
|
|
1310
|
+
type: "done",
|
|
1311
|
+
threadId: runtime.codex
|
|
1312
|
+
? (runtime.thread?.id ?? null)
|
|
1313
|
+
: runtime.cliThreadId,
|
|
1314
|
+
});
|
|
673
1315
|
}
|
|
674
1316
|
catch (error) {
|
|
675
1317
|
send({
|
|
@@ -692,10 +1334,16 @@ export async function runCodexCompanion(argv) {
|
|
|
692
1334
|
});
|
|
693
1335
|
});
|
|
694
1336
|
const port = await listenOnLoopback(server, options.port, !options.portWasExplicit);
|
|
695
|
-
const pairing = Buffer.from(JSON.stringify({
|
|
696
|
-
|
|
1337
|
+
const pairing = Buffer.from(JSON.stringify({
|
|
1338
|
+
port,
|
|
1339
|
+
token,
|
|
1340
|
+
provider: providerInfo(defaultProviderId),
|
|
1341
|
+
providers: Array.from(runtimes.keys(), providerInfo),
|
|
1342
|
+
}), "utf8").toString("base64url");
|
|
1343
|
+
const url = companionPairingUrl(options.appUrl, pairing);
|
|
697
1344
|
console.log("");
|
|
698
|
-
console.log("Nudge
|
|
1345
|
+
console.log("Nudge Agent companion is running");
|
|
1346
|
+
console.log(` Agents: ${Array.from(runtimes.keys(), (id) => PROVIDERS[id].displayName).join(", ")}`);
|
|
699
1347
|
console.log(` App: ${options.appUrl}`);
|
|
700
1348
|
console.log(` Workspace: ${options.cwd}`);
|
|
701
1349
|
console.log(` Listener: http://127.0.0.1:${port}`);
|
|
@@ -704,7 +1352,15 @@ export async function runCodexCompanion(argv) {
|
|
|
704
1352
|
}
|
|
705
1353
|
console.log(" Files: read-only");
|
|
706
1354
|
console.log("");
|
|
707
|
-
|
|
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
|
+
}
|
|
708
1364
|
console.log("Keep this terminal open while using Nudge chat.");
|
|
709
1365
|
console.log("");
|
|
710
1366
|
if (options.openBrowser)
|
|
@@ -714,11 +1370,16 @@ export async function runCodexCompanion(argv) {
|
|
|
714
1370
|
await new Promise((resolve) => {
|
|
715
1371
|
const shutdown = () => {
|
|
716
1372
|
activeAbort?.abort();
|
|
717
|
-
|
|
1373
|
+
for (const runtime of runtimes.values())
|
|
1374
|
+
runtime.metadataClient?.stop();
|
|
718
1375
|
server.close(() => resolve());
|
|
719
1376
|
};
|
|
720
1377
|
process.once("SIGINT", shutdown);
|
|
721
1378
|
process.once("SIGTERM", shutdown);
|
|
722
1379
|
});
|
|
723
1380
|
}
|
|
1381
|
+
/** Backward-compatible alias for the original Codex-only command. */
|
|
1382
|
+
export async function runCodexCompanion(argv) {
|
|
1383
|
+
await runAgentCompanion(["--provider", "codex", ...argv]);
|
|
1384
|
+
}
|
|
724
1385
|
//# sourceMappingURL=codex-companion.js.map
|