multiclaw 0.9.0 → 0.10.0

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.
@@ -1,6 +1,6 @@
1
1
  export interface PruneOptions {
2
2
  config?: string;
3
- history?: boolean;
3
+ sessions?: boolean;
4
4
  runs?: boolean;
5
5
  all?: boolean;
6
6
  dryRun?: boolean;
@@ -11,7 +11,7 @@ export interface PruneItem {
11
11
  label: string;
12
12
  path: string;
13
13
  size: number;
14
- kind: "history" | "run" | "all-runs";
14
+ kind: "session" | "run" | "all-runs";
15
15
  }
16
16
  /** Scan `.multiclaw/` and list prune-able items with their sizes. */
17
17
  export declare function scanPruneItems(workDir: string): PruneItem[];
@@ -0,0 +1,13 @@
1
+ export interface SessionsCommandOptions {
2
+ config?: string;
3
+ }
4
+ /** `multiclaw sessions ls` — list saved sessions, newest first. */
5
+ export declare function sessionsListCommand(options: SessionsCommandOptions): Promise<void>;
6
+ /**
7
+ * `multiclaw sessions rm <id>` — delete a session by id or a unique prefix.
8
+ *
9
+ * A prefix is accepted because the ids are UUIDs no one types in full. An
10
+ * ambiguous prefix is refused rather than guessed, so a `rm` never deletes a
11
+ * session the user did not mean.
12
+ */
13
+ export declare function sessionsRemoveCommand(id: string, options: SessionsCommandOptions): Promise<void>;
@@ -1,4 +1,8 @@
1
1
  export interface TuiCommandOptions {
2
2
  config?: string;
3
+ /** Start a fresh session instead of continuing the most recent one. */
4
+ new?: boolean;
5
+ /** Resume a session: a specific id, or `true` to open the picker. */
6
+ resume?: string | boolean;
3
7
  }
4
8
  export declare function tuiCommand(options: TuiCommandOptions): Promise<void>;
package/dist/index.js CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  // src/index.ts
4
4
  import { readFileSync as readFileSync4 } from "fs";
5
- import { dirname as dirname4, join as join6 } from "path";
5
+ import { dirname as dirname5, join as join6 } from "path";
6
6
  import { fileURLToPath as fileURLToPath2 } from "url";
7
7
  import { Command } from "commander";
8
8
 
@@ -475,24 +475,28 @@ import { checkbox as checkbox2, confirm } from "@inquirer/prompts";
475
475
  function scanPruneItems(workDir) {
476
476
  const items = [];
477
477
  const multiclawDir = path3.join(workDir, ".multiclaw");
478
- const historyFile = path3.join(multiclawDir, "console", "history.json");
479
- if (fs3.existsSync(historyFile)) {
480
- const stat = fs3.statSync(historyFile);
481
- const raw = fs3.readFileSync(historyFile, "utf-8");
482
- let turns = 0;
483
- try {
484
- const parsed = JSON.parse(raw);
485
- if (Array.isArray(parsed)) turns = parsed.length;
486
- } catch {
487
- }
488
- items.push({
489
- id: "history",
490
- label: `Console history (${turns} turn${turns === 1 ? "" : "s"}, ${formatBytes(stat.size)})`,
491
- path: historyFile,
492
- size: stat.size,
493
- kind: "history"
478
+ const sessionsDir = path3.join(multiclawDir, "sessions");
479
+ const index = readSessionIndex(sessionsDir);
480
+ const metaById = new Map(index.map((meta) => [meta.id, meta]));
481
+ const onDisk = fs3.existsSync(sessionsDir) ? fs3.readdirSync(sessionsDir, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => entry.name) : [];
482
+ const sessionItems2 = [];
483
+ for (const id of /* @__PURE__ */ new Set([...metaById.keys(), ...onDisk])) {
484
+ const dir = path3.join(sessionsDir, id);
485
+ const size = fs3.existsSync(dir) ? dirSize(dir) : 0;
486
+ const meta = metaById.get(id);
487
+ const title = meta?.title && meta.title.trim() !== "" ? meta.title : "(untitled)";
488
+ const turns = meta?.turnCount ?? 0;
489
+ sessionItems2.push({
490
+ id,
491
+ label: `${title} (${turns} turn${turns === 1 ? "" : "s"}, ${formatBytes(size)})`,
492
+ path: dir,
493
+ size,
494
+ kind: "session",
495
+ lastActiveAt: meta?.lastActiveAt ?? 0
494
496
  });
495
497
  }
498
+ sessionItems2.sort((a, b) => b.lastActiveAt - a.lastActiveAt);
499
+ for (const { lastActiveAt: _unused, ...item } of sessionItems2) items.push(item);
496
500
  const runsDir = path3.join(multiclawDir, "runs");
497
501
  if (fs3.existsSync(runsDir)) {
498
502
  const entries = fs3.readdirSync(runsDir, { withFileTypes: true }).filter((e) => e.isDirectory() && e.name.startsWith("run-")).sort((a, b) => a.name.localeCompare(b.name));
@@ -527,8 +531,9 @@ function deletePruneItems(items) {
527
531
  const failed = [];
528
532
  for (const item of items) {
529
533
  try {
530
- if (item.kind === "history") {
531
- fs3.unlinkSync(item.path);
534
+ if (item.kind === "session") {
535
+ fs3.rmSync(item.path, { recursive: true, force: true });
536
+ removeSessionFromIndex(path3.dirname(item.path), path3.basename(item.path));
532
537
  deleted.push(item.id);
533
538
  } else if (item.kind === "run") {
534
539
  fs3.rmSync(item.path, { recursive: true, force: true });
@@ -555,11 +560,11 @@ function resolvePruneTargets(options, items) {
555
560
  if (options.all) {
556
561
  return items.map((i) => i.id);
557
562
  }
558
- if (options.history && options.runs) {
563
+ if (options.sessions && options.runs) {
559
564
  return items.map((i) => i.id);
560
565
  }
561
- if (options.history) {
562
- return items.filter((i) => i.kind === "history").map((i) => i.id);
566
+ if (options.sessions) {
567
+ return items.filter((i) => i.kind === "session").map((i) => i.id);
563
568
  }
564
569
  if (options.runs) {
565
570
  return items.filter((i) => i.kind === "run" || i.kind === "all-runs").map((i) => i.id);
@@ -570,7 +575,7 @@ async function pruneCommand(options) {
570
575
  const config = await loadConfig(options.config);
571
576
  const items = scanPruneItems(config.workDir);
572
577
  if (items.length === 0) {
573
- console.log("Nothing to prune \u2014 no history or runs found.");
578
+ console.log("Nothing to prune \u2014 no sessions or runs found.");
574
579
  return;
575
580
  }
576
581
  let selectedIds;
@@ -635,6 +640,46 @@ async function pruneCommand(options) {
635
640
  process.exitCode = 1;
636
641
  }
637
642
  }
643
+ function readSessionIndex(sessionsDir) {
644
+ try {
645
+ const parsed = JSON.parse(
646
+ fs3.readFileSync(path3.join(sessionsDir, "index.json"), "utf-8")
647
+ );
648
+ if (!Array.isArray(parsed)) return [];
649
+ const entries = [];
650
+ for (const value of parsed) {
651
+ if (typeof value !== "object" || value === null) continue;
652
+ const entry = value;
653
+ if (typeof entry.id !== "string") continue;
654
+ entries.push({
655
+ id: entry.id,
656
+ title: typeof entry.title === "string" ? entry.title : "",
657
+ turnCount: typeof entry.turnCount === "number" ? entry.turnCount : 0,
658
+ lastActiveAt: typeof entry.lastActiveAt === "number" ? entry.lastActiveAt : 0
659
+ });
660
+ }
661
+ return entries;
662
+ } catch {
663
+ return [];
664
+ }
665
+ }
666
+ function removeSessionFromIndex(sessionsDir, id) {
667
+ const indexPath = path3.join(sessionsDir, "index.json");
668
+ let parsed;
669
+ try {
670
+ parsed = JSON.parse(fs3.readFileSync(indexPath, "utf-8"));
671
+ } catch {
672
+ return;
673
+ }
674
+ if (!Array.isArray(parsed)) return;
675
+ const next = parsed.filter(
676
+ (entry) => !(typeof entry === "object" && entry !== null && entry.id === id)
677
+ );
678
+ if (next.length === parsed.length) return;
679
+ const tmp = `${indexPath}.tmp`;
680
+ fs3.writeFileSync(tmp, JSON.stringify(next), "utf-8");
681
+ fs3.renameSync(tmp, indexPath);
682
+ }
638
683
  function formatBytes(bytes) {
639
684
  if (bytes === 0) return "0 B";
640
685
  const k = 1024;
@@ -997,9 +1042,9 @@ function resumeOptionsFromReport(report, requirementOverride) {
997
1042
  if (agents2.length === 0) {
998
1043
  throw new Error("The previous run has no agent results \u2014 nothing to resume");
999
1044
  }
1000
- const sessions = {};
1045
+ const sessions2 = {};
1001
1046
  for (const result of latest.values()) {
1002
- if (result.sessionId) sessions[result.agentId] = result.sessionId;
1047
+ if (result.sessionId) sessions2[result.agentId] = result.sessionId;
1003
1048
  }
1004
1049
  const requirement = requirementOverride ?? report.requirement;
1005
1050
  if (requirement === void 0) {
@@ -1011,7 +1056,7 @@ function resumeOptionsFromReport(report, requirementOverride) {
1011
1056
  agents: agents2,
1012
1057
  // Absent, not `{}`: core reads a missing `sessions` as "start every agent
1013
1058
  // fresh", which is exactly the single-agent retry of a failed agent.
1014
- ...Object.keys(sessions).length > 0 ? { sessions } : {}
1059
+ ...Object.keys(sessions2).length > 0 ? { sessions: sessions2 } : {}
1015
1060
  }
1016
1061
  };
1017
1062
  }
@@ -1128,13 +1173,67 @@ function walkFiles(root, visit) {
1128
1173
  }
1129
1174
  }
1130
1175
 
1176
+ // src/commands/sessions.ts
1177
+ function sessionItems(workDir) {
1178
+ return scanPruneItems(workDir).filter((item) => item.kind === "session");
1179
+ }
1180
+ async function sessionsListCommand(options) {
1181
+ const config = await loadConfig(options.config);
1182
+ const items = sessionItems(config.workDir);
1183
+ if (items.length === 0) {
1184
+ console.log("No saved sessions.");
1185
+ return;
1186
+ }
1187
+ for (const item of items) {
1188
+ console.log(` \x1B[2m${item.id}\x1B[0m ${item.label}`);
1189
+ }
1190
+ }
1191
+ async function sessionsRemoveCommand(id, options) {
1192
+ const config = await loadConfig(options.config);
1193
+ const items = sessionItems(config.workDir);
1194
+ const exact = items.find((item) => item.id === id);
1195
+ const matches = exact ? [exact] : items.filter((item) => item.id.startsWith(id));
1196
+ if (matches.length === 0) {
1197
+ console.error(`No session matching "${id}".`);
1198
+ process.exitCode = 1;
1199
+ return;
1200
+ }
1201
+ if (matches.length > 1) {
1202
+ console.error(
1203
+ `"${id}" matches ${matches.length} sessions \u2014 use more of the id:
1204
+ ${matches.map((m) => ` ${m.id}`).join("\n")}`
1205
+ );
1206
+ process.exitCode = 1;
1207
+ return;
1208
+ }
1209
+ const { deleted, failed } = deletePruneItems(matches);
1210
+ if (deleted.length > 0) {
1211
+ console.log(`\x1B[32m\u2713\x1B[0m Removed session ${matches[0].id}`);
1212
+ }
1213
+ if (failed.length > 0) {
1214
+ console.error(`\x1B[31m\u2717\x1B[0m Failed to remove ${matches[0].id}: ${failed[0].error}`);
1215
+ process.exitCode = 1;
1216
+ }
1217
+ }
1218
+
1131
1219
  // src/commands/tui.ts
1220
+ function initialSession(options) {
1221
+ if (options.new) return { kind: "new" };
1222
+ if (options.resume !== void 0) {
1223
+ return {
1224
+ kind: "resume",
1225
+ ...typeof options.resume === "string" ? { id: options.resume } : {}
1226
+ };
1227
+ }
1228
+ return void 0;
1229
+ }
1132
1230
  async function tuiCommand(options) {
1133
1231
  const config = await loadConfig(options.config);
1134
1232
  await assertRuntimesAvailable(config);
1135
1233
  const { startTui } = await import("@multiclawcli/dashboard/tui");
1136
1234
  await startTui({
1137
1235
  config,
1236
+ initialSession: initialSession(options),
1138
1237
  onTurnStart: (eventBus, logDir) => {
1139
1238
  const writer = new FileLogWriter(logDir);
1140
1239
  const detach = writer.attach(eventBus);
@@ -1152,7 +1251,7 @@ async function tuiCommand(options) {
1152
1251
  // src/commands/upgrade.ts
1153
1252
  import { execSync } from "child_process";
1154
1253
  import { existsSync as existsSync6, readFileSync as readFileSync3 } from "fs";
1155
- import { dirname as dirname3, join as join5, resolve as resolve4 } from "path";
1254
+ import { dirname as dirname4, join as join5, resolve as resolve4 } from "path";
1156
1255
  import { fileURLToPath } from "url";
1157
1256
  var PACKAGE_NAME = "multiclaw";
1158
1257
  var RED2 = "\x1B[31m";
@@ -1171,7 +1270,7 @@ function findPackageRoot(start) {
1171
1270
  let dir = resolve4(start);
1172
1271
  for (; ; ) {
1173
1272
  if (readManifest(dir)?.name === PACKAGE_NAME) return dir;
1174
- const parent = dirname3(dir);
1273
+ const parent = dirname4(dir);
1175
1274
  if (parent === dir) return void 0;
1176
1275
  dir = parent;
1177
1276
  }
@@ -1202,7 +1301,7 @@ var MANUAL = {
1202
1301
  unknown: "npm install -g multiclaw@latest"
1203
1302
  };
1204
1303
  function createUpgradeContext() {
1205
- const pkgRoot = findPackageRoot(dirname3(fileURLToPath(import.meta.url)));
1304
+ const pkgRoot = findPackageRoot(dirname4(fileURLToPath(import.meta.url)));
1206
1305
  if (!pkgRoot) return void 0;
1207
1306
  const currentVersion = readInstalledVersion(pkgRoot);
1208
1307
  if (!currentVersion) return void 0;
@@ -1275,10 +1374,12 @@ async function upgradeCommand() {
1275
1374
 
1276
1375
  // src/index.ts
1277
1376
  var __filename2 = fileURLToPath2(import.meta.url);
1278
- var __dirname2 = dirname4(__filename2);
1377
+ var __dirname2 = dirname5(__filename2);
1279
1378
  var pkg = JSON.parse(readFileSync4(join6(__dirname2, "../package.json"), "utf-8"));
1280
1379
  var program = new Command();
1281
- program.name("multiclaw").description("Multi-agent orchestration CLI").version(pkg.version, "-v, --version", "Display version number").action((options) => tuiCommand(options));
1380
+ program.name("multiclaw").description("Multi-agent orchestration CLI").version(pkg.version, "-v, --version", "Display version number").option("--config <path>", "Config file path (default: auto-discover ./multiclaw.config.ts)").option("--new", "Start a new session instead of continuing the most recent").option("-r, --resume [id]", "Resume a session by id, or open the session picker").action(
1381
+ (options) => tuiCommand(options)
1382
+ );
1282
1383
  program.command("init").description("Scaffold a config file (interactive)").action(() => initCommand());
1283
1384
  program.command("run [requirement]").description("Run an orchestration headlessly").option("--config <path>", "Config file path (default: auto-discover ./multiclaw.config.ts)").option("--no-leader", "Batch mode only: skip routing and run the full team").option("--dry-run", "Preview the routed plan without running the pipeline").option(
1284
1385
  "--resume [path]",
@@ -1286,7 +1387,10 @@ program.command("run [requirement]").description("Run an orchestration headlessl
1286
1387
  ).action((requirement, opts) => runCommand(requirement, opts));
1287
1388
  program.command("upgrade").description("Upgrade multiclaw to the latest version").action(() => upgradeCommand());
1288
1389
  program.command("doctor").description("Check which agent providers are installed and ready").option("--config <path>", "Config file path (default: auto-discover ./multiclaw.config.ts)").action((opts) => doctorCommand(opts.config));
1289
- program.command("prune").description("Clean up history entries and old run directories (interactive)").option("--config <path>", "Config file path (default: auto-discover ./multiclaw.config.ts)").option("--history", "Prune console history only (non-interactive)").option("--runs", "Prune all runs only (non-interactive)").option("--all", "Prune everything (non-interactive)").option("--dry-run", "Show what would be deleted without deleting").option("--yes", "Skip confirmation prompt").action((opts) => pruneCommand(opts));
1390
+ program.command("prune").description("Clean up saved sessions and old run directories (interactive)").option("--config <path>", "Config file path (default: auto-discover ./multiclaw.config.ts)").option("--sessions", "Prune saved sessions only (non-interactive)").option("--runs", "Prune all runs only (non-interactive)").option("--all", "Prune everything (non-interactive)").option("--dry-run", "Show what would be deleted without deleting").option("--yes", "Skip confirmation prompt").action((opts) => pruneCommand(opts));
1391
+ var sessions = program.command("sessions").description("Manage saved console sessions");
1392
+ sessions.command("ls").description("List saved sessions, newest first").option("--config <path>", "Config file path (default: auto-discover ./multiclaw.config.ts)").action((opts) => sessionsListCommand(opts));
1393
+ sessions.command("rm <id>").description("Delete a saved session by id (or a unique prefix)").option("--config <path>", "Config file path (default: auto-discover ./multiclaw.config.ts)").action((id, opts) => sessionsRemoveCommand(id, opts));
1290
1394
  program.parseAsync(process.argv).catch((err) => {
1291
1395
  const message = err instanceof Error ? err.message : String(err);
1292
1396
  console.error(`\x1B[31mError: ${message}\x1B[0m`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "multiclaw",
3
- "version": "0.9.0",
3
+ "version": "0.10.0",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "multiclaw": "dist/index.js"
@@ -30,8 +30,8 @@
30
30
  "@agentclientprotocol/claude-agent-acp": "^0.95.0",
31
31
  "@agentclientprotocol/codex-acp": "^2.2.2",
32
32
  "@inquirer/prompts": "^8.7.4",
33
- "@multiclawcli/core": "0.9.0",
34
- "@multiclawcli/dashboard": "0.9.0",
33
+ "@multiclawcli/core": "0.10.0",
34
+ "@multiclawcli/dashboard": "0.10.0",
35
35
  "commander": "^15.0.0",
36
36
  "jiti": "^2.7.0"
37
37
  },