@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.
@@ -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 Local Codex",
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 codex companion option: ${arg}`);
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 buildPrompt(prompt, context) {
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 subscriptionEnvironment() {
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(process.env).filter((entry) => entry[1] !== undefined && !blocked.has(entry[0])));
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 runCodexCompanion(argv) {
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
- assertChatGptLogin();
452
- const codex = new Codex({
453
- config: companionCodexConfig(),
454
- env: subscriptionEnvironment(),
455
- });
456
- const metadataClient = new CodexMetadataClient();
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, metadata, headers);
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 Codex thread id" }, headers);
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: "A Codex turn is already running" }, headers);
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
- thread ??= codex.startThread(companionThreadOptions(options.cwd, selectedModel, selectedEffort));
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
- send({ type: "connected", cwd: options.cwd, threadId: thread.id });
666
- const streamed = await thread.runStreamed(buildPrompt(prompt, parsed.context), {
667
- signal: abort.signal,
668
- });
669
- for await (const event of streamed.events) {
670
- send({ type: "codex", event: event });
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.id });
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({ port, token }), "utf8").toString("base64url");
696
- const url = `${options.appUrl}/#nudge-codex=${pairing}`;
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 Codex companion is running");
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("Codex will reuse the account from `codex login`.");
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.stop();
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