@loompad/cli 1.2.1 → 1.2.2

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.
Files changed (73) hide show
  1. package/CHANGELOG.md +36 -0
  2. package/dist/adapters/opencode.d.ts +2 -0
  3. package/dist/adapters/opencode.js +9 -2
  4. package/dist/adapters/opencode.js.map +1 -1
  5. package/dist/cli/doctor.d.ts +1 -1
  6. package/dist/cli/doctor.js +7 -2
  7. package/dist/cli/doctor.js.map +1 -1
  8. package/dist/cli/index.js +80 -13
  9. package/dist/cli/index.js.map +1 -1
  10. package/dist/cli/ui.js +2 -1
  11. package/dist/cli/ui.js.map +1 -1
  12. package/dist/core/brain-index.d.ts +0 -18
  13. package/dist/core/brain-index.js +10 -2
  14. package/dist/core/brain-index.js.map +1 -1
  15. package/dist/core/crew.d.ts +10 -0
  16. package/dist/core/crew.js +40 -3
  17. package/dist/core/crew.js.map +1 -1
  18. package/dist/core/prices.d.ts +16 -0
  19. package/dist/core/prices.js +29 -1
  20. package/dist/core/prices.js.map +1 -1
  21. package/dist/core/registry.d.ts +7 -0
  22. package/dist/core/registry.js.map +1 -1
  23. package/dist/core/replay.js +2 -0
  24. package/dist/core/replay.js.map +1 -1
  25. package/dist/daemon/auth.d.ts +7 -1
  26. package/dist/daemon/auth.js +19 -3
  27. package/dist/daemon/auth.js.map +1 -1
  28. package/dist/daemon/client.d.ts +15 -0
  29. package/dist/daemon/client.js +31 -0
  30. package/dist/daemon/client.js.map +1 -1
  31. package/dist/daemon/council.d.ts +8 -0
  32. package/dist/daemon/council.js +115 -36
  33. package/dist/daemon/council.js.map +1 -1
  34. package/dist/daemon/review.d.ts +4 -0
  35. package/dist/daemon/review.js +22 -2
  36. package/dist/daemon/review.js.map +1 -1
  37. package/dist/daemon/routes/brain.js +4 -0
  38. package/dist/daemon/routes/brain.js.map +1 -1
  39. package/dist/daemon/routes/council.js +9 -0
  40. package/dist/daemon/routes/council.js.map +1 -1
  41. package/dist/daemon/routes/crews.js +2 -0
  42. package/dist/daemon/routes/crews.js.map +1 -1
  43. package/dist/daemon/routes/pairing.js +6 -1
  44. package/dist/daemon/routes/pairing.js.map +1 -1
  45. package/dist/daemon/routes/team.js +2 -0
  46. package/dist/daemon/routes/team.js.map +1 -1
  47. package/dist/daemon/runtime/accounting.js +7 -3
  48. package/dist/daemon/runtime/accounting.js.map +1 -1
  49. package/dist/daemon/runtime/queue.d.ts +2 -0
  50. package/dist/daemon/runtime/queue.js +14 -0
  51. package/dist/daemon/runtime/queue.js.map +1 -1
  52. package/dist/daemon/runtime/turns.d.ts +2 -0
  53. package/dist/daemon/runtime/turns.js +28 -0
  54. package/dist/daemon/runtime/turns.js.map +1 -1
  55. package/dist/daemon/runtime-support.d.ts +5 -1
  56. package/dist/daemon/runtime-support.js +8 -2
  57. package/dist/daemon/runtime-support.js.map +1 -1
  58. package/dist/daemon/runtime.d.ts +21 -0
  59. package/dist/daemon/runtime.js +94 -6
  60. package/dist/daemon/runtime.js.map +1 -1
  61. package/dist/daemon/server.js +4 -0
  62. package/dist/daemon/server.js.map +1 -1
  63. package/dist/daemon/shift.d.ts +6 -0
  64. package/dist/daemon/shift.js +10 -1
  65. package/dist/daemon/shift.js.map +1 -1
  66. package/dist/daemon/team.d.ts +6 -0
  67. package/dist/daemon/team.js +15 -0
  68. package/dist/daemon/team.js.map +1 -1
  69. package/dist/web/app.css +65 -0
  70. package/dist/web/app.js +11783 -11607
  71. package/dist/web/app.js.map +4 -4
  72. package/docs/release-notes/v1.2.2.md +39 -0
  73. package/package.json +1 -1
