premanmcp 1.1.4 → 1.1.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/bin/runner.js CHANGED
@@ -470,7 +470,10 @@ export async function runLeasedEval(args, state, job, { log = () => {}, headless
470
470
  surface: { ...surface, open: () => openUrl(surface.href, { app: surface.app }) },
471
471
  });
472
472
  if (result.reason === "lease_lost") log(`eval ${job.id} was taken over or cancelled`);
473
- else log(`eval ${job.id} finished: ${result.ok ? "ok" : "failed"}`);
473
+ else if (result.ok) log(`eval ${job.id} finished: ok`);
474
+ // The reason, on the line that says it failed. Without it this was the whole
475
+ // of what a terminal learned about a run that did not work.
476
+ else log(`eval ${job.id} finished: failed${result.error ? ` — ${result.error}` : ""}`);
474
477
  return result;
475
478
  }
476
479
 
package/bin/target.js ADDED
@@ -0,0 +1,505 @@
1
+ /**
2
+ * `preman target` — teach PreMan which agent to test, from the repo it lives in.
3
+ *
4
+ * The eval stack can measure an agent it does not own: a session renders
5
+ * `target: {kind: "endpoint", url}` against whatever address the device reports.
6
+ * What was missing is everything before that — the customer's agent is on their
7
+ * machine, behind a route only they know, speaking a shape nobody wrote down,
8
+ * and the CLI's own discovery only ever looked for *PreMan's* agent.
9
+ *
10
+ * So this is the step that turns "point the eval at a URL" into "PreMan found
11
+ * your agent". It probes what is running (`agent_dialects.js` decides how to
12
+ * talk to it) and writes the answer into the repository's `.preman.json`, which
13
+ * is where the pre-push hook already reads its team-level decisions from. In the
14
+ * repo rather than under `~/.preman` on purpose: the next person to clone this,
15
+ * and the CI job that will run these evals on every push, need the same answer
16
+ * without repeating the discovery.
17
+ *
18
+ * Nothing here starts a run or spends a provider bill. The worst outcome is a
19
+ * saved address that turns out to be wrong, and `preman target try` is the
20
+ * cheap way to find that out before anything is measured.
21
+ */
22
+
23
+ import { spawn } from "node:child_process";
24
+ import { existsSync, readFileSync, writeFileSync } from "node:fs";
25
+ import path from "node:path";
26
+
27
+ import { askAgent, dialectNamed, probeAgent, PROBE_MESSAGE } from "./agent_dialects.js";
28
+ import { detectCandidates } from "./detect.js";
29
+ import { cliInvocation } from "./shared.js";
30
+
31
+ export const TARGET_HELP = `
32
+ Agent under test:
33
+ target detect Find the agent running for this project and remember it
34
+ target try [message] Send one message through what was remembered
35
+ target show What this repository has saved
36
+ target forget Remove it
37
+ --url <base> Skip discovery; the agent is here
38
+ --path <path> Skip path discovery; the agent answers on this path
39
+ --port <n> Also try this port when discovering
40
+ --start Start the project first, using the command it names
41
+ --start-with <cmd> Start it with this command instead
42
+ `;
43
+
44
+ /** Committed to the repository, so the answer travels with the code. */
45
+ export const REPO_CONFIG = ".preman.json";
46
+
47
+ export function configPath(root = process.cwd()) {
48
+ return path.join(root, REPO_CONFIG);
49
+ }
50
+
51
+ /**
52
+ * The nearest directory at or above `from` that holds a `.preman.json`.
53
+ *
54
+ * Sold as "your team and CI find it", and until this it was found only by a
55
+ * command run from the repository root -- so an eval started from `services/api`
56
+ * discovered nothing and fell back to probing for PreMan's own agent. Stops at
57
+ * the filesystem root, and stops early at a `.git` directory, because a config
58
+ * above the repository belongs to a different project.
59
+ */
60
+ export function findConfigRoot(from = process.cwd()) {
61
+ let dir = path.resolve(from);
62
+ for (;;) {
63
+ if (existsSync(path.join(dir, REPO_CONFIG))) return dir;
64
+ if (existsSync(path.join(dir, ".git"))) return null;
65
+ const parent = path.dirname(dir);
66
+ if (parent === dir) return null;
67
+ dir = parent;
68
+ }
69
+ }
70
+
71
+ export function readConfig(root = process.cwd()) {
72
+ try {
73
+ const parsed = JSON.parse(readFileSync(configPath(root), "utf8"));
74
+ return parsed && typeof parsed === "object" ? parsed : {};
75
+ } catch {
76
+ // Absent or malformed reads the same way: nothing has been decided here.
77
+ return {};
78
+ }
79
+ }
80
+
81
+ /**
82
+ * Merge rather than overwrite, and keep the file readable.
83
+ *
84
+ * `.preman.json` already carries `blockOnBreaking`, a decision somebody made
85
+ * about their pushes. Rewriting the file as "the agent" would take that with it.
86
+ */
87
+ export function saveAgent(agent, root = process.cwd()) {
88
+ const config = readConfig(root);
89
+ const next = { ...config, agent };
90
+ writeFileSync(configPath(root), `${JSON.stringify(next, null, 2)}\n`, "utf8");
91
+ return next;
92
+ }
93
+
94
+ export function savedAgent(root = process.cwd()) {
95
+ const agent = readConfig(findConfigRoot(root) || root).agent;
96
+ if (!agent || typeof agent !== "object") return null;
97
+ const url = String(agent.url || "").trim();
98
+ if (!url) return null;
99
+ return {
100
+ url,
101
+ path: String(agent.path || ""),
102
+ dialect: String(agent.dialect || "message"),
103
+ start: String(agent.start || ""),
104
+ };
105
+ }
106
+
107
+ /**
108
+ * Where this project's agent might be listening.
109
+ *
110
+ * `detectCandidates` reads the things a project already says about itself --
111
+ * env files, package scripts, compose, Procfile, framework defaults -- and it
112
+ * was written for finding an API. An agent is served by the same kinds of
113
+ * process, so the list is the same list; what differs is the question asked of
114
+ * each address, which is `probeAgent`'s job rather than this one's.
115
+ */
116
+ export function candidateUrls(root = process.cwd(), { extraPorts = [] } = {}) {
117
+ const seen = new Set();
118
+ const urls = [];
119
+ const add = (url, why) => {
120
+ const clean = String(url || "").replace(/\/+$/, "");
121
+ if (!clean || seen.has(clean)) return;
122
+ seen.add(clean);
123
+ urls.push({ url: clean, why });
124
+ };
125
+
126
+ for (const candidate of detectCandidates(root, { extraPorts })) {
127
+ // `signals` is what a candidate carries -- ".env:PORT", "package.json:dev".
128
+ // Reading a `detail` it has never had printed the raw enum instead, so
129
+ // every address said "env_file" whatever had actually named it.
130
+ add(candidate.url, candidate.signals?.[0] || candidate.source || "");
131
+ }
132
+ // Then the ports things listen on anyway. A project that says nothing about
133
+ // itself is the common case, not the edge one -- `node agent.mjs` names no
134
+ // port, and a repo with no compose file, no Procfile and no framework in its
135
+ // dependencies leaves detection with nothing to read. Guessing is cheap here
136
+ // because a port with nothing behind it refuses the connection immediately.
137
+ for (const port of COMMON_PORTS) add(`http://127.0.0.1:${port}`, `port ${port}`);
138
+ return urls;
139
+ }
140
+
141
+ /**
142
+ * Where an agent ends up when nobody decided.
143
+ *
144
+ * Framework defaults first (Next and Vite on 3000 and 5173, FastAPI and Django
145
+ * on 8000), then the ports people reach for when writing a server by hand.
146
+ */
147
+ export const COMMON_PORTS = [3000, 8000, 8080, 5000, 5173, 4000, 8001, 9000, 7860];
148
+
149
+ /**
150
+ * How long a project gets to come up before this gives up on it.
151
+ *
152
+ * A minute because the first run of a dev server is the slow one -- installing,
153
+ * compiling, warming a model client -- and a developer who has just been told
154
+ * PreMan will start their agent will wait through it once. It polls every
155
+ * second rather than sleeping the whole minute, so a fast project costs a
156
+ * second and not a minute.
157
+ */
158
+ export const WAIT_FOR_START_MS = 60_000;
159
+
160
+ /** How long a project gets to stop politely before it is killed. */
161
+ export const STOP_GRACE_MS = 5_000;
162
+
163
+ /**
164
+ * How this project starts itself.
165
+ *
166
+ * Read from the files a project already uses to say so, in the order of how
167
+ * much of a decision each one is: a command somebody saved here beats a script
168
+ * in `package.json`, which beats a Procfile's web process, which beats a guess
169
+ * from whichever framework's files are lying around.
170
+ *
171
+ * Guesses are included and labelled as guesses, because the alternative for a
172
+ * developer whose repo says nothing is to type the command themselves -- which
173
+ * is fine, and is what `--start "<cmd>"` is for, but should not be the first
174
+ * thing asked of somebody who ran `preman target detect` expecting it to work.
175
+ */
176
+ export function startCommands(root = process.cwd()) {
177
+ const found = [];
178
+ const add = (command, why) => {
179
+ const clean = String(command || "").trim();
180
+ if (clean && !found.some((entry) => entry.command === clean)) found.push({ command: clean, why });
181
+ };
182
+
183
+ const saved = savedAgent(root);
184
+ if (saved?.start) add(saved.start, `${REPO_CONFIG} remembers it`);
185
+
186
+ const pkg = readJson(path.join(root, "package.json"));
187
+ for (const name of ["dev", "start", "serve", "dev:server", "server"]) {
188
+ if (pkg?.scripts?.[name]) add(`npm run ${name}`, `package.json has a ${name} script`);
189
+ }
190
+
191
+ const procfile = readText(path.join(root, "Procfile"));
192
+ const web = /^web:\s*(.+)$/m.exec(procfile);
193
+ if (web) add(web[1], "the Procfile's web process");
194
+
195
+ for (const name of ["docker-compose.yml", "docker-compose.yaml", "compose.yml", "compose.yaml"]) {
196
+ if (existsSync(path.join(root, name))) {
197
+ add("docker compose up", `${name} is here`);
198
+ break;
199
+ }
200
+ }
201
+
202
+ // Python projects rarely write the command down anywhere a machine can read,
203
+ // so these are read off the files that exist rather than off configuration.
204
+ if (existsSync(path.join(root, "manage.py"))) add("python manage.py runserver", "a guess, from manage.py");
205
+ if (existsSync(path.join(root, "main.py"))) add("python main.py", "a guess, from main.py");
206
+ if (existsSync(path.join(root, "app.py"))) add("python app.py", "a guess, from app.py");
207
+
208
+ return found;
209
+ }
210
+
211
+ function readJson(file) {
212
+ try {
213
+ return JSON.parse(readFileSync(file, "utf8"));
214
+ } catch {
215
+ return null;
216
+ }
217
+ }
218
+
219
+ function readText(file) {
220
+ try {
221
+ return readFileSync(file, "utf8");
222
+ } catch {
223
+ return "";
224
+ }
225
+ }
226
+
227
+ /**
228
+ * Run the project, wait for its agent to answer, and hand back a way to stop it.
229
+ *
230
+ * Started in its own process group and stopped by group, because the command is
231
+ * nearly always a wrapper -- `npm run dev` is npm, which is a shell, which is
232
+ * the server -- and killing the pid we spawned leaves the server holding the
233
+ * port. A developer whose next run says "something is already on 3000" would
234
+ * have PreMan to thank for it.
235
+ *
236
+ * Output is captured rather than inherited: a dev server's log is not this
237
+ * command's output, and interleaving it with discovery makes both unreadable.
238
+ * It is kept, and printed only when the wait fails, which is the one moment it
239
+ * is the most useful thing on the screen.
240
+ */
241
+ export async function startProject(command, { root = process.cwd(), waitMs = 60_000, log = () => {} } = {}) {
242
+ const child = spawn(command, {
243
+ cwd: root,
244
+ shell: true,
245
+ detached: true,
246
+ stdio: ["ignore", "pipe", "pipe"],
247
+ });
248
+
249
+ let output = "";
250
+ const keep = (chunk) => {
251
+ if (output.length < 8000) output += chunk.toString("utf8");
252
+ };
253
+ child.stdout.on("data", keep);
254
+ child.stderr.on("data", keep);
255
+
256
+ let exited = null;
257
+ child.on("exit", (code, signal) => {
258
+ exited = signal || code;
259
+ });
260
+
261
+ const signal = (name) => {
262
+ try {
263
+ // Negative pid: the group, not just the wrapper.
264
+ process.kill(-child.pid, name);
265
+ } catch {
266
+ try {
267
+ child.kill(name);
268
+ } catch {
269
+ // Already gone, which is the state being asked for.
270
+ }
271
+ }
272
+ };
273
+
274
+ const stop = async () => {
275
+ if (exited !== null) return;
276
+ signal("SIGTERM");
277
+ // Then SIGKILL, and the pipes closed behind it. A dev server that ignores
278
+ // SIGTERM keeps this process alive forever otherwise: its stdout is still
279
+ // open here, so Node has a reason to stay, and `preman target detect`
280
+ // simply never returns.
281
+ const deadline = Date.now() + STOP_GRACE_MS;
282
+ while (exited === null && Date.now() < deadline) {
283
+ await new Promise((resume) => setTimeout(resume, 100));
284
+ }
285
+ if (exited === null) {
286
+ log(`${command.split(" ")[0]} did not stop when asked; killing it`);
287
+ signal("SIGKILL");
288
+ }
289
+ child.stdout?.destroy();
290
+ child.stderr?.destroy();
291
+ };
292
+
293
+ return { child, stop, output: () => output, exited: () => exited };
294
+ }
295
+
296
+ /** A base address and the path on it, from whichever form somebody typed. */
297
+ export function splitUrl(value) {
298
+ const raw = String(value || "").trim();
299
+ if (!raw) return { base: "", path: "" };
300
+ // `localhost:3000` is a URL to a person and a `localhost:` scheme to the
301
+ // parser, which turns the port into the path. Anything that is not http(s)
302
+ // is treated as the host somebody meant.
303
+ const candidate = /^https?:\/\//i.test(raw) ? raw : `http://${raw}`;
304
+ try {
305
+ const url = new URL(candidate);
306
+ const path = url.pathname === "/" ? "" : url.pathname.replace(/\/+$/, "");
307
+ return { base: `${url.protocol}//${url.host}`, path };
308
+ } catch {
309
+ return { base: raw.replace(/\/+$/, ""), path: "" };
310
+ }
311
+ }
312
+
313
+ /** Try each candidate until one answers like an agent. */
314
+ export async function findAgent(root, { url = "", paths = null, extraPorts = [], log = () => {} } = {}) {
315
+ const candidates = url
316
+ ? [{ url: String(url).replace(/\/+$/, ""), why: "you named it" }]
317
+ : candidateUrls(root, { extraPorts });
318
+
319
+ if (!candidates.length) return { found: null, tried: [] };
320
+
321
+ const tried = [];
322
+ for (const candidate of candidates) {
323
+ log(`looking at ${candidate.url}${candidate.why ? ` (${candidate.why})` : ""}`);
324
+ const result = await probeAgent(candidate.url, paths ? { paths } : {});
325
+ if (result.dialect) return { found: result, tried };
326
+ tried.push({ url: candidate.url, why: summarise(result.tried) });
327
+ }
328
+ return { found: null, tried };
329
+ }
330
+
331
+ /** The one sentence a failed sweep can honestly offer about an address. */
332
+ function summarise(attempts = []) {
333
+ if (!attempts.length) return "nothing answered";
334
+ const statuses = attempts.filter((a) => /^\d{3}/.test(a.why || ""));
335
+ if (statuses.length === attempts.length) return "answered, but not like an agent";
336
+ const first = attempts.find((a) => a.why && !/^\d{3}/.test(a.why));
337
+ return first?.why || "nothing answered";
338
+ }
339
+
340
+ export async function targetCommand(commandArgs = [], { makeArgs, root = process.cwd() } = {}) {
341
+ const args = makeArgs(commandArgs);
342
+ const sub = commandArgs.find((value) => !value.startsWith("-")) || "detect";
343
+ const say = (line = "") => process.stdout.write(`${line}\n`);
344
+ const cli = cliInvocation();
345
+
346
+ if (sub === "show") {
347
+ const agent = savedAgent(root);
348
+ if (!agent) {
349
+ say(`Nothing saved in ${REPO_CONFIG}. Run \`${cli} target detect\`.`);
350
+ return { ok: false, reason: "nothing_saved" };
351
+ }
352
+ say(`${agent.url}${agent.path}`);
353
+ say(` speaks ${agent.dialect}`);
354
+ if (agent.start) say(` started with ${agent.start}`);
355
+ return { ok: true, agent };
356
+ }
357
+
358
+ if (sub === "forget") {
359
+ const config = readConfig(root);
360
+ if (!config.agent) {
361
+ say(`Nothing saved in ${REPO_CONFIG}.`);
362
+ return { ok: true, forgot: false };
363
+ }
364
+ delete config.agent;
365
+ writeFileSync(configPath(root), `${JSON.stringify(config, null, 2)}\n`, "utf8");
366
+ say(`Forgotten. ${REPO_CONFIG} no longer names an agent.`);
367
+ return { ok: true, forgot: true };
368
+ }
369
+
370
+ if (sub === "try") {
371
+ const agent = savedAgent(root);
372
+ if (!agent) {
373
+ say(`Nothing saved to try. Run \`${cli} target detect\` first.`);
374
+ return { ok: false, reason: "nothing_saved" };
375
+ }
376
+ const message = commandArgs.filter((value) => value !== "try" && !value.startsWith("-")).join(" ") || PROBE_MESSAGE;
377
+ say(`> ${message}`);
378
+ try {
379
+ const { text, events } = await askAgent(agent, message);
380
+ say("");
381
+ say(text);
382
+ if (events.length) {
383
+ say("");
384
+ say(`tools it used: ${events.map((event) => event.tool_name).join(", ")}`);
385
+ } else {
386
+ // Said plainly rather than left blank: a run against an agent whose
387
+ // tool calls are invisible measures what it said and not what it did,
388
+ // and that is a different product promise.
389
+ say("");
390
+ say("no tool calls in the answer, so an eval will score what it says, not what it does");
391
+ }
392
+ return { ok: true, text, events };
393
+ } catch (error) {
394
+ say(`could not get an answer: ${error.message}`);
395
+ return { ok: false, reason: "no_answer" };
396
+ }
397
+ }
398
+
399
+ if (sub !== "detect") {
400
+ say(`Unknown target subcommand: ${sub}${TARGET_HELP}`);
401
+ return { ok: false, reason: "unknown_subcommand" };
402
+ }
403
+
404
+ // `--url http://localhost:3000/api/chat` is what people type, and it is the
405
+ // same thing as `--url http://localhost:3000 --path /api/chat`. Splitting it
406
+ // here rather than appending to it is the difference between probing the
407
+ // address they named and probing `/api/chat/chat`.
408
+ const named = splitUrl(args.value("--url", ""));
409
+ const explicit = named.base;
410
+ const explicitPath = args.value("--path", "") || named.path;
411
+ const port = args.value("--port", "");
412
+ say("Looking for the agent this project runs...");
413
+
414
+ let { found, tried } = await findAgent(root, {
415
+ url: explicit,
416
+ paths: explicitPath ? [explicitPath] : null,
417
+ extraPorts: port ? [port] : [],
418
+ log: (line) => say(` ${line}`),
419
+ });
420
+
421
+ // Nothing running, and permission to do something about it. Starting is
422
+ // opt-in and never inferred: this runs a command out of somebody's repository,
423
+ // which is the one thing in this CLI that executes code nobody typed here.
424
+ const startWith = args.value("--start-with", "");
425
+ const mayStart = startWith || args.has("--start");
426
+ let started = null;
427
+ let startCommand = "";
428
+
429
+ if (!found && mayStart) {
430
+ const commands = startCommands(root);
431
+ startCommand = startWith || commands[0]?.command || "";
432
+ if (!startCommand) {
433
+ say("");
434
+ say("Nothing here says how this project starts.");
435
+ say(` ${cli} target detect --start-with "npm run dev"`);
436
+ return { ok: false, reason: "no_start_command" };
437
+ }
438
+ const why = startWith ? "you named it" : commands[0].why;
439
+ say("");
440
+ say(`Starting the project: ${startCommand} (${why})`);
441
+ started = await startProject(startCommand, { root });
442
+
443
+ const deadline = Date.now() + WAIT_FOR_START_MS;
444
+ while (Date.now() < deadline && started.exited() === null) {
445
+ await new Promise((resume) => setTimeout(resume, 1000));
446
+ const retry = await findAgent(root, {
447
+ url: explicit,
448
+ paths: explicitPath ? [explicitPath] : null,
449
+ extraPorts: port ? [port] : [],
450
+ });
451
+ if (retry.found) {
452
+ found = retry.found;
453
+ break;
454
+ }
455
+ }
456
+
457
+ await started.stop();
458
+ if (!found) {
459
+ say("");
460
+ say(
461
+ started.exited() === null
462
+ ? `It started, but nothing answered like an agent within ${Math.round(WAIT_FOR_START_MS / 1000)}s.`
463
+ : `That command exited (${started.exited()}) before anything answered.`
464
+ );
465
+ const log = started.output().trim();
466
+ // The log is the useful half of this failure and is otherwise thrown
467
+ // away: a missing env var says so in line three of a dev server's output.
468
+ if (log) {
469
+ say("");
470
+ say(log.split("\n").slice(-12).join("\n"));
471
+ }
472
+ return { ok: false, reason: "start_failed" };
473
+ }
474
+ }
475
+
476
+ if (!found) {
477
+ say("");
478
+ say("No agent answered here.");
479
+ for (const attempt of tried) say(` ${attempt.url} — ${attempt.why}`);
480
+ say("");
481
+ say("Start it yourself and run this again, or let PreMan start it:");
482
+ say(` ${cli} target detect --start`);
483
+ say(` ${cli} target detect --url http://localhost:3000 --path /api/chat`);
484
+ return { ok: false, reason: "not_found", tried };
485
+ }
486
+
487
+ const agent = { url: found.url, path: found.path, dialect: found.dialect };
488
+ if (startCommand) agent.start = startCommand;
489
+ saveAgent(agent, root);
490
+
491
+ say("");
492
+ say(`Found it: ${agent.url}${agent.path}`);
493
+ say(` it speaks ${dialectNamed(agent.dialect)?.summary || agent.dialect}`);
494
+ say(` it said: ${found.sample}`);
495
+ if (found.events?.length) {
496
+ say(` and it reported using ${found.events.map((event) => event.tool_name).join(", ")}`);
497
+ }
498
+ say("");
499
+ if (agent.start) say(` it starts with ${agent.start}`);
500
+ say("");
501
+ say(`Saved to ${REPO_CONFIG}, so your team and CI find it without repeating this.`);
502
+ say(` ${cli} target try "I want a refund" to send it something`);
503
+ say(` ${cli} eval to measure it`);
504
+ return { ok: true, agent };
505
+ }
package/dist/server.js CHANGED
@@ -8,6 +8,12 @@ import { createInterface } from "node:readline";
8
8
  import { readFile } from "node:fs/promises";
