@nudge-ui/mcp 0.1.3 → 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
@@ -1,8 +1,9 @@
1
1
  import { createHash } from "node:crypto";
2
2
  import { spawn } from "node:child_process";
3
3
  import { basename, resolve } from "node:path";
4
- import { defaultBridgePort } from "@nudge-ui/agent-protocol";
5
- import { createAgentCompanion } from "./server.js";
4
+ import { defaultBridgePort } from "./internal/agent-protocol.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,9 +60,12 @@ 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;
68
+ // SAFETY: NUDGE_UI_BRIDGE_HOST is trusted operator config for a loopback-bound dev bridge; CliOptions["host"] is its declared address union.
65
69
  let host = (environment.NUDGE_UI_BRIDGE_HOST ?? "127.0.0.1");
66
70
  let port = environment.NUDGE_UI_BRIDGE_PORT === undefined
67
71
  ? DEFAULT_PORT
@@ -72,27 +76,27 @@ export function parseCliArguments(argv = process.argv.slice(2), environment = pr
72
76
  let workspaceRoot = root;
73
77
  let help = false;
74
78
  let explicitPort = environment.NUDGE_UI_BRIDGE_PORT !== undefined;
75
- for (let index = 0; index < argv.length; index += 1) {
76
- const argument = argv[index];
79
+ for (let index = 0; index < argumentsToParse.length; index += 1) {
80
+ const argument = argumentsToParse[index];
77
81
  switch (argument) {
78
82
  case "--help":
79
83
  case "-h":
80
84
  help = true;
81
85
  break;
82
86
  case "--project-id":
83
- projectId = valueAfter(argv, index, argument);
87
+ projectId = valueAfter(argumentsToParse, index, argument);
84
88
  index += 1;
85
89
  break;
86
90
  case "--origin":
87
- origin = valueAfter(argv, index, argument);
91
+ origin = valueAfter(argumentsToParse, index, argument);
88
92
  index += 1;
89
93
  break;
90
94
  case "--workspace-root":
91
- workspaceRoot = resolve(valueAfter(argv, index, argument));
95
+ workspaceRoot = resolve(valueAfter(argumentsToParse, index, argument));
92
96
  index += 1;
93
97
  break;
94
98
  case "--host": {
95
- const candidate = valueAfter(argv, index, argument);
99
+ const candidate = valueAfter(argumentsToParse, index, argument);
96
100
  if (candidate !== "127.0.0.1" && candidate !== "::1" && candidate !== "localhost") {
97
101
  throw new TypeError("--host must be 127.0.0.1, ::1, or localhost");
98
102
  }
@@ -101,12 +105,12 @@ export function parseCliArguments(argv = process.argv.slice(2), environment = pr
101
105
  break;
102
106
  }
103
107
  case "--port":
104
- port = parseInteger(valueAfter(argv, index, argument), argument, 65_535);
108
+ port = parseInteger(valueAfter(argumentsToParse, index, argument), argument, 65_535);
105
109
  explicitPort = true;
106
110
  index += 1;
107
111
  break;
108
112
  case "--command-timeout-ms":
109
- commandTimeoutMs = parseInteger(valueAfter(argv, index, argument), argument, 60_000);
113
+ commandTimeoutMs = parseInteger(valueAfter(argumentsToParse, index, argument), argument, 60_000);
110
114
  if (commandTimeoutMs === 0)
111
115
  throw new TypeError("--command-timeout-ms must be greater than zero");
112
116
  index += 1;
@@ -119,11 +123,12 @@ export function parseCliArguments(argv = process.argv.slice(2), environment = pr
119
123
  throw new TypeError("--project-id must be a bounded non-empty string");
120
124
  if (!explicitPort)
121
125
  port = defaultBridgePort(projectId);
122
- return { projectId, origin, workspaceRoot, host, port, commandTimeoutMs, help };
126
+ return { command, projectId, origin, workspaceRoot, host, port, commandTimeoutMs, help };
123
127
  }
124
- export const CLI_USAGE = `Usage: nudge-mcp [options]
128
+ export const CLI_USAGE = `Usage: nudge-mcp [doctor] [options]
125
129
 
126
- 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.
127
132
 
128
133
  Options:
129
134
  --project-id <id> Stable project pairing identity
@@ -141,6 +146,55 @@ export async function runCli(argv = process.argv.slice(2), environment = process
141
146
  process.stdout.write(CLI_USAGE);
142
147
  return;
143
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
+ }
144
198
  const companion = createAgentCompanion({
145
199
  ...options,
146
200
  onControllerUnavailable: (pageUrl) => openPairedPage(pageUrl).then(() => undefined),