package/dist/cli/index.js CHANGED
@@ -27,7 +27,7 @@ import qrcode from "qrcode-terminal";
27
27
  }
28
28
  }
29
29
  import { exitCodeFor, turnOutcome } from "../core/turn-wait.js";
30
- import { DaemonClient, DaemonError, daemonRunning, ensureDaemon, stopDaemon, } from "../daemon/client.js";
30
+ import { DaemonClient, DaemonError, daemonRunning, desktopOwnsDaemon, ensureDaemon, stopDaemon, } from "../daemon/client.js";
31
31
  import { installCrashGuards } from "../daemon/guards.js";
32
32
  import { LoomDaemon, DEFAULT_PORT } from "../daemon/server.js";
33
33
  import { ensureLoomHome, findProject, loomHome, readDaemonConfig } from "../core/registry.js";
@@ -200,8 +200,14 @@ program
200
200
  .description("start the loom daemon in the background")
201
201
  .option("--tailnet", "bind to this machine's Tailscale IP", false)
202
202
  .option("--restart", "restart even if a daemon is already running", false)
203
+ .option("--force", "with --restart: replace even Loom Desktop's daemon with this CLI's")
203
204
  .action(async (opts) => {
204
205
  const running = await daemonRunning();
206
+ if (running && !opts.force && (await desktopOwnsDaemon(running))) {
207
+ // replacing it swapped Desktop onto the CLI's build without a word (#306)
208
+ console.log(pc.yellow("Loom Desktop runs this daemon — quit and reopen Loom Desktop to restart it (or loom up --restart --force to replace it with this CLI's)"));
209
+ return;
210
+ }
205
211
  if (running) {
206
212
  const { BUILD_REV } = await import("../daemon/server.js");
207
213
  const health = (await fetch(`http://${running.host}:${running.port}/api/health`)
@@ -251,15 +257,21 @@ program
251
257
  program
252
258
  .command("status")
253
259
  .description("daemon health and project board")
254
- .action(async () => {
260
+ .option("--json", "print the projects as JSON")
261
+ .action(async (opts) => {
255
262
  const cfg = await daemonRunning();
256
263
  if (!cfg) {
264
+ if (opts.json)
265
+ return void console.log(JSON.stringify({ daemon: null, projects: [] }));
257
266
  console.log(pc.dim("daemon: not running (loom up)"));
258
267
  return;
259
268
  }
260
- console.log(pc.green(`daemon: http://${cfg.host}:${cfg.port}`));
269
+ if (!opts.json)
270
+ console.log(pc.green(`daemon: http://${cfg.host}:${cfg.port}`));
261
271
  const client = new DaemonClient(cfg);
262
272
  const { projects } = await client.listProjects();
273
+ if (opts.json)
274
+ return void console.log(JSON.stringify(projects)); // #315
263
275
  if (!projects.length)
264
276
  console.log(pc.dim("no projects yet — loom init"));
265
277
  for (const p of projects)
@@ -290,6 +302,7 @@ program
290
302
  .command("projects")
291
303
  .description("board of all projects")
292
304
  .option("--forget <idOrName>", "stop tracking a project (its .loom/ stays on disk)")
305
+ .option("--json", "print the projects as JSON")
293
306
  .action(async (opts) => {
294
307
  const client = await ensureDaemon();
295
308
  if (opts.forget) {
@@ -309,6 +322,8 @@ program
309
322
  return;
310
323
  }
311
324
  const { projects } = await client.listProjects();
325
+ if (opts.json)
326
+ return void console.log(JSON.stringify(projects)); // #315
312
327
  if (!projects.length)
313
328
  console.log(pc.dim("no projects yet — loom init"));
314
329
  for (const p of projects)
@@ -1114,7 +1129,8 @@ program
1114
1129
  return pc.dim(`${t} ${e.kind}`);
1115
1130
  }
1116
1131
  };
1117
- console.log(pc.dim(project
1132
+ // the banner is for people: stderr, so --json's stdout is events and nothing else (#314)
1133
+ console.error(pc.dim(project
1118
1134
  ? `watching ${project.name} — ^C to stop`
1119
1135
  : "watching every project — ^C to stop"));
1120
1136
  const close = client.subscribe((pid, e) => {
@@ -1203,6 +1219,9 @@ program
1203
1219
  }
1204
1220
  return fail(err instanceof Error ? err.message : String(err));
1205
1221
  }
1222
+ const shaky = review.unreliable ?? [];
1223
+ if (!opts.json && shaky.length)
1224
+ console.error(pc.yellow(`⚠ ${shaky.join(", ")} hasn't finished a review in this project yet (left out each time) — it may wait out review.timeoutMin`));
1206
1225
  if (!opts.json)
1207
1226
  console.error(pc.cyan(`🔍 ${agents.join(", ")} reviewing ${review.target.kind === "file" ? review.target.file : review.target.kind === "diff" ? "the uncommitted changes" : `“${review.target.label ?? "your request"}”`} · read-only, nobody takes the baton`));
1208
1227
  if (!opts.wait)
@@ -1257,8 +1276,10 @@ program
1257
1276
  await new Promise((r) => setTimeout(r, 1500));
1258
1277
  }
1259
1278
  console.log((council.status === "done" ? pc.green : council.status === "stopped" ? pc.yellow : pc.red)(`\n${council.summary ?? council.status}`));
1260
- if (council.status === "failed")
1279
+ if (council.status === "failed") {
1261
1280
  process.exitCode = 1;
1281
+ console.log(pc.dim(`carry on from where it stopped: loom council:resume ${council.councilId}`));
1282
+ }
1262
1283
  });
1263
1284
  program
1264
1285
  .command("council:status [councilId]")
@@ -1275,6 +1296,21 @@ program
1275
1296
  if (council.summary)
1276
1297
  console.log(pc.dim(`\n${council.summary}`));
1277
1298
  });
1299
+ program
1300
+ .command("council:resume <councilId>")
1301
+ .description("carry on a council that a restart, a failed turn or Stop ended — from the first stage that didn't finish")
1302
+ .action(async (councilId) => {
1303
+ const client = await ensureDaemon();
1304
+ const project = await currentProject(client);
1305
+ try {
1306
+ const { council } = await client.resumeCouncil(project.id, councilId);
1307
+ const next = council.stages.find((st) => !["done", "skipped", "unverified"].includes(st.status) && !(st.key === "prove" && st.status === "failed"));
1308
+ console.log(pc.cyan(`🏛 resuming council ${council.councilId} from ${next?.name ?? "the end"} — loom council:status ${council.councilId}`));
1309
+ }
1310
+ catch (err) {
1311
+ fail(err instanceof Error ? err.message : String(err));
1312
+ }
1313
+ });
1278
1314
  program
1279
1315
  .command("council:stop")
1280
1316
  .description("stop the running council after its current stage")
@@ -1389,6 +1425,8 @@ program
1389
1425
  });
1390
1426
  // ── orchestra: one orchestrator, many parallel workers (core/orchestra.ts) ──
1391
1427
  const RUN_DONE = new Set(["completed", "failed", "aborted", "waiting_human"]);
1428
+ /** A run a restart cut off is "aborted" on disk but resumable: say so, and how (#305). */
1429
+ const runStatus = (r) => r.status === "aborted" && r.interrupted ? "interrupted (resumable)" : r.status;
1392
1430
  const TASK_COLOR = {
1393
1431
  done: pc.green,
1394
1432
  running: pc.cyan,
@@ -1401,8 +1439,8 @@ const TASK_COLOR = {
1401
1439
  function printRun(run) {
1402
1440
  const done = run.tasks.filter((t) => t.status === "done").length;
1403
1441
  console.log(run.race
1404
- ? `${pc.magenta("🏁")} ${pc.bold(run.id)} ${pc.dim(run.status)} race · ${done}/${run.tasks.length} finished · ${fmtUsd(run.costUsd)}`
1405
- : `${pc.magenta("🎼")} ${pc.bold(run.id)} ${pc.dim(run.status)} ${done}/${run.tasks.length} tasks · ` +
1442
+ ? `${pc.magenta("🏁")} ${pc.bold(run.id)} ${pc.dim(runStatus(run))} race · ${done}/${run.tasks.length} finished · ${fmtUsd(run.costUsd)}`
1443
+ : `${pc.magenta("🎼")} ${pc.bold(run.id)} ${pc.dim(runStatus(run))} ${done}/${run.tasks.length} tasks · ` +
1406
1444
  `round ${run.round}/${run.maxRounds} · ${fmtUsd(run.costUsd)} · ${pc.dim(run.branch)}`);
1407
1445
  console.log(` ${pc.dim("goal")} ${run.goal}`);
1408
1446
  if (run.race)
@@ -1515,7 +1553,7 @@ program
1515
1553
  return void console.log(pc.dim('no orchestra runs yet — loom orchestrate "<goal>"'));
1516
1554
  for (const r of runs.slice(0, 15)) {
1517
1555
  const done = r.tasks.filter((t) => t.status === "done").length;
1518
- console.log(`${pc.bold(r.id)} ${r.status.padEnd(13)} ${String(done).padStart(2)}/${String(r.tasks.length).padEnd(2)} ` +
1556
+ console.log(`${pc.bold(r.id)} ${runStatus(r).padEnd(13)} ${String(done).padStart(2)}/${String(r.tasks.length).padEnd(2)} ` +
1519
1557
  `${pc.dim(new Date(r.createdAt).toLocaleString())} ${r.goal.slice(0, 70)}`);
1520
1558
  }
1521
1559
  }
@@ -1803,7 +1841,7 @@ program
1803
1841
  });
1804
1842
  program
1805
1843
  .command("crew [action] [args...]")
1806
- .description("Agent Teams: status | create | goal | say | approve | pause | stop | resume | pick | refresh | budget | overlap | deliver | apply | diff | remove")
1844
+ .description("Agent Teams: status | create | goal | say | approve | pause | stop | resume | accept | pick | refresh | budget | overlap | deliver | apply | diff | remove")
1807
1845
  .option("--crew <id>", "which crew, when the project has several")
1808
1846
  .option("--to <teammate>", "say: to one teammate")
1809
1847
  .option("--no-approval", "create: the Lead's plan starts without your OK")
@@ -1814,7 +1852,7 @@ program
1814
1852
  .option("--reviewer <agent>", "create: who reviews")
1815
1853
  .option("--tester <agent>", "create: who tests")
1816
1854
  .option("--amount <usd>", "budget: new goal budget in USD")
1817
- .option("--card <id>", "overlap: held card id")
1855
+ .option("--card <id>", "overlap: held card id; accept: the failed card to take as it stands")
1818
1856
  .option("--decision <decision>", "overlap: narrow, wait:<goal>, or proceed:<reason>")
1819
1857
  .option("--touches <globs>", "overlap: comma-separated revised paths")
1820
1858
  .action(async (action, args, opts) => {
@@ -2577,7 +2615,8 @@ program
2577
2615
  const ago = weight.ageMs >= 3_600_000 ? `${Math.round(weight.ageMs / 3_600_000)}h` : `${Math.round(weight.ageMs / 60_000)}m`;
2578
2616
  console.error(pc.yellow(`⚠ ${target}'s session here is ${Math.round(weight.tokens / 1000)}k tokens, last used ${ago} ago — its cache has likely expired, so this turn re-reads all of it` +
2579
2617
  `${weight.estUsd !== undefined ? ` (~${fmtUsd(weight.estUsd)} est.)` : ""}. --fresh starts a new session instead.`));
2580
- if (process.stdin.isTTY && !opts.wait) {
2618
+ // a person at a terminal is asked, --wait or not; a script only gets the warning (#300)
2619
+ if (process.stdin.isTTY && process.stdout.isTTY) {
2581
2620
  const go = await confirm(pc.yellow("send it to the existing session anyway?"));
2582
2621
  if (!go)
2583
2622
  return void console.log(pc.dim(`not sent — loom send --fresh${opts.agent ? ` -a ${opts.agent}` : ""} "…"`));
@@ -3124,6 +3163,15 @@ program
3124
3163
  console.log(pc.dim(" no turns recorded yet"));
3125
3164
  console.log(pc.dim("\n(costs come from agents that report them — claude-code and opencode do)"));
3126
3165
  });
3166
+ program
3167
+ .command("memory:repair")
3168
+ .description("mend decisions recorded before 1.2.1 whose underscores and paths were stripped, from the planner's own words")
3169
+ .action(async () => {
3170
+ const client = await ensureDaemon();
3171
+ const project = await currentProject(client);
3172
+ const { repaired, checked } = await client.repairDecisions(project.id);
3173
+ console.log(repaired ? pc.green(`✓ mended ${repaired} of ${checked} review decisions`) : pc.dim(`nothing to mend among ${checked} review decisions`));
3174
+ });
3127
3175
  program
3128
3176
  .command("decision <text...>")
3129
3177
  .description("record a decision into shared memory (projected on every handoff)")
@@ -3272,8 +3320,9 @@ program
3272
3320
  }
3273
3321
  const checks = await envChecks();
3274
3322
  checks.push(...(await (await import("./doctor.js")).diskChecks()));
3323
+ const signedOut = checks.filter((c) => c.status === "fail" && /signed out/.test(c.detail)).map((c) => c.name);
3275
3324
  if (dir)
3276
- checks.push(...projectChecks(dir));
3325
+ checks.push(...projectChecks(dir, signedOut));
3277
3326
  else
3278
3327
  checks.push({ name: "project", status: "warn", detail: "not inside a Loom project (loom init)" });
3279
3328
  // The daemon is often the desktop app's, started without this shell's PATH or keys:
@@ -3339,8 +3388,24 @@ program
3339
3388
  .description("list paired devices, revoke one, or ping them with a test push")
3340
3389
  .option("--revoke <id>", "revoke a paired device's access")
3341
3390
  .option("--ping", "send a test push notification to all registered devices", false)
3391
+ .option("--prune", "revoke devices not seen for --days (never one registered for push, nor Loom Desktop)")
3392
+ .option("--days <n>", "with --prune: how long unseen counts as gone", "30")
3393
+ .option("--dry-run", "with --prune: only list what would go")
3342
3394
  .action(async (opts) => {
3343
3395
  const client = await ensureDaemon();
3396
+ if (opts.prune) {
3397
+ // every pairing made a device and nothing retired them: 86 piled up in a day of testing (#320)
3398
+ const { clients } = await client.pairedClients();
3399
+ const cutoff = Date.now() - Math.max(1, Number(opts.days) || 30) * 86_400_000;
3400
+ const gone = clients.filter((c) => !c.push && !c.owner && (c.lastSeen ?? c.createdAt) < cutoff);
3401
+ for (const c of gone) {
3402
+ if (!opts.dryRun)
3403
+ await client.revokeClient(c.id);
3404
+ console.log(`${opts.dryRun ? pc.dim("would revoke") : pc.green("✓ revoked")} ${c.name} ${pc.dim(`(${c.id}) last seen ${new Date(c.lastSeen ?? c.createdAt).toLocaleDateString()}`)}`);
3405
+ }
3406
+ console.log(pc.dim(gone.length ? `${gone.length} of ${clients.length} devices${opts.dryRun ? " would go" : " revoked"}` : `nothing unseen for ${opts.days} days among ${clients.length} devices`));
3407
+ return;
3408
+ }
3344
3409
  if (opts.revoke) {
3345
3410
  await client.revokeClient(opts.revoke);
3346
3411
  console.log(pc.green(`✓ revoked ${opts.revoke}`));
@@ -3358,9 +3423,11 @@ program
3358
3423
  return void console.log(pc.dim("no paired devices — loom pair"));
3359
3424
  for (const c of clients) {
3360
3425
  console.log(` ${pc.bold(c.name)} ${pc.dim(`(${c.id})`)} paired ${new Date(c.createdAt).toLocaleString()}` +
3426
+ pc.dim(` ·seen ${c.lastSeen ? new Date(c.lastSeen).toLocaleDateString() : "never"}`) +
3427
+ (c.owner ? pc.cyan(" ·owner") : "") +
3361
3428
  (c.push ? pc.green(" ·push✓") : pc.dim(" ·no push")));
3362
3429
  }
3363
- console.log(pc.dim("\nrevoke: loom clients --revoke <id> · test push: loom clients --ping"));
3430
+ console.log(pc.dim("\nrevoke: loom clients --revoke <id> · tidy up: loom clients --prune [--days 30] [--dry-run] · test push: loom clients --ping"));
3364
3431
  });
3365
3432
  program
3366
3433
  .command("pair")