9
9
  import os from "node:os";
10
10
  import path from "node:path";
11
+ // The stdio bridge is the process an MCP host launches and nobody started on
12
+ // purpose, so it is the one most likely to be found later as an unexplained
13
+ // `node` in a process list. Named apart from the CLI and the session, because
14
+ // "which of these three is the one my editor spawned" is the question being
15
+ // asked when somebody goes looking.
16
+ process.title = "preman-mcp";
11
17
  let sessionId = "";
12
18
  let protocolVersion = "2025-06-18";
13
19
  async function credentials() {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "premanmcp",
3
- "version": "1.1.4",
3
+ "version": "1.1.6",
4
4
  "description": "PreMan CLI and stdio proxy for PreMan's hosted MCP server",
5
5
  "type": "module",
6
6
  "bin": {
@@ -15,7 +15,7 @@
15
15
  "test": "npm run build && npm run test:proxy",
16
16
  "test:proxy": "node --test scripts/smoke-proxy.mjs",
17
17
  "test:connect": "node scripts/smoke-connect.mjs",
18
- "test:node": "node --test --test-timeout=90000 scripts/smoke-account.mjs scripts/smoke-cli-entrypoint.mjs scripts/smoke-launcher-config.mjs scripts/smoke-runner.mjs scripts/smoke-eval.mjs scripts/smoke-repo-config.mjs scripts/smoke-onboard.mjs scripts/smoke-onboard-opening.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-desktop-session.mjs scripts/smoke-api-tools.mjs scripts/smoke-tests-workbench.mjs scripts/smoke-bin-scope.mjs scripts/smoke-shared-errors.mjs",
18
+ "test:node": "node --test --test-timeout=90000 scripts/smoke-account.mjs scripts/smoke-agent-session.mjs scripts/smoke-agent-dock.mjs scripts/smoke-terminal-lifecycle.mjs scripts/smoke-cli-entrypoint.mjs scripts/smoke-cli-update.mjs scripts/smoke-launcher-config.mjs scripts/smoke-runner.mjs scripts/smoke-eval.mjs scripts/smoke-eval-behaviors.mjs scripts/smoke-repo-config.mjs scripts/smoke-onboard.mjs scripts/smoke-onboard-opening.mjs scripts/smoke-local-detect.mjs scripts/smoke-agent-target.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-desktop-session.mjs scripts/smoke-api-tools.mjs scripts/smoke-tests-workbench.mjs scripts/smoke-bin-scope.mjs scripts/smoke-shared-errors.mjs scripts/smoke-process-title.mjs scripts/smoke-predict.mjs",
19
19
  "test:dmg": "node --test --test-timeout=300000 scripts/smoke-install-desktop-volume.mjs"
20
20
  },
21
21
  "devDependencies": {