premanmcp 0.10.5 → 0.10.6

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 CHANGED
@@ -133,6 +133,7 @@ npm exec -y premanmcp@latest -- connect --project
133
133
  ## What It Does
134
134
 
135
135
  - Converts API endpoints into agent-callable MCP tools.
136
+ - Migrates a Postman collection in one call (`migrate_from_postman`): keeps the assertions your `pm.test` blocks declared, splits the environment into shared variables and encrypted secrets, and schedules a monitored test suite per request.
136
137
  - Creates/connects a PreMan account from the terminal or IDE agent.
137
138
  - Lets agents test real backend endpoints from the IDE.
138
139
  - Syncs endpoint inventory across backend and frontend workflows.
@@ -166,6 +167,10 @@ Convert these endpoints into an MCP.
166
167
  Show me the audit log for this hosted MCP.
167
168
  ```
168
169
 
170
+ ```text
171
+ Move my Postman collection over to PreMan.
172
+ ```
173
+
169
174
  ```text
170
175
  Pull my pending PreMan fix tasks and fix the failing endpoint.
171
176
  ```
@@ -230,6 +235,7 @@ npm exec -y premanmcp@latest -- install # Cursor-only installer (legacy)
230
235
  ```bash
231
236
  preman hook install # Write the pre-push hook (connect does this too)
232
237
  preman hook status # Installed? And does the command in it still answer?
238
+ preman hook repair # Rewrite it if it stopped working (usually automatic)
233
239
  preman hook uninstall # Remove it, restoring any hook it replaced
234
240
  ```
235
241
 
@@ -242,17 +248,33 @@ than `@latest`, so upgrading us never changes what your pushes run; re-run `hook
242
248
  to move it. Set `PREMAN_HOOK_INVOCATION` to write a command of your own instead, and
243
249
  `PREMAN_SKIP_HOOK=1` to silence the hook for a push.
244
250
 
251
+ You should not have to run `repair`. A hook of ours that stops answering is rewritten in the
252
+ background by anything that proves PreMan runs here — any command, or your agent starting the
253
+ MCP server — because nothing re-runs `connect` after an upgrade to notice, and the broken
254
+ state is silent. It is narrow on purpose: a hook that still works keeps the version it pins, a
255
+ hook we did not write is never touched, and no hook is installed where you never asked for
256
+ one. The answer is remembered for an hour per directory, and `PREMAN_NO_HOOK_REPAIR=1` turns
257
+ it off.
258
+
245
259
  ### Runner
246
260
 
247
- `connect --guide` sets this up for you; these are for managing it afterwards.
261
+ `preman onboard` and `connect --guide` set this up for you; these are for managing it
262
+ afterwards.
248
263
 
249
264
  ```bash
250
265
  preman runner status # Paired? Running?
251
266
  preman runner start --background # Hold the job stream, log to ~/.preman/runner.log
252
267
  preman runner stop # Stop it and report offline
253
- preman runner register --agent claude-code # Re-pair (e.g. a different project)
268
+ preman runner register --agent claude-code # Pair without starting
254
269
  ```
255
270
 
271
+ `start` pairs this machine itself when it is not paired yet, and re-pairs it when the
272
+ backend has revoked the token it held, so neither is a command you have to be told to run.
273
+ It works out which agent to pair as from the session it is running in, then from the agent
274
+ PreMan is already configured in, then from the only one installed — and asks only when
275
+ those disagree. Pass `--agent cursor|claude-code|codex` to settle it yourself, which is
276
+ also what a machine with no terminal to ask in needs.
277
+
256
278
  The runner holds one outbound connection to PreMan and runs the work PreMan queues for
257
279
  this machine — a failing endpoint becomes an agent run in your own repo instead of a
258
280
  prompt you have to paste. It is bound to the agent and directory it was registered with
package/bin/api_tools.js CHANGED
@@ -8,7 +8,7 @@
8
8
 
9
9
  import { readFileSync } from "node:fs";
10
10
 
11
- import { callBackendJson, makeArgs, resolveApiKey } from "./shared.js";
11
+ import { callBackendJson, cliInvocation, makeArgs, resolveApiKey } from "./shared.js";
12
12
 
