@euqns/nudge-mcp 0.14.0 → 0.16.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 +47 -19
- 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 +367 -29
- package/dist/codex-companion.js.map +1 -1
- package/dist/codex-companion.test.js +51 -1
- package/dist/codex-companion.test.js.map +1 -1
- package/dist/engines/claude.js +3 -0
- package/dist/engines/claude.js.map +1 -1
- package/dist/engines/codex.js +4 -2
- package/dist/engines/codex.js.map +1 -1
- package/dist/engines/types.js.map +1 -1
- package/dist/index.js +18 -6
- package/dist/index.js.map +1 -1
- package/dist/runner.js +176 -176
- package/dist/runner.js.map +1 -1
- package/dist/runner.test.js +27 -0
- package/dist/runner.test.js.map +1 -0
- package/dist/version.js +3 -3
- package/dist/version.js.map +1 -1
- package/package.json +3 -1
package/dist/codex-companion.js
CHANGED
|
@@ -10,6 +10,23 @@ 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
|
+
};
|
|
13
30
|
const require = createRequire(import.meta.url);
|
|
14
31
|
const REASONING_EFFORTS = new Set([
|
|
15
32
|
"none",
|
|
@@ -73,7 +90,7 @@ class CodexMetadataClient {
|
|
|
73
90
|
await this.rawRequest("initialize", {
|
|
74
91
|
clientInfo: {
|
|
75
92
|
name: "nudge-companion",
|
|
76
|
-
title: "Nudge
|
|
93
|
+
title: "Nudge Agent",
|
|
77
94
|
version: NUDGE_MCP_VERSION,
|
|
78
95
|
},
|
|
79
96
|
capabilities: {
|
|
@@ -227,6 +244,7 @@ function parseArgs(argv) {
|
|
|
227
244
|
let openBrowserOnStart = true;
|
|
228
245
|
let port = DEFAULT_PORT;
|
|
229
246
|
let portWasExplicit = false;
|
|
247
|
+
let provider = "codex";
|
|
230
248
|
for (let index = 0; index < argv.length; index++) {
|
|
231
249
|
const arg = argv[index];
|
|
232
250
|
const next = argv[index + 1];
|
|
@@ -256,8 +274,22 @@ function parseArgs(argv) {
|
|
|
256
274
|
else if (arg === "--no-open") {
|
|
257
275
|
openBrowserOnStart = false;
|
|
258
276
|
}
|
|
277
|
+
else if (arg === "--provider" && next) {
|
|
278
|
+
if (!(next in PROVIDERS)) {
|
|
279
|
+
throw new Error("--provider must be codex, claude, or cursor");
|
|
280
|
+
}
|
|
281
|
+
provider = next;
|
|
282
|
+
index++;
|
|
283
|
+
}
|
|
284
|
+
else if (arg.startsWith("--provider=")) {
|
|
285
|
+
const value = arg.slice("--provider=".length);
|
|
286
|
+
if (!(value in PROVIDERS)) {
|
|
287
|
+
throw new Error("--provider must be codex, claude, or cursor");
|
|
288
|
+
}
|
|
289
|
+
provider = value;
|
|
290
|
+
}
|
|
259
291
|
else {
|
|
260
|
-
throw new Error(`Unknown
|
|
292
|
+
throw new Error(`Unknown agent companion option: ${arg}`);
|
|
261
293
|
}
|
|
262
294
|
}
|
|
263
295
|
if (!Number.isInteger(port) || port < 1024 || port > 65535) {
|
|
@@ -269,6 +301,7 @@ function parseArgs(argv) {
|
|
|
269
301
|
openBrowser: openBrowserOnStart,
|
|
270
302
|
port,
|
|
271
303
|
portWasExplicit,
|
|
304
|
+
provider,
|
|
272
305
|
};
|
|
273
306
|
}
|
|
274
307
|
async function listenOnLoopback(server, preferredPort, allowFallback) {
|
|
@@ -332,7 +365,7 @@ function writeJson(res, status, value, headers) {
|
|
|
332
365
|
res.writeHead(status, { ...headers, "Content-Type": "application/json" });
|
|
333
366
|
res.end(JSON.stringify(value));
|
|
334
367
|
}
|
|
335
|
-
function
|
|
368
|
+
export function buildCompanionPrompt(prompt, context) {
|
|
336
369
|
const pathname = typeof context?.pathname === "string" ? context.pathname.slice(0, 500) : undefined;
|
|
337
370
|
const routeContext = pathname
|
|
338
371
|
? `The user opened this chat from the Nudge route ${JSON.stringify(pathname)}.`
|
|
@@ -347,21 +380,52 @@ function buildPrompt(prompt, context) {
|
|
|
347
380
|
source: context.boardSource === "cloud" || context.boardSource === "local"
|
|
348
381
|
? context.boardSource
|
|
349
382
|
: undefined,
|
|
383
|
+
role: context.boardRole === "owner" ||
|
|
384
|
+
context.boardRole === "editor" ||
|
|
385
|
+
context.boardRole === "viewer"
|
|
386
|
+
? context.boardRole
|
|
387
|
+
: undefined,
|
|
350
388
|
view: typeof context.view === "string" ? context.view.slice(0, 50) : undefined,
|
|
351
389
|
canvasId: typeof context.canvasId === "string"
|
|
352
390
|
? context.canvasId.slice(0, 200)
|
|
353
391
|
: undefined,
|
|
354
392
|
cardId: typeof context.cardId === "string" ? context.cardId.slice(0, 200) : undefined,
|
|
393
|
+
cardTitle: typeof context.cardTitle === "string"
|
|
394
|
+
? context.cardTitle.slice(0, 500)
|
|
395
|
+
: undefined,
|
|
396
|
+
cardDescription: typeof context.cardDescription === "string"
|
|
397
|
+
? context.cardDescription.slice(0, 4_000)
|
|
398
|
+
: undefined,
|
|
399
|
+
cardHref: typeof context.cardHref === "string"
|
|
400
|
+
? context.cardHref.slice(0, 1_000)
|
|
401
|
+
: undefined,
|
|
402
|
+
listId: typeof context.listId === "string" ? context.listId.slice(0, 200) : undefined,
|
|
403
|
+
listTitle: typeof context.listTitle === "string"
|
|
404
|
+
? context.listTitle.slice(0, 500)
|
|
405
|
+
: undefined,
|
|
406
|
+
selectionSource: typeof context.selectionSource === "string"
|
|
407
|
+
? context.selectionSource.slice(0, 50)
|
|
408
|
+
: undefined,
|
|
409
|
+
repository: context.repository && typeof context.repository === "object"
|
|
410
|
+
? context.repository
|
|
411
|
+
: undefined,
|
|
355
412
|
listCount: typeof context.listCount === "number" ? context.listCount : undefined,
|
|
356
413
|
cardCount: typeof context.cardCount === "number" ? context.cardCount : undefined,
|
|
357
414
|
}),
|
|
358
415
|
"Use this exact board id for Nudge MCP calls unless the user names another board.",
|
|
416
|
+
context.cardId
|
|
417
|
+
? "Treat the selected card as the subject when the user says this task, this card, or it."
|
|
418
|
+
: "No card is selected; do not guess which card the user means.",
|
|
359
419
|
]
|
|
360
420
|
: [];
|
|
361
421
|
return [
|
|
362
422
|
routeContext,
|
|
363
423
|
...boardContext,
|
|
364
424
|
"Use the configured Nudge MCP tools when the request needs board or card data.",
|
|
425
|
+
"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.",
|
|
426
|
+
"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.",
|
|
427
|
+
"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.",
|
|
428
|
+
"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
429
|
"Be concise about progress and return a clear final answer for the Nudge chat UI.",
|
|
366
430
|
"The local filesystem is read-only. Nudge MCP tools may update boards when requested; do not claim that local files were changed.",
|
|
367
431
|
"",
|
|
@@ -386,13 +450,34 @@ export function assertChatGptLogin() {
|
|
|
386
450
|
].join("\n"));
|
|
387
451
|
}
|
|
388
452
|
}
|
|
389
|
-
export function
|
|
453
|
+
export function assertCompanionProvider(provider) {
|
|
454
|
+
if (provider === "codex") {
|
|
455
|
+
assertChatGptLogin();
|
|
456
|
+
return;
|
|
457
|
+
}
|
|
458
|
+
const definition = PROVIDERS[provider];
|
|
459
|
+
const args = provider === "claude" ? ["auth", "status"] : ["status"];
|
|
460
|
+
const result = spawnSync(definition.binary, args, {
|
|
461
|
+
encoding: "utf8",
|
|
462
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
463
|
+
});
|
|
464
|
+
if (result.error) {
|
|
465
|
+
throw new Error(`${definition.displayName} CLI was not found. Install it, then run \`${definition.loginCommand}\`.`);
|
|
466
|
+
}
|
|
467
|
+
if (result.status !== 0) {
|
|
468
|
+
const detail = `${result.stdout ?? ""}\n${result.stderr ?? ""}`.trim();
|
|
469
|
+
throw new Error(`${definition.displayName} is not authenticated. Run \`${definition.loginCommand}\`.${detail ? `\nCurrent status: ${detail}` : ""}`);
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
export function subscriptionEnvironment(environment = process.env) {
|
|
390
473
|
const blocked = new Set([
|
|
474
|
+
"ANTHROPIC_API_KEY",
|
|
475
|
+
"CURSOR_API_KEY",
|
|
391
476
|
"CODEX_ACCESS_TOKEN",
|
|
392
477
|
"CODEX_API_KEY",
|
|
393
478
|
"OPENAI_API_KEY",
|
|
394
479
|
]);
|
|
395
|
-
return Object.fromEntries(Object.entries(
|
|
480
|
+
return Object.fromEntries(Object.entries(environment).filter((entry) => entry[1] !== undefined && !blocked.has(entry[0])));
|
|
396
481
|
}
|
|
397
482
|
export function companionMcpTransport() {
|
|
398
483
|
const companionFile = fileURLToPath(import.meta.url);
|
|
@@ -432,6 +517,186 @@ export function companionCodexConfig(env) {
|
|
|
432
517
|
},
|
|
433
518
|
};
|
|
434
519
|
}
|
|
520
|
+
function companionClaudeConfig() {
|
|
521
|
+
const transport = companionMcpTransport();
|
|
522
|
+
return JSON.stringify({
|
|
523
|
+
mcpServers: {
|
|
524
|
+
nudge: {
|
|
525
|
+
command: transport.command,
|
|
526
|
+
args: transport.args,
|
|
527
|
+
},
|
|
528
|
+
},
|
|
529
|
+
});
|
|
530
|
+
}
|
|
531
|
+
export function companionCliInvocation(provider, prompt, threadId) {
|
|
532
|
+
const definition = PROVIDERS[provider];
|
|
533
|
+
if (provider === "claude") {
|
|
534
|
+
return {
|
|
535
|
+
command: definition.binary,
|
|
536
|
+
args: [
|
|
537
|
+
"-p",
|
|
538
|
+
"--output-format",
|
|
539
|
+
"stream-json",
|
|
540
|
+
"--verbose",
|
|
541
|
+
"--permission-mode",
|
|
542
|
+
"bypassPermissions",
|
|
543
|
+
"--disallowedTools",
|
|
544
|
+
"Edit",
|
|
545
|
+
"Write",
|
|
546
|
+
"NotebookEdit",
|
|
547
|
+
"Bash",
|
|
548
|
+
"--mcp-config",
|
|
549
|
+
companionClaudeConfig(),
|
|
550
|
+
"--strict-mcp-config",
|
|
551
|
+
...(threadId ? ["--resume", threadId] : []),
|
|
552
|
+
],
|
|
553
|
+
stdin: prompt,
|
|
554
|
+
};
|
|
555
|
+
}
|
|
556
|
+
return {
|
|
557
|
+
command: definition.binary,
|
|
558
|
+
args: [
|
|
559
|
+
"-p",
|
|
560
|
+
"--mode=ask",
|
|
561
|
+
"--output-format",
|
|
562
|
+
"stream-json",
|
|
563
|
+
...(threadId ? ["--resume", threadId] : []),
|
|
564
|
+
prompt,
|
|
565
|
+
],
|
|
566
|
+
stdin: null,
|
|
567
|
+
};
|
|
568
|
+
}
|
|
569
|
+
export function companionPairingUrl(appUrl, pairing) {
|
|
570
|
+
return `${appUrl}/#nudge-agent=${pairing}`;
|
|
571
|
+
}
|
|
572
|
+
function syntheticUsage(usage) {
|
|
573
|
+
return {
|
|
574
|
+
input_tokens: typeof usage?.input_tokens === "number" ? usage.input_tokens : 0,
|
|
575
|
+
cached_input_tokens: typeof usage?.cache_read_input_tokens === "number"
|
|
576
|
+
? usage.cache_read_input_tokens
|
|
577
|
+
: 0,
|
|
578
|
+
cache_write_input_tokens: typeof usage?.cache_creation_input_tokens === "number"
|
|
579
|
+
? usage.cache_creation_input_tokens
|
|
580
|
+
: 0,
|
|
581
|
+
output_tokens: typeof usage?.output_tokens === "number" ? usage.output_tokens : 0,
|
|
582
|
+
reasoning_output_tokens: 0,
|
|
583
|
+
};
|
|
584
|
+
}
|
|
585
|
+
function cliActivity(provider, event) {
|
|
586
|
+
if (provider === "cursor" && event.type === "tool_call") {
|
|
587
|
+
const call = event.tool_call;
|
|
588
|
+
if (!call || typeof call !== "object")
|
|
589
|
+
return "Used a Cursor tool";
|
|
590
|
+
const name = Object.keys(call)[0];
|
|
591
|
+
return name ? `Used ${name.replace(/ToolCall$/, "")}` : "Used a Cursor tool";
|
|
592
|
+
}
|
|
593
|
+
if (provider === "claude" && event.type === "assistant") {
|
|
594
|
+
const message = event.message;
|
|
595
|
+
const blocks = Array.isArray(message?.content) ? message.content : [];
|
|
596
|
+
const tool = blocks.find((block) => !!block &&
|
|
597
|
+
typeof block === "object" &&
|
|
598
|
+
block.type === "tool_use");
|
|
599
|
+
return tool?.name ? `Used ${String(tool.name)}` : null;
|
|
600
|
+
}
|
|
601
|
+
return null;
|
|
602
|
+
}
|
|
603
|
+
async function runSubscriptionCliTurn({ provider, prompt, cwd, threadId, signal, send, }) {
|
|
604
|
+
const definition = PROVIDERS[provider];
|
|
605
|
+
const invocation = companionCliInvocation(provider, prompt, threadId);
|
|
606
|
+
send({ type: "agent", event: { type: "turn.started" } });
|
|
607
|
+
return await new Promise((resolve, reject) => {
|
|
608
|
+
const child = spawn(invocation.command, invocation.args, {
|
|
609
|
+
cwd,
|
|
610
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
611
|
+
env: subscriptionEnvironment(),
|
|
612
|
+
});
|
|
613
|
+
let stderr = "";
|
|
614
|
+
let activeThreadId = threadId;
|
|
615
|
+
let finalText = "";
|
|
616
|
+
let usage;
|
|
617
|
+
const lines = readline.createInterface({ input: child.stdout, crlfDelay: Infinity });
|
|
618
|
+
lines.on("line", (line) => {
|
|
619
|
+
if (!line.trim())
|
|
620
|
+
return;
|
|
621
|
+
let event;
|
|
622
|
+
try {
|
|
623
|
+
event = JSON.parse(line);
|
|
624
|
+
}
|
|
625
|
+
catch {
|
|
626
|
+
return;
|
|
627
|
+
}
|
|
628
|
+
if (typeof event.session_id === "string") {
|
|
629
|
+
activeThreadId = event.session_id;
|
|
630
|
+
if (event.type === "system") {
|
|
631
|
+
send({
|
|
632
|
+
type: "agent",
|
|
633
|
+
event: { type: "thread.started", thread_id: activeThreadId },
|
|
634
|
+
});
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
const activity = cliActivity(provider, event);
|
|
638
|
+
if (activity) {
|
|
639
|
+
const id = typeof event.call_id === "string"
|
|
640
|
+
? event.call_id
|
|
641
|
+
: `tool-${Date.now().toString(36)}`;
|
|
642
|
+
send({
|
|
643
|
+
type: "agent",
|
|
644
|
+
event: {
|
|
645
|
+
type: event.subtype === "completed" ? "item.completed" : "item.started",
|
|
646
|
+
item: { id, type: "reasoning", text: activity },
|
|
647
|
+
},
|
|
648
|
+
});
|
|
649
|
+
}
|
|
650
|
+
if (event.type === "result") {
|
|
651
|
+
if (typeof event.result === "string")
|
|
652
|
+
finalText = event.result;
|
|
653
|
+
if (event.usage && typeof event.usage === "object") {
|
|
654
|
+
usage = event.usage;
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
});
|
|
658
|
+
child.stderr.on("data", (chunk) => {
|
|
659
|
+
stderr = `${stderr}${chunk.toString("utf8")}`.slice(-8_000);
|
|
660
|
+
});
|
|
661
|
+
const onAbort = () => child.kill("SIGTERM");
|
|
662
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
663
|
+
child.once("error", (error) => {
|
|
664
|
+
signal.removeEventListener("abort", onAbort);
|
|
665
|
+
reject(error);
|
|
666
|
+
});
|
|
667
|
+
child.once("close", (code) => {
|
|
668
|
+
signal.removeEventListener("abort", onAbort);
|
|
669
|
+
if (signal.aborted) {
|
|
670
|
+
resolve(activeThreadId);
|
|
671
|
+
return;
|
|
672
|
+
}
|
|
673
|
+
if (code !== 0) {
|
|
674
|
+
reject(new Error(`${definition.displayName} exited ${code}: ${stderr.trim().slice(-800) || "no error detail"}`));
|
|
675
|
+
return;
|
|
676
|
+
}
|
|
677
|
+
send({
|
|
678
|
+
type: "agent",
|
|
679
|
+
event: {
|
|
680
|
+
type: "item.completed",
|
|
681
|
+
item: {
|
|
682
|
+
id: `message-${Date.now().toString(36)}`,
|
|
683
|
+
type: "agent_message",
|
|
684
|
+
text: finalText,
|
|
685
|
+
},
|
|
686
|
+
},
|
|
687
|
+
});
|
|
688
|
+
send({
|
|
689
|
+
type: "agent",
|
|
690
|
+
event: { type: "turn.completed", usage: syntheticUsage(usage) },
|
|
691
|
+
});
|
|
692
|
+
resolve(activeThreadId);
|
|
693
|
+
});
|
|
694
|
+
if (invocation.stdin !== null) {
|
|
695
|
+
child.stdin.write(invocation.stdin);
|
|
696
|
+
}
|
|
697
|
+
child.stdin.end();
|
|
698
|
+
});
|
|
699
|
+
}
|
|
435
700
|
function companionThreadOptions(cwd, model, effort) {
|
|
436
701
|
return {
|
|
437
702
|
approvalPolicy: "never",
|
|
@@ -444,17 +709,23 @@ function companionThreadOptions(cwd, model, effort) {
|
|
|
444
709
|
modelReasoningEffort: effort,
|
|
445
710
|
};
|
|
446
711
|
}
|
|
447
|
-
export async function
|
|
712
|
+
export async function runAgentCompanion(argv) {
|
|
448
713
|
const options = parseArgs(argv);
|
|
714
|
+
const provider = PROVIDERS[options.provider];
|
|
449
715
|
const allowedOrigin = new URL(options.appUrl).origin;
|
|
450
716
|
const token = crypto.randomBytes(32).toString("base64url");
|
|
451
|
-
|
|
452
|
-
const codex =
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
717
|
+
assertCompanionProvider(options.provider);
|
|
718
|
+
const codex = options.provider === "codex"
|
|
719
|
+
? new Codex({
|
|
720
|
+
config: companionCodexConfig(),
|
|
721
|
+
env: subscriptionEnvironment(),
|
|
722
|
+
})
|
|
723
|
+
: null;
|
|
724
|
+
const metadataClient = options.provider === "codex"
|
|
725
|
+
? new CodexMetadataClient()
|
|
726
|
+
: null;
|
|
457
727
|
let thread = null;
|
|
728
|
+
let cliThreadId = null;
|
|
458
729
|
let selectedModel = null;
|
|
459
730
|
let selectedEffort = null;
|
|
460
731
|
let activeAbort = null;
|
|
@@ -491,18 +762,45 @@ export async function runCodexCompanion(argv) {
|
|
|
491
762
|
ok: true,
|
|
492
763
|
cwd: options.cwd,
|
|
493
764
|
running: activeAbort !== null,
|
|
494
|
-
threadId: thread?.id ?? null,
|
|
765
|
+
threadId: options.provider === "codex" ? thread?.id ?? null : cliThreadId,
|
|
495
766
|
model: selectedModel,
|
|
496
767
|
effort: selectedEffort,
|
|
768
|
+
provider: { id: options.provider, displayName: provider.displayName },
|
|
497
769
|
}, headers);
|
|
498
770
|
return;
|
|
499
771
|
}
|
|
500
772
|
if (req.method === "GET" && req.url === "/v1/metadata") {
|
|
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
|
+
}
|
|
501
795
|
try {
|
|
502
796
|
const metadata = await metadataClient.metadata(selectedModel, selectedEffort);
|
|
503
797
|
selectedModel = metadata.currentModel;
|
|
504
798
|
selectedEffort = metadata.currentReasoningEffort;
|
|
505
|
-
writeJson(res, 200,
|
|
799
|
+
writeJson(res, 200, {
|
|
800
|
+
...metadata,
|
|
801
|
+
provider: { id: options.provider, displayName: provider.displayName },
|
|
802
|
+
capabilities: { modelSelection: true, effortSelection: true },
|
|
803
|
+
}, headers);
|
|
506
804
|
}
|
|
507
805
|
catch (error) {
|
|
508
806
|
writeJson(res, 503, {
|
|
@@ -514,6 +812,10 @@ export async function runCodexCompanion(argv) {
|
|
|
514
812
|
return;
|
|
515
813
|
}
|
|
516
814
|
if (req.method === "POST" && req.url === "/v1/model") {
|
|
815
|
+
if (!metadataClient) {
|
|
816
|
+
writeJson(res, 400, { error: `${provider.displayName} controls model selection in its own CLI.` }, headers);
|
|
817
|
+
return;
|
|
818
|
+
}
|
|
517
819
|
if (activeAbort) {
|
|
518
820
|
writeJson(res, 409, { error: "A Codex turn is still running" }, headers);
|
|
519
821
|
return;
|
|
@@ -540,6 +842,10 @@ export async function runCodexCompanion(argv) {
|
|
|
540
842
|
return;
|
|
541
843
|
}
|
|
542
844
|
if (req.method === "POST" && req.url === "/v1/effort") {
|
|
845
|
+
if (!metadataClient) {
|
|
846
|
+
writeJson(res, 400, { error: `${provider.displayName} controls reasoning settings in its own CLI.` }, headers);
|
|
847
|
+
return;
|
|
848
|
+
}
|
|
543
849
|
if (activeAbort) {
|
|
544
850
|
writeJson(res, 409, { error: "A Codex turn is still running" }, headers);
|
|
545
851
|
return;
|
|
@@ -563,6 +869,7 @@ export async function runCodexCompanion(argv) {
|
|
|
563
869
|
selectedModel = metadata.currentModel;
|
|
564
870
|
selectedEffort = effort;
|
|
565
871
|
thread = null;
|
|
872
|
+
cliThreadId = null;
|
|
566
873
|
writeJson(res, 200, { ok: true, model: selectedModel, effort }, headers);
|
|
567
874
|
return;
|
|
568
875
|
}
|
|
@@ -572,6 +879,7 @@ export async function runCodexCompanion(argv) {
|
|
|
572
879
|
return;
|
|
573
880
|
}
|
|
574
881
|
thread = null;
|
|
882
|
+
cliThreadId = null;
|
|
575
883
|
writeJson(res, 200, { ok: true }, headers);
|
|
576
884
|
return;
|
|
577
885
|
}
|
|
@@ -590,7 +898,12 @@ export async function runCodexCompanion(argv) {
|
|
|
590
898
|
}
|
|
591
899
|
const threadId = typeof parsed.threadId === "string" ? parsed.threadId.trim() : "";
|
|
592
900
|
if (!/^[a-zA-Z0-9-]{10,100}$/.test(threadId)) {
|
|
593
|
-
writeJson(res, 400, { error: "Invalid
|
|
901
|
+
writeJson(res, 400, { error: "Invalid coding-agent session id" }, headers);
|
|
902
|
+
return;
|
|
903
|
+
}
|
|
904
|
+
if (!metadataClient || !codex) {
|
|
905
|
+
cliThreadId = threadId;
|
|
906
|
+
writeJson(res, 200, { ok: true, threadId }, headers);
|
|
594
907
|
return;
|
|
595
908
|
}
|
|
596
909
|
const requestedModel = typeof parsed.model === "string" ? parsed.model.trim() : "";
|
|
@@ -628,7 +941,7 @@ export async function runCodexCompanion(argv) {
|
|
|
628
941
|
return;
|
|
629
942
|
}
|
|
630
943
|
if (activeAbort) {
|
|
631
|
-
writeJson(res, 409, { error:
|
|
944
|
+
writeJson(res, 409, { error: `A ${provider.displayName} turn is already running` }, headers);
|
|
632
945
|
return;
|
|
633
946
|
}
|
|
634
947
|
let parsed;
|
|
@@ -644,7 +957,9 @@ export async function runCodexCompanion(argv) {
|
|
|
644
957
|
writeJson(res, 400, { error: "prompt is required" }, headers);
|
|
645
958
|
return;
|
|
646
959
|
}
|
|
647
|
-
|
|
960
|
+
if (codex) {
|
|
961
|
+
thread ??= codex.startThread(companionThreadOptions(options.cwd, selectedModel, selectedEffort));
|
|
962
|
+
}
|
|
648
963
|
const abort = new AbortController();
|
|
649
964
|
activeAbort = abort;
|
|
650
965
|
let responseFinished = false;
|
|
@@ -662,14 +977,28 @@ export async function runCodexCompanion(argv) {
|
|
|
662
977
|
res.write(`${JSON.stringify(value)}\n`);
|
|
663
978
|
};
|
|
664
979
|
try {
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
980
|
+
const currentThreadId = codex ? thread?.id ?? null : cliThreadId;
|
|
981
|
+
send({ type: "connected", cwd: options.cwd, threadId: currentThreadId });
|
|
982
|
+
const contextualPrompt = buildCompanionPrompt(prompt, parsed.context);
|
|
983
|
+
if (codex && thread) {
|
|
984
|
+
const streamed = await thread.runStreamed(contextualPrompt, {
|
|
985
|
+
signal: abort.signal,
|
|
986
|
+
});
|
|
987
|
+
for await (const event of streamed.events) {
|
|
988
|
+
send({ type: "agent", event: event });
|
|
989
|
+
}
|
|
990
|
+
}
|
|
991
|
+
else {
|
|
992
|
+
cliThreadId = await runSubscriptionCliTurn({
|
|
993
|
+
provider: options.provider,
|
|
994
|
+
prompt: contextualPrompt,
|
|
995
|
+
cwd: options.cwd,
|
|
996
|
+
threadId: cliThreadId,
|
|
997
|
+
signal: abort.signal,
|
|
998
|
+
send,
|
|
999
|
+
});
|
|
671
1000
|
}
|
|
672
|
-
send({ type: "done", threadId: thread
|
|
1001
|
+
send({ type: "done", threadId: codex ? thread?.id ?? null : cliThreadId });
|
|
673
1002
|
}
|
|
674
1003
|
catch (error) {
|
|
675
1004
|
send({
|
|
@@ -692,10 +1021,15 @@ export async function runCodexCompanion(argv) {
|
|
|
692
1021
|
});
|
|
693
1022
|
});
|
|
694
1023
|
const port = await listenOnLoopback(server, options.port, !options.portWasExplicit);
|
|
695
|
-
const pairing = Buffer.from(JSON.stringify({
|
|
696
|
-
|
|
1024
|
+
const pairing = Buffer.from(JSON.stringify({
|
|
1025
|
+
port,
|
|
1026
|
+
token,
|
|
1027
|
+
provider: { id: options.provider, displayName: provider.displayName },
|
|
1028
|
+
}), "utf8").toString("base64url");
|
|
1029
|
+
const url = companionPairingUrl(options.appUrl, pairing);
|
|
697
1030
|
console.log("");
|
|
698
|
-
console.log("Nudge
|
|
1031
|
+
console.log("Nudge Agent companion is running");
|
|
1032
|
+
console.log(` Agent: ${provider.displayName}`);
|
|
699
1033
|
console.log(` App: ${options.appUrl}`);
|
|
700
1034
|
console.log(` Workspace: ${options.cwd}`);
|
|
701
1035
|
console.log(` Listener: http://127.0.0.1:${port}`);
|
|
@@ -704,7 +1038,7 @@ export async function runCodexCompanion(argv) {
|
|
|
704
1038
|
}
|
|
705
1039
|
console.log(" Files: read-only");
|
|
706
1040
|
console.log("");
|
|
707
|
-
console.log(
|
|
1041
|
+
console.log(`${provider.displayName} will reuse the account from \`${provider.loginCommand}\`.`);
|
|
708
1042
|
console.log("Keep this terminal open while using Nudge chat.");
|
|
709
1043
|
console.log("");
|
|
710
1044
|
if (options.openBrowser)
|
|
@@ -714,11 +1048,15 @@ export async function runCodexCompanion(argv) {
|
|
|
714
1048
|
await new Promise((resolve) => {
|
|
715
1049
|
const shutdown = () => {
|
|
716
1050
|
activeAbort?.abort();
|
|
717
|
-
metadataClient
|
|
1051
|
+
metadataClient?.stop();
|
|
718
1052
|
server.close(() => resolve());
|
|
719
1053
|
};
|
|
720
1054
|
process.once("SIGINT", shutdown);
|
|
721
1055
|
process.once("SIGTERM", shutdown);
|
|
722
1056
|
});
|
|
723
1057
|
}
|
|
1058
|
+
/** Backward-compatible alias for the original Codex-only command. */
|
|
1059
|
+
export async function runCodexCompanion(argv) {
|
|
1060
|
+
await runAgentCompanion(["--provider", "codex", ...argv]);
|
|
1061
|
+
}
|
|
724
1062
|
//# sourceMappingURL=codex-companion.js.map
|