@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.
- package/CHANGELOG.md +36 -0
- package/dist/adapters/opencode.d.ts +2 -0
- package/dist/adapters/opencode.js +9 -2
- package/dist/adapters/opencode.js.map +1 -1
- package/dist/cli/doctor.d.ts +1 -1
- package/dist/cli/doctor.js +7 -2
- package/dist/cli/doctor.js.map +1 -1
- package/dist/cli/index.js +80 -13
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/ui.js +2 -1
- package/dist/cli/ui.js.map +1 -1
- package/dist/core/brain-index.d.ts +0 -18
- package/dist/core/brain-index.js +10 -2
- package/dist/core/brain-index.js.map +1 -1
- package/dist/core/crew.d.ts +10 -0
- package/dist/core/crew.js +40 -3
- package/dist/core/crew.js.map +1 -1
- package/dist/core/prices.d.ts +16 -0
- package/dist/core/prices.js +29 -1
- package/dist/core/prices.js.map +1 -1
- package/dist/core/registry.d.ts +7 -0
- package/dist/core/registry.js.map +1 -1
- package/dist/core/replay.js +2 -0
- package/dist/core/replay.js.map +1 -1
- package/dist/daemon/auth.d.ts +7 -1
- package/dist/daemon/auth.js +19 -3
- package/dist/daemon/auth.js.map +1 -1
- package/dist/daemon/client.d.ts +15 -0
- package/dist/daemon/client.js +31 -0
- package/dist/daemon/client.js.map +1 -1
- package/dist/daemon/council.d.ts +8 -0
- package/dist/daemon/council.js +115 -36
- package/dist/daemon/council.js.map +1 -1
- package/dist/daemon/review.d.ts +4 -0
- package/dist/daemon/review.js +22 -2
- package/dist/daemon/review.js.map +1 -1
- package/dist/daemon/routes/brain.js +4 -0
- package/dist/daemon/routes/brain.js.map +1 -1
- package/dist/daemon/routes/council.js +9 -0
- package/dist/daemon/routes/council.js.map +1 -1
- package/dist/daemon/routes/crews.js +2 -0
- package/dist/daemon/routes/crews.js.map +1 -1
- package/dist/daemon/routes/pairing.js +6 -1
- package/dist/daemon/routes/pairing.js.map +1 -1
- package/dist/daemon/routes/team.js +2 -0
- package/dist/daemon/routes/team.js.map +1 -1
- package/dist/daemon/runtime/accounting.js +7 -3
- package/dist/daemon/runtime/accounting.js.map +1 -1
- package/dist/daemon/runtime/queue.d.ts +2 -0
- package/dist/daemon/runtime/queue.js +14 -0
- package/dist/daemon/runtime/queue.js.map +1 -1
- package/dist/daemon/runtime/turns.d.ts +2 -0
- package/dist/daemon/runtime/turns.js +28 -0
- package/dist/daemon/runtime/turns.js.map +1 -1
- package/dist/daemon/runtime-support.d.ts +5 -1
- package/dist/daemon/runtime-support.js +8 -2
- package/dist/daemon/runtime-support.js.map +1 -1
- package/dist/daemon/runtime.d.ts +21 -0
- package/dist/daemon/runtime.js +94 -6
- package/dist/daemon/runtime.js.map +1 -1
- package/dist/daemon/server.js +4 -0
- package/dist/daemon/server.js.map +1 -1
- package/dist/daemon/shift.d.ts +6 -0
- package/dist/daemon/shift.js +10 -1
- package/dist/daemon/shift.js.map +1 -1
- package/dist/daemon/team.d.ts +6 -0
- package/dist/daemon/team.js +15 -0
- package/dist/daemon/team.js.map +1 -1
- package/dist/web/app.css +65 -0
- package/dist/web/app.js +11783 -11607
- package/dist/web/app.js.map +4 -4
- package/docs/release-notes/v1.2.2.md +39 -0
- 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
|
-
.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
1405
|
-
: `${pc.magenta("🎼")} ${pc.bold(run.id)} ${pc.dim(run
|
|
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.
|
|
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
|
-
|
|
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")
|