13
13
  export const ENDPOINTS_HELP = `
14
14
  Endpoints:
package/bin/cli.js CHANGED
@@ -23,6 +23,7 @@ import {
23
23
  connectCommand,
24
24
  discoverEndpoints,
25
25
  dispatchCommand,
26
+ resolveAgentForPairing,
26
27
  writeCursorConfig,
27
28
  } from "./connect.js";
28
29
  import {
@@ -34,7 +35,7 @@ import {
34
35
  } from "./integrations.js";
35
36
  import { HOSTED_HELP, linkCommand, runCommand, toolsCommand } from "./hosted.js";
36
37
  import { STATUS_HELP, statusCommand } from "./status.js";
37
- import { HOOK_HELP, hookCommand } from "./hook.js";
38
+ import { HOOK_HELP, hookCommand, scheduleHookRepair } from "./hook.js";
38
39
  import { RUNNER_HELP, runnerCommand } from "./runner.js";
39
40
  import { VERIFY_HELP, verifyCommand } from "./verify.js";
40
41
  import { DESKTOP_HELP, installDesktopCommand } from "./desktop.js";
@@ -135,6 +136,7 @@ async function loginCommand() {
135
136
  await loginBrowser(cliArgs);
136
137
  return;
137
138
  }
139
+ const cli = cliInvocation();
138
140
  const creds = await authenticateTerminal(cliArgs);
139
141
  process.stdout.write(`PreMan account ready.
140
142
 
@@ -153,6 +155,7 @@ You can now run:
153
155
  * the same write plus agent choice, pairing, and dispatch setup.
154
156
  */
