@nudge-ui/mcp 0.2.0 → 0.2.1

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/dist/cli.js CHANGED
@@ -2,7 +2,8 @@ import { createHash } from "node:crypto";
2
2
  import { spawn } from "node:child_process";
3
3
  import { basename, resolve } from "node:path";
4
4
  import { defaultBridgePort } from "./internal/agent-protocol.js";
5
- import { createAgentCompanion } from "./server.js";
5
+ import { discoverProjectSessions } from "./discovery.js";
6
+ import { createAgentCompanion, createDiscoveredAgentAdapter, } from "./server.js";
6
7
  const DEFAULT_PORT = 0;
7
8
  const DEFAULT_COMMAND_TIMEOUT_MS = 5_000;
8
9
  const PAGE_PROBE_TIMEOUT_MS = 1_500;
@@ -59,6 +60,8 @@ function parseInteger(value, flag, maximum) {
59
60
  }
60
61
  /** Parses only project-scoped, host-neutral CLI options. */
61
62
  export function parseCliArguments(argv = process.argv.slice(2), environment = process.env) {
63
+ const command = argv[0] === "doctor" ? "doctor" : "serve";
64
+ const argumentsToParse = command === "doctor" ? argv.slice(1) : argv;
62
65
  const root = resolve(environment.NUDGE_UI_WORKSPACE_ROOT ?? environment.INIT_CWD ?? process.cwd());
63
66
  let projectId = environment.NUDGE_UI_PROJECT_ID ?? projectIdFor(root);
64
67
  let origin = environment.NUDGE_UI_ORIGIN;
@@ -73,27 +76,27 @@ export function parseCliArguments(argv = process.argv.slice(2), environment = pr
73
76
  let workspaceRoot = root;
74
77
  let help = false;
75
78
  let explicitPort = environment.NUDGE_UI_BRIDGE_PORT !== undefined;
76
- for (let index = 0; index < argv.length; index += 1) {
77
- const argument = argv[index];
79
+ for (let index = 0; index < argumentsToParse.length; index += 1) {
80
+ const argument = argumentsToParse[index];
78
81
  switch (argument) {
79
82
  case "--help":
80
83
  case "-h":
81
84
  help = true;
82
85
  break;
83
86
  case "--project-id":
84
- projectId = valueAfter(argv, index, argument);
87
+ projectId = valueAfter(argumentsToParse, index, argument);
85
88
  index += 1;
86
89
  break;
87
90
  case "--origin":
88
- origin = valueAfter(argv, index, argument);
91
+ origin = valueAfter(argumentsToParse, index, argument);
89
92
  index += 1;
90
93
  break;
91
94
  case "--workspace-root":
92
- workspaceRoot = resolve(valueAfter(argv, index, argument));
95
+ workspaceRoot = resolve(valueAfter(argumentsToParse, index, argument));
93
96
  index += 1;
94
97
  break;
95
98
  case "--host": {
96
- const candidate = valueAfter(argv, index, argument);
99
+ const candidate = valueAfter(argumentsToParse, index, argument);
97
100
  if (candidate !== "127.0.0.1" && candidate !== "::1" && candidate !== "localhost") {
98
101
  throw new TypeError("--host must be 127.0.0.1, ::1, or localhost");
99
102
  }
@@ -102,12 +105,12 @@ export function parseCliArguments(argv = process.argv.slice(2), environment = pr
102
105
  break;
103
106
  }
104
107
  case "--port":
105
- port = parseInteger(valueAfter(argv, index, argument), argument, 65_535);
108
+ port = parseInteger(valueAfter(argumentsToParse, index, argument), argument, 65_535);
106
109
  explicitPort = true;
107
110
  index += 1;
108
111
  break;
109
112
  case "--command-timeout-ms":
110
- commandTimeoutMs = parseInteger(valueAfter(argv, index, argument), argument, 60_000);
113
+ commandTimeoutMs = parseInteger(valueAfter(argumentsToParse, index, argument), argument, 60_000);
111
114
  if (commandTimeoutMs === 0)
112
115
  throw new TypeError("--command-timeout-ms must be greater than zero");
113
116
  index += 1;
@@ -120,11 +123,12 @@ export function parseCliArguments(argv = process.argv.slice(2), environment = pr
120
123
  throw new TypeError("--project-id must be a bounded non-empty string");
121
124
  if (!explicitPort)
122
125
  port = defaultBridgePort(projectId);
123
- return { projectId, origin, workspaceRoot, host, port, commandTimeoutMs, help };
126
+ return { command, projectId, origin, workspaceRoot, host, port, commandTimeoutMs, help };
124
127
  }
125
- export const CLI_USAGE = `Usage: nudge-mcp [options]
128
+ export const CLI_USAGE = `Usage: nudge-mcp [doctor] [options]
126
129
 
127
- Starts a project-scoped standard MCP stdio server and loopback browser bridge.
130
+ Starts a standard MCP stdio adapter. It discovers the bridge owned by the
131
+ running project unless --origin selects the legacy coupled mode.
128
132
 
129
133
  Options:
130
134
  --project-id <id> Stable project pairing identity
@@ -142,6 +146,55 @@ export async function runCli(argv = process.argv.slice(2), environment = process
142
146
  process.stdout.write(CLI_USAGE);
143
147
  return;
144
148
  }
149
+ if (options.command === "doctor") {
150
+ const sessions = await discoverProjectSessions(options.workspaceRoot);
151
+ const matches = sessions.filter((session) => session.matchesApplication);
152
+ process.stdout.write([
153
+ `[nudge-ui] workspace: ${options.workspaceRoot}`,
154
+ `[nudge-ui] live sessions: ${sessions.length}`,
155
+ `[nudge-ui] matching sessions: ${matches.length}`,
156
+ ...(matches.length === 0
157
+ ? ["[nudge-ui] no matching project bridge is running; start the development server to make Nudge available"]
158
+ : matches.map((session) => {
159
+ const state = session.status.connection === "working"
160
+ ? "working"
161
+ : session.status.listenerActive ? "listening" : "available";
162
+ return `[nudge-ui] ${state}: ${session.projectId} (${session.origin})${session.claimed ? " [claimed]" : ""}`;
163
+ })),
164
+ ].join("\n") + "\n");
165
+ return;
166
+ }
167
+ if (!options.origin) {
168
+ const adapter = await createDiscoveredAgentAdapter({ workspaceRoot: options.workspaceRoot });
169
+ let stopping = false;
170
+ let resolveAdapterWait = null;
171
+ const stop = () => {
172
+ if (stopping)
173
+ return;
174
+ stopping = true;
175
+ void adapter.close();
176
+ resolveAdapterWait?.();
177
+ };
178
+ process.once("SIGINT", stop);
179
+ process.once("SIGTERM", stop);
180
+ try {
181
+ await adapter.start();
182
+ await new Promise((resolveAdapter) => {
183
+ const finish = () => resolveAdapter();
184
+ // Keep a direct resolver so signal handlers terminate even though
185
+ // installing a signal handler suppresses Node's default process exit.
186
+ resolveAdapterWait = finish;
187
+ process.stdin.once("end", finish);
188
+ process.once("exit", finish);
189
+ });
190
+ }
191
+ finally {
192
+ process.off("SIGINT", stop);
193
+ process.off("SIGTERM", stop);
194
+ await adapter.close();
195
+ }
196
+ return;
197
+ }
145
198
  const companion = createAgentCompanion({
146
199
  ...options,
147
200
  onControllerUnavailable: (pageUrl) => openPairedPage(pageUrl).then(() => undefined),