155
157
  async function installCursorMcp() {
158
+ const cli = cliInvocation();
156
159
  const serverName = argValue("--name", "preman");
157
160
  const projectInstall = hasFlag("--project");
158
161
 
@@ -179,7 +182,7 @@ Server name: ${serverName}
179
182
  Backend: ${serverConfig.env.PREMAN_BACKEND}
180
183
 
181
184
  Next steps:
182
- 1. ${hasInlineKey ? "Your PreMan API key was written to the MCP config." : hasStoredKey ? `Your PreMan API key is saved in ${CREDENTIALS_FILE}; the MCP server will load it automatically.` : "Run ${cli} login to create/connect your account and generate an API key."}
185
+ 1. ${hasInlineKey ? "Your PreMan API key was written to the MCP config." : hasStoredKey ? `Your PreMan API key is saved in ${CREDENTIALS_FILE}; the MCP server will load it automatically.` : `Run ${cli} login to create/connect your account and generate an API key.`}
183
186
  2. Restart Cursor or toggle the PreMan MCP server off/on.
184
187
  3. In your API repo, ask your coding agent:
185
188
  "Use PreMan to convert the endpoints I choose into a hosted MCP server, then give me the Cursor/Claude install snippet."
@@ -216,6 +219,11 @@ function startServer() {
216
219
  }
217
220
 
218
221
  async function main() {
222
+ // Running at all is the signal. `hook` and `verify` own the hook themselves,
223
+ // and connect reinstalls it in the foreground, so a background rewrite there
224
+ // would only race with the one the user is watching.
225
+ if (!["hook", "verify", "connect"].includes(command)) scheduleHookRepair();
226
+
219
227
  if (command === "login") {
220
228
  await loginCommand();
221
229
  } else if (command === "status") {
@@ -225,7 +233,10 @@ async function main() {
225
233
  } else if (command === "hook") {
226
234
  await hookCommand(commandArgs);
227
235
  } else if (command === "runner") {
228
- await runnerCommand(commandArgs);
236
+ // Injected rather than imported inside runner.js, which connect.js already
237
+ // depends on: the agent picker lives there, and importing it back would
238
+ // close the cycle.
239
+ await runnerCommand(commandArgs, { resolveAgent: resolveAgentForPairing });
229
240
  } else if (command === "logout") {
230
241
  await logoutCommand();
231
242
  } else if (command === "doctor") {
package/bin/connect.js CHANGED
@@ -157,6 +157,52 @@ export function runningInside(env = process.env) {
157
157
  return "";
158
158
  }
159
159
 
160
+ /** Is PreMan's MCP server already written into this agent's config? */
161
+ function agentIsLinked(agentId, serverName = "preman") {
162
+ try {
163
+ const home = os.homedir();
164
+ if (agentId === "cursor") {
165
+ return Boolean(readJsonFile(path.join(home, ".cursor", "mcp.json")).mcpServers?.[serverName]);
166
+ }
167
+ if (agentId === "claude_code") {
168
+ return Boolean(readJsonFile(path.join(home, ".claude.json")).mcpServers?.[serverName]);
169
+ }
170
+ if (agentId === "codex") {
171
+ const file = path.join(process.env.CODEX_HOME || path.join(home, ".codex"), "config.toml");
172
+ return existsSync(file) && readFileSync(file, "utf8").includes(`[mcp_servers.${serverName}]`);
173
+ }
174
+ } catch {
175
+ // An unreadable config is not an answer; fall through to the other signals.
176
+ }
177
+ return false;
178
+ }
179
+
180
+ /**
181
+ * Which agent this machine should pair as, or "" when only a person can say.
182
+ *
183
+ * Pairing needs an agent id, and demanding `--agent` for it turned "start the
184
+ * runner" into a command someone had to be told twice. The signals are ordered
185
+ * by how much they actually know: the session we are inside, then the agent
186
+ * PreMan is already configured in, then the only one installed. Asking is the
187
+ * last resort rather than the first, and a machine with no terminal to ask in
188
+ * gets "" so the caller can fail with a sentence instead of hanging on a prompt.
189
+ */
190
+ export async function resolveAgentForPairing({ interactive = Boolean(process.stdin.isTTY) } = {}) {
191
+ const inside = runningInside();
192
+ if (inside) return inside;
193
+
194
+ const linked = AGENTS.filter((agent) => agentIsLinked(agent.id));
195
+ if (linked.length === 1) return linked[0].id;
196
+
197
+ const detected = detectAgents();
198
+ const present = AGENTS.filter((agent) => detected[agent.id]);
199
+ if (present.length === 1) return present[0].id;
200
+
201
+ if (!interactive) return "";
202
+ const picked = await promptAgentChoice(detected);
203
+ return picked?.id || "";
204
+ }
205
+
160
206
  async function promptAgentChoice(detected, insideId = runningInside()) {
161
207
  process.stdout.write("Which coding agent?\n");
162
208
  AGENTS.forEach((agent, index) => {
package/bin/hook.js CHANGED
@@ -8,9 +8,11 @@
8
8
  * by construction rather than by configuration.
9
9
  */
10
10
 
11
- import { spawnSync } from "node:child_process";
11
+ import { spawn, spawnSync } from "node:child_process";
12
12
  import { chmodSync, existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync } from "node:fs";
13
+ import os from "node:os";
13
14
  import path from "node:path";
15
+ import { fileURLToPath } from "node:url";
14
16
 
15
17
  import { cliInvocation, makeArgs, packageVersion } from "./shared.js";
16
18
  import { BLOCK_EXIT_CODE } from "./verify.js";
@@ -20,14 +22,23 @@ Hook options:
20
22
  install Write the pre-push hook into this repository
21
23
  uninstall Remove PreMan's pre-push hook
22
24
  status Report whether the hook is installed, and still works
25
+ repair Rewrite a hook of ours whose command no longer answers
23
26
  --force Overwrite a foreign pre-push hook (a backup is kept)
24
27
  PREMAN_HOOK_INVOCATION=<cmd> Use this command line instead of probing for one
28
+ PREMAN_NO_HOOK_REPAIR=1 Never repair a hook in the background
25
29
  `;
26
30
 
27
31
  const MARKER = "# >>> preman pre-push >>>";
28
32
  const END_MARKER = "# <<< preman pre-push <<<";
29
33
  const HOOK_TIMEOUT_SECONDS = 120;
30
34
  const PROBE_TIMEOUT_MS = 45000;
35
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
36
+ const HEALTH_FILE = path.join(os.homedir(), ".preman", "hook-health.json");
37
+ // How long a healthy answer is believed for. The repair costs a probe, and a
38
+ // hook that worked an hour ago almost always still works, so this is the knob
39
+ // that keeps a background check off the critical path of every command.
40
+ const RECHECK_MS = 60 * 60 * 1000;
41
+ const HEALTH_TTL_MS = 30 * 24 * 60 * 60 * 1000;
31
42
 
32
43
  function gitDir() {
33
44
  const result = spawnSync("git", ["rev-parse", "--git-dir"], { encoding: "utf8" });
@@ -259,6 +270,94 @@ export function hookStatus({ probe = false } = {}) {
259
270
  };
260
271
  }
261
272
 
273
+ function readHookChecks() {
274
+ try {
275
+ const parsed = JSON.parse(readFileSync(HEALTH_FILE, "utf8"));
276
+ return parsed && typeof parsed === "object" ? parsed : {};
277
+ } catch {
278
+ return {};
279
+ }
280
+ }
281
+
282
+ /** Remember what a check found, so the next command does not pay for it again. */
283
+ function rememberHookCheck(cwd, now, fields) {
284
+ const checks = readHookChecks();
285
+ for (const [key, value] of Object.entries(checks)) {
286
+ if (now - Number(value?.at || 0) > HEALTH_TTL_MS) delete checks[key];
287
+ }
288
+ checks[cwd] = { at: now, ...fields };
289
+ try {
290
+ mkdirSync(path.dirname(HEALTH_FILE), { recursive: true });
291
+ writeFileSync(HEALTH_FILE, JSON.stringify(checks, null, 2) + "\n", { mode: 0o600 });
292
+ } catch {
293
+ // A cache we cannot write costs a probe next time and nothing else.
294
+ }
295
+ return { cwd, ...fields };
296
+ }
297
+
298
+ /**
299
+ * Put a hook of ours back into working order, without being asked.
300
+ *
301
+ * The failure this exists for is invisible: a hook written by a version that
302
+ * embedded the wrong command keeps printing one skipped line per push, and
303
+ * nothing re-runs `connect` after an upgrade to notice. So anything that proves
304
+ * PreMan is running here -- any command, the MCP server starting -- is taken as
305
+ * the moment to check.
306
+ *
307
+ * Deliberately narrow. It only ever rewrites a hook we wrote that no longer
308
+ * answers: an absent hook is not installed behind the user, a foreign hook is
309
+ * not touched, and a hook that works is left on whatever version it pins.
310
+ */
311
+ export function repairDeadHook({ cwd = process.cwd(), now = Date.now(), force = false } = {}) {
312
+ const remember = (fields) => rememberHookCheck(cwd, now, fields);
313
+ if (!force) {
314
+ const last = readHookChecks()[cwd];
315
+ if (last && now - Number(last.at || 0) < RECHECK_MS) return { cwd, action: "recent" };
316
+ }
317
+
318
+ let status;
319
+ try {
320
+ status = hookStatus({ probe: true });
321
+ } catch {
322
+ return remember({ action: "not-a-repository" });
323
+ }
324
+ if (status.state !== "installed") return remember({ action: status.state });
325
+ if (status.works) return remember({ action: "healthy", invocation: status.invocation });
326
+
327
+ const result = installHook(makeArgs([]));
328
+ return remember({
329
+ action: result.action === "updated" ? "repaired" : result.action,
330
+ invocation: result.invocation || "",
331
+ replaced: status.invocation,
332
+ });
333
+ }
334
+
335
+ /**
336
+ * Run the repair in a detached child, because every step of it is synchronous
337
+ * and one of them shells out to npm. Nothing that merely wants a healthy hook
338
+ * should wait on a probe, least of all the MCP server, whose stdout is a
339
+ * protocol -- hence stdio ignored rather than inherited.
340
+ */
341
+ export function scheduleHookRepair({ cwd = process.cwd(), now = Date.now() } = {}) {
342
+ if (process.env.PREMAN_HOOK) return { action: "skipped", reason: "running inside the hook" };
343
+ if (process.env.PREMAN_NO_HOOK_REPAIR) return { action: "skipped", reason: "disabled" };
344
+ const last = readHookChecks()[cwd];
345
+ if (last && now - Number(last.at || 0) < RECHECK_MS) {
346
+ return { action: "skipped", reason: "checked recently" };
347
+ }
348
+ try {
349
+ const child = spawn(process.execPath, [path.join(__dirname, "cli.js"), "hook", "repair"], {
350
+ cwd,
351
+ detached: true,
352
+ stdio: "ignore",
353
+ });
354
+ child.unref();
355
+ return { action: "spawned", pid: child.pid };
356
+ } catch {
357
+ return { action: "skipped", reason: "could not spawn" };
358
+ }
359
+ }
360
+
262
361
  export async function hookCommand(commandArgs = []) {
263
362
  const sub = commandArgs.find((value) => !value.startsWith("-")) || "status";
264
363
  const args = makeArgs(commandArgs);
@@ -279,6 +378,22 @@ export async function hookCommand(commandArgs = []) {
279
378
  return result;
280
379
  }
281
380
 
381
+ if (sub === "repair") {
382
+ // Asked for by hand, "repair" means now: the hourly gate exists to keep this
383
+ // off the critical path of unrelated commands, not to refuse a person.
384
+ const result = repairDeadHook({ force: !args.has("--if-stale") });
385
+ const said = {
386
+ repaired: () => `Pre-push hook repaired: it now runs ${result.invocation}\n`,
387
+ healthy: () => "Pre-push hook already works here.\n",
388
+ recent: () => "Pre-push hook checked recently; nothing to do.\n",
389
+ absent: () => `No PreMan hook here. Install one: ${cliInvocation()} hook install\n`,
390
+ foreign: () => "The pre-push hook here is not ours; left alone.\n",
391
+ unproven: () => "Cannot repair: no way to run the PreMan CLI was found here.\n",
392
+ }[result.action];
393
+ process.stdout.write(said ? said() : `Pre-push hook: ${result.action}\n`);
394
+ return result;
395
+ }
396
+
282
397
  if (sub === "uninstall") {
283
398
  const result = uninstallHook();
284
399
  process.stdout.write(
package/bin/runner.js CHANGED
@@ -765,8 +765,9 @@ async function goOffline(args, state, log = defaultLog) {
765
765
 
766
766
  export const RUNNER_HELP = `
767
767
  Runner options:
768
- runner start [--background] Hold the job stream and run queued work here
769
- runner register [--agent a] Pair this machine as a runner (connect does this)
768
+ runner start [--background] Hold the job stream and run queued work here,
769
+ pairing this machine first if it is not paired
770
+ runner register [--agent a] Pair without starting (start does this for you)
770
771
  runner status Report whether the local runner is running
771
772
  runner stop Stop the background runner and go offline
772
773
  --agent <name> claude-code | cursor | codex
@@ -858,7 +859,9 @@ async function startForeground(args, commandArgs) {
858
859
  const agent = normalizeAgentId(args.value("--agent", "")) || "";
859
860
  if (!agent) {
860
861
  throw new RunnerError(
861
- `This machine is not paired as a runner yet. Run '${cliInvocation()} runner register --agent claude-code'.`,
862
+ "This machine is not paired as a runner yet, and which agent to pair as could not be" +
863
+ ` worked out here. Say so directly: '${cliInvocation()} runner start --background` +
864
+ " --agent cursor|claude-code|codex'.",
862
865
  2
863
866
  );
864
867
  }
@@ -915,9 +918,22 @@ async function startForeground(args, commandArgs) {
915
918
  }
916
919
  }
917
920
 
918
- export async function runnerCommand(commandArgs = []) {
921
+ export async function runnerCommand(commandArgs = [], { resolveAgent = null } = {}) {
919
922
  const sub = commandArgs.find((value) => !value.startsWith("-")) || "status";
920
- const args = makeArgs(commandArgs);
923
+ let args = makeArgs(commandArgs);
924
+
925
+ // Pairing needs an agent id, and the only place that can be asked for one is
926
+ // here: `--background` hands the work to a detached child with no terminal, so
927
+ // a question deferred to it is a question nobody ever sees.
928
+ if ((sub === "register" || sub === "start") && resolveAgent && !args.value("--agent", "")) {
929
+ if (sub === "register" || !readRunnerState()) {
930
+ const resolved = await resolveAgent();
931
+ if (resolved) {
932
+ commandArgs = [...commandArgs, "--agent", resolved];
933
+ args = makeArgs(commandArgs);
934
+ }
935
+ }
936
+ }
921
937
 
922
938
  if (sub === "register") {
923
939
  const agent = normalizeAgentId(args.value("--agent", ""));
@@ -966,7 +982,7 @@ export async function runnerCommand(commandArgs = []) {
966
982
  const alive = runnerIsAlive(pid);
967
983
  if (!state) {
968
984
  process.stdout.write(
969
- `Local runner: not paired\n Pair it: ${cliInvocation()} runner register --agent claude-code\n`
985
+ `Local runner: not paired\n Start it: ${cliInvocation()} runner start --background\n`
970
986
  );
971
987
  return { state: "unpaired" };
972
988
  }
package/dist/server.js CHANGED
@@ -3,10 +3,12 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
3
3
  import { z } from "zod";
4
4
  import { registerUserAuthFlowTools } from "./user_auth_flow.js";
5
5
  import { shareAuthFlowToUi } from "./auth_flow_ui.js";
6
+ import { spawn } from "node:child_process";
6
7
  import { existsSync, readFileSync } from "node:fs";
7
8
  import fs from "node:fs/promises";
8
9
  import path from "node:path";
9
10
  import os from "node:os";
11
+ import { fileURLToPath } from "node:url";
10
12
  import { randomUUID } from "node:crypto";
11
13
  import { MCP_PREVIEW_RESOURCE_URI, RESOURCE_URI_META_KEY, buildConversionPanelHtml, writeMcpPreviewFile, } from "./mcp-preview-panel.js";
12
14
  let REPO_CONFIG = null;
@@ -1444,11 +1446,34 @@ export function createServer() {
1444
1446
  return server;
1445
1447
  }
1446
1448
  // ── Start ─────────────────────────────────────────────────────────────
1449
+ /**
1450
+ * A pre-push hook PreMan wrote can stop working — the machine gained a `preman`
1451
+ * belonging to another package, or lost the one it pinned — and the only symptom
1452
+ * is one skipped line per push. This process starting is proof PreMan runs here,
1453
+ * so it is a good moment to check. Detached with stdio ignored: the check shells
1454
+ * out to npm, and this process's stdout is the MCP protocol.
1455
+ */
1456
+ function repairPushHookInBackground() {
1457
+ try {
1458
+ const cli = path.join(path.dirname(fileURLToPath(import.meta.url)), "..", "bin", "cli.js");
1459
+ if (!existsSync(cli))
1460
+ return;
1461
+ const child = spawn(process.execPath, [cli, "hook", "repair", "--if-stale"], {
1462
+ detached: true,
1463
+ stdio: "ignore",
1464
+ });
1465
+ child.unref();
1466
+ }
1467
+ catch {
1468
+ // Never worth failing a server start over.
1469
+ }
1470
+ }
1447
1471
  async function main() {
1448
1472
  await initAuth();
1449
1473
  const server = createServer();
1450
1474
  const transport = new StdioServerTransport();
1451
1475
  await server.connect(transport);
1476
+ repairPushHookInBackground();
1452
1477
  // Starting the configured MCP server is itself proof that the coding agent
1453
1478
  // is live. Previously the workbench remained in `connecting` until someone
1454
1479
  // explicitly asked the agent to call `preman_status`, even when this process
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "premanmcp",
3
- "version": "0.10.5",
3
+ "version": "0.10.6",
4
4
  "description": "Turn APIs into agent-callable MCP tools with auth, testing, and audit logs",
5
5
  "type": "module",
6
6
  "bin": {
@@ -15,7 +15,7 @@
15
15
  "open-mcp-preview-in-cursor": "node scripts/open-cursor-preview.mjs",
16
16
  "test": "npm run build && npm run test:connect && npm run test:node",
17
17
  "test:connect": "node scripts/smoke-connect.mjs",
18
- "test:node": "node --test --test-timeout=30000 scripts/smoke-launcher-config.mjs scripts/smoke-runner.mjs scripts/smoke-repo-config.mjs scripts/smoke-onboard.mjs scripts/smoke-local-detect.mjs scripts/smoke-prepush-hook.mjs scripts/smoke-cli-identity.mjs scripts/smoke-runner-heartbeat.mjs scripts/smoke-verify-prepush.mjs scripts/smoke-push-diff.mjs scripts/smoke-progress-reporter.mjs scripts/smoke-verify-plan.mjs scripts/smoke-install-desktop.mjs"
18
+ "test:node": "node --test --test-timeout=90000 scripts/smoke-launcher-config.mjs scripts/smoke-runner.mjs scripts/smoke-repo-config.mjs scripts/smoke-onboard.mjs scripts/smoke-local-detect.mjs scripts/smoke-prepush-hook.mjs scripts/smoke-cli-identity.mjs scripts/smoke-runner-heartbeat.mjs scripts/smoke-verify-prepush.mjs scripts/smoke-push-diff.mjs scripts/smoke-progress-reporter.mjs scripts/smoke-verify-plan.mjs scripts/smoke-install-desktop.mjs scripts/smoke-api-tools.mjs scripts/smoke-bin-scope.mjs"
19
19
  },
20
20
  "dependencies": {
21
21
  "@modelcontextprotocol/ext-apps": "^0.1.0",