@basou/cli 0.5.0 → 0.7.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.
package/dist/index.js CHANGED
@@ -1327,9 +1327,9 @@ async function assertWorkspaceInitialized4(basouRoot) {
1327
1327
 
1328
1328
  // src/commands/import.ts
1329
1329
  import { createReadStream } from "fs";
1330
- import { readdir, readFile, rm } from "fs/promises";
1330
+ import { readdir, readFile, rm, stat } from "fs/promises";
1331
1331
  import { homedir as homedir2 } from "os";
1332
- import { basename, join as join3 } from "path";
1332
+ import { basename, join as join3, resolve } from "path";
1333
1333
  import { createInterface } from "readline";
1334
1334
  import {
1335
1335
  assertBasouRootSafe as assertBasouRootSafe6,
@@ -1348,11 +1348,16 @@ import {
1348
1348
  } from "@basou/core";
1349
1349
  var SES_PREFIX2 = "ses_";
1350
1350
  var SHORT_ID_LEN2 = 6;
1351
+ function collectPath(value, previous) {
1352
+ return [...previous, value];
1353
+ }
1351
1354
  function registerImportCommand(program2) {
1352
1355
  const importCmd = program2.command("import").description("Import provenance from an external AI tool's native logs");
1353
1356
  importCmd.command("claude-code").description("Derive Basou sessions from Claude Code native transcripts (~/.claude/projects)").option(
1354
1357
  "--project <path>",
1355
- "Source project path whose transcripts to import (defaults to the current repository root)"
1358
+ "Source project path whose transcripts to import (repeatable; defaults to the manifest source roots, then the repository root)",
1359
+ collectPath,
1360
+ []
1356
1361
  ).option("--session <id>", "Import a single transcript by its Claude session id").option("--all", "Import every transcript found for the project").option(
1357
1362
  "--force",
1358
1363
  "Re-import sessions already imported: delete and replace them instead of skipping"
@@ -1361,7 +1366,9 @@ function registerImportCommand(program2) {
1361
1366
  });
1362
1367
  importCmd.command("codex").description("Derive Basou sessions from OpenAI Codex native rollout logs (~/.codex/sessions)").option(
1363
1368
  "--project <path>",
1364
- "Source project path whose rollouts to import (defaults to the current repository root)"
1369
+ "Source project path whose rollouts to import (repeatable; defaults to the manifest source roots, then the repository root)",
1370
+ collectPath,
1371
+ []
1365
1372
  ).option("--session <id>", "Import a single rollout by its Codex session id").option("--all", "Import every rollout found for the project").option(
1366
1373
  "--force",
1367
1374
  "Re-import sessions already imported: delete and replace them instead of skipping"
@@ -1385,13 +1392,28 @@ async function runImportCodex(options, ctx = {}) {
1385
1392
  process.exitCode = 1;
1386
1393
  }
1387
1394
  }
1395
+ function resolveSourceRoots(args) {
1396
+ const { projectFlags, manifest, repoRoot, cwd } = args;
1397
+ let resolved;
1398
+ if (projectFlags.length > 0) {
1399
+ resolved = projectFlags.map((p) => resolve(cwd, p));
1400
+ } else {
1401
+ const roots = manifest.import?.source_roots;
1402
+ resolved = roots !== void 0 && roots.length > 0 ? roots.map((r) => resolve(repoRoot, r)) : [repoRoot];
1403
+ }
1404
+ return [...new Set(resolved)];
1405
+ }
1388
1406
  async function doRunImportClaudeCode(options, ctx) {
1389
1407
  assertSelector(options);
1390
1408
  const { repositoryRoot, paths, manifest } = await resolveImportTarget(ctx);
1391
- const projectPath = options.project ?? repositoryRoot;
1409
+ const projectPaths = resolveSourceRoots({
1410
+ projectFlags: options.project ?? [],
1411
+ manifest,
1412
+ repoRoot: repositoryRoot,
1413
+ cwd: ctx.cwd ?? process.cwd()
1414
+ });
1392
1415
  const projectsRoot = ctx.claudeProjectsDir ?? join3(homedir2(), ".claude", "projects");
1393
- const transcriptDir = join3(projectsRoot, encodeProjectDir(projectPath));
1394
- const files = await selectTranscriptFiles(transcriptDir, options);
1416
+ const files = await selectTranscriptFiles(projectsRoot, projectPaths, options);
1395
1417
  const candidates = files.map((file) => {
1396
1418
  const externalId = basename(file, ".jsonl");
1397
1419
  return {
@@ -1407,9 +1429,14 @@ async function doRunImportClaudeCode(options, ctx) {
1407
1429
  async function doRunImportCodex(options, ctx) {
1408
1430
  assertSelector(options);
1409
1431
  const { repositoryRoot, paths, manifest } = await resolveImportTarget(ctx);
1410
- const projectPath = options.project ?? repositoryRoot;
1432
+ const projectPaths = resolveSourceRoots({
1433
+ projectFlags: options.project ?? [],
1434
+ manifest,
1435
+ repoRoot: repositoryRoot,
1436
+ cwd: ctx.cwd ?? process.cwd()
1437
+ });
1411
1438
  const sessionsRoot = ctx.codexSessionsDir ?? join3(homedir2(), ".codex", "sessions");
1412
- const rollouts = await discoverCodexRollouts(sessionsRoot, projectPath, options);
1439
+ const rollouts = await discoverCodexRollouts(sessionsRoot, projectPaths, options);
1413
1440
  const candidates = rollouts.map(({ file, externalId }) => ({
1414
1441
  externalId,
1415
1442
  toPayload: async () => codexRolloutToImportPayload(await readJsonlRecords(file), {
@@ -1420,6 +1447,9 @@ async function doRunImportCodex(options, ctx) {
1420
1447
  await importDerivedSessions(paths, manifest, options, CODEX_IMPORT_SOURCE, candidates);
1421
1448
  }
1422
1449
  function assertSelector(options) {
1450
+ if (options.session !== void 0 && options.all === true) {
1451
+ throw new Error("Specify either --session <id> or --all, not both");
1452
+ }
1423
1453
  if (options.session === void 0 && options.all !== true) {
1424
1454
  throw new Error("Specify --session <id> or --all");
1425
1455
  }
@@ -1517,28 +1547,56 @@ async function loadExistingByExternalId(paths, sourceKind) {
1517
1547
  }
1518
1548
  return byExternalId;
1519
1549
  }
1520
- async function selectTranscriptFiles(transcriptDir, options) {
1550
+ async function selectTranscriptFiles(projectsRoot, projectPaths, options) {
1521
1551
  if (options.session !== void 0) {
1522
- return [join3(transcriptDir, `${options.session}.jsonl`)];
1552
+ const matches = [];
1553
+ for (const projectPath of projectPaths) {
1554
+ const file = join3(projectsRoot, encodeProjectDir(projectPath), `${options.session}.jsonl`);
1555
+ if (await pathExists(file)) matches.push(file);
1556
+ }
1557
+ if (matches.length === 0) {
1558
+ throw new Error("Claude transcript not found for session id in project");
1559
+ }
1560
+ return [...new Set(matches)];
1561
+ }
1562
+ const files = [];
1563
+ let anyDirFound = false;
1564
+ for (const projectPath of projectPaths) {
1565
+ const transcriptDir = join3(projectsRoot, encodeProjectDir(projectPath));
1566
+ let entries;
1567
+ try {
1568
+ entries = await readdir(transcriptDir);
1569
+ } catch (error) {
1570
+ if (findErrorCode5(error, "ENOENT")) continue;
1571
+ throw new Error("Failed to read Claude transcript directory", { cause: error });
1572
+ }
1573
+ anyDirFound = true;
1574
+ for (const name of entries) {
1575
+ if (name.endsWith(".jsonl")) files.push(join3(transcriptDir, name));
1576
+ }
1523
1577
  }
1524
- let entries;
1578
+ if (!anyDirFound) {
1579
+ throw new Error("Claude transcript directory not found for project");
1580
+ }
1581
+ return [...new Set(files)].sort();
1582
+ }
1583
+ async function pathExists(file) {
1525
1584
  try {
1526
- entries = await readdir(transcriptDir);
1585
+ await stat(file);
1586
+ return true;
1527
1587
  } catch (error) {
1528
- if (findErrorCode5(error, "ENOENT")) {
1529
- throw new Error("Claude transcript directory not found for project", { cause: error });
1530
- }
1531
- throw new Error("Failed to read Claude transcript directory", { cause: error });
1588
+ if (findErrorCode5(error, "ENOENT")) return false;
1589
+ throw error;
1532
1590
  }
1533
- return entries.filter((name) => name.endsWith(".jsonl")).sort().map((name) => join3(transcriptDir, name));
1534
1591
  }
1535
- async function discoverCodexRollouts(sessionsRoot, projectPath, options) {
1592
+ async function discoverCodexRollouts(sessionsRoot, projectPaths, options) {
1593
+ const projectSet = new Set(projectPaths);
1536
1594
  const files = await findRolloutFiles(sessionsRoot);
1537
1595
  const matched = [];
1538
1596
  for (const file of files) {
1539
1597
  const meta = await readRolloutMeta(file);
1540
1598
  if (meta === void 0) continue;
1541
- if (meta.cwd !== projectPath) continue;
1599
+ if (!projectSet.has(meta.cwd)) continue;
1542
1600
  if (options.session !== void 0 && meta.id !== options.session) continue;
1543
1601
  matched.push({ file, externalId: meta.id });
1544
1602
  }
@@ -1709,7 +1767,7 @@ async function assertWorkspaceInitialized5(basouRoot) {
1709
1767
  }
1710
1768
 
1711
1769
  // src/commands/init.ts
1712
- import { basename as basename2 } from "path";
1770
+ import { basename as basename2, relative, resolve as resolve2 } from "path";
1713
1771
  import {
1714
1772
  appendBasouGitignore,
1715
1773
  createManifest,
@@ -1718,10 +1776,18 @@ import {
1718
1776
  tryRemoteUrl,
1719
1777
  writeManifest
1720
1778
  } from "@basou/core";
1779
+ function collectValue(value, previous) {
1780
+ return [...previous, value];
1781
+ }
1721
1782
  function registerInitCommand(program2) {
1722
1783
  program2.command("init").description("Initialize a Basou workspace at the current Git repository root").option("--name <name>", "Workspace name (defaults to the repository directory name)").option("--project-name <name>", "Project display name").option("--project-description <description>", "Project description").option(
1723
1784
  "--repo-url <url>",
1724
1785
  "Repository URL (defaults to git remote.origin.url; pass empty string for null)"
1786
+ ).option(
1787
+ "--source-root <path>",
1788
+ "Extra import source root, relative to the repo root (repeatable; aggregates sibling repos into this workspace)",
1789
+ collectValue,
1790
+ []
1725
1791
  ).option("-f, --force", "Overwrite an existing manifest").option("-v, --verbose", "Show error causes").action(async (options) => {
1726
1792
  await runInit(options);
1727
1793
  });
@@ -1744,12 +1810,17 @@ async function doRunInit(options, ctx) {
1744
1810
  } else {
1745
1811
  repositoryUrl = await tryRemoteUrl(repositoryRoot);
1746
1812
  }
1813
+ const sourceRoots = (options.sourceRoot ?? []).map((p) => {
1814
+ const rel = relative(repositoryRoot, resolve2(cwd, p));
1815
+ return rel === "" ? "." : rel;
1816
+ });
1747
1817
  const paths = await ensureBasouDirectory(repositoryRoot);
1748
1818
  const manifest = createManifest({
1749
1819
  workspaceName,
1750
1820
  ...options.projectName !== void 0 ? { projectName: options.projectName } : {},
1751
1821
  ...options.projectDescription !== void 0 ? { projectDescription: options.projectDescription } : {},
1752
- ...repositoryUrl !== void 0 ? { repositoryUrl } : {}
1822
+ ...repositoryUrl !== void 0 ? { repositoryUrl } : {},
1823
+ ...sourceRoots.length > 0 ? { sourceRoots } : {}
1753
1824
  });
1754
1825
  await writeManifest(paths, manifest, { force: options.force === true });
1755
1826
  try {
@@ -1783,7 +1854,8 @@ async function resolveRepositoryRootForInit(cwd) {
1783
1854
  }
1784
1855
 
1785
1856
  // src/commands/refresh.ts
1786
- import { assertBasouRootSafe as assertBasouRootSafe7, basouPaths as basouPaths7, findErrorCode as findErrorCode6, resolveRepositoryRoot as resolveRepositoryRoot8 } from "@basou/core";
1857
+ import { assertBasouRootSafe as assertBasouRootSafe7, basouPaths as basouPaths7, findErrorCode as findErrorCode7, resolveRepositoryRoot as resolveRepositoryRoot8 } from "@basou/core";
1858
+ import { InvalidArgumentError as InvalidArgumentError2 } from "commander";
1787
1859
 
1788
1860
  // src/lib/provenance-actions.ts
1789
1861
  import {
@@ -1906,25 +1978,213 @@ async function refreshAll(args) {
1906
1978
  };
1907
1979
  }
1908
1980
 
1981
+ // src/commands/refresh-watch.ts
1982
+ import { readdir as readdir2, stat as stat2 } from "fs/promises";
1983
+ import { homedir as homedir3 } from "os";
1984
+ import { join as join4 } from "path";
1985
+ import { findErrorCode as findErrorCode6 } from "@basou/core";
1986
+ var DEFAULT_WATCH_INTERVAL_SEC = 30;
1987
+ var MIN_WATCH_INTERVAL_SEC = 5;
1988
+ var MAX_WATCH_INTERVAL_SEC = 86400;
1989
+ function watchedRoots(ctx) {
1990
+ return [
1991
+ ctx.codexSessionsDir ?? join4(homedir3(), ".codex", "sessions"),
1992
+ ctx.claudeProjectsDir ?? join4(homedir3(), ".claude", "projects")
1993
+ ];
1994
+ }
1995
+ async function scanSourceLogs(roots) {
1996
+ const out = /* @__PURE__ */ new Map();
1997
+ const walk = async (dir) => {
1998
+ let entries;
1999
+ try {
2000
+ entries = await readdir2(dir, { withFileTypes: true });
2001
+ } catch (error) {
2002
+ if (findErrorCode6(error, "ENOENT") || findErrorCode6(error, "ENOTDIR")) return;
2003
+ throw new Error("Failed to read a source log directory", { cause: error });
2004
+ }
2005
+ for (const entry of entries) {
2006
+ const full = join4(dir, entry.name);
2007
+ if (entry.isDirectory()) {
2008
+ await walk(full);
2009
+ } else if (entry.isFile() && entry.name.endsWith(".jsonl")) {
2010
+ try {
2011
+ const info = await stat2(full);
2012
+ out.set(full, { mtimeMs: info.mtimeMs, size: info.size });
2013
+ } catch (error) {
2014
+ if (findErrorCode6(error, "ENOENT")) continue;
2015
+ throw new Error("Failed to stat a source log file", { cause: error });
2016
+ }
2017
+ }
2018
+ }
2019
+ };
2020
+ for (const root of roots) await walk(root);
2021
+ return out;
2022
+ }
2023
+ function scansEqual(a, b) {
2024
+ if (a.size !== b.size) return false;
2025
+ for (const [path, sig] of a) {
2026
+ const other = b.get(path);
2027
+ if (other === void 0 || other.mtimeMs !== sig.mtimeMs || other.size !== sig.size) {
2028
+ return false;
2029
+ }
2030
+ }
2031
+ return true;
2032
+ }
2033
+ function importedCount(outcome) {
2034
+ return outcome.status === "ran" ? outcome.importedCount : 0;
2035
+ }
2036
+ function describeOutcome(outcome) {
2037
+ return outcome.status === "ran" ? `${outcome.adapter} +${outcome.importedCount}` : `${outcome.adapter} skipped`;
2038
+ }
2039
+ function hms(date) {
2040
+ return date.toISOString().slice(11, 19);
2041
+ }
2042
+ async function runImports(deps) {
2043
+ const claude = await importClaudeCode(deps.importOptions, deps.ctx);
2044
+ const codex = await importCodex(deps.importOptions, deps.ctx);
2045
+ return { claude, codex, imported: importedCount(claude) + importedCount(codex) };
2046
+ }
2047
+ async function regenerate(deps) {
2048
+ const nowIso = deps.now().toISOString();
2049
+ const handoff = await regenerateHandoff(deps.paths, nowIso);
2050
+ await regenerateDecisions(deps.paths, nowIso);
2051
+ return handoff.sessionCount;
2052
+ }
2053
+ async function runRefreshWatch(deps) {
2054
+ const { intervalMs, ctx, signal, sleep, log } = deps;
2055
+ const roots = watchedRoots(ctx);
2056
+ log(
2057
+ `watching ${roots.join(", ")} every ${Math.round(intervalMs / 1e3)}s (imports on change; Ctrl-C to stop)`
2058
+ );
2059
+ let lastScan = await scanSourceLogs(roots);
2060
+ let importedScan = lastScan;
2061
+ const initial = await runImports(deps);
2062
+ const initialSessions = await regenerate(deps);
2063
+ log(
2064
+ `[${hms(deps.now())}] refreshed: ${describeOutcome(initial.codex)}, ${describeOutcome(initial.claude)} (sessions: ${initialSessions})`
2065
+ );
2066
+ if (signal.aborted) {
2067
+ log("watch stopped");
2068
+ return;
2069
+ }
2070
+ let pendingRegen = false;
2071
+ while (!signal.aborted) {
2072
+ await sleep(intervalMs, signal);
2073
+ if (signal.aborted) break;
2074
+ try {
2075
+ const current = await scanSourceLogs(roots);
2076
+ if (scansEqual(current, lastScan) && !scansEqual(current, importedScan)) {
2077
+ const { claude, codex, imported } = await runImports(deps);
2078
+ if (imported > 0) pendingRegen = true;
2079
+ if (pendingRegen) {
2080
+ const sessions = await regenerate(deps);
2081
+ pendingRegen = false;
2082
+ log(
2083
+ `[${hms(deps.now())}] refreshed: ${describeOutcome(codex)}, ${describeOutcome(claude)} (sessions: ${sessions})`
2084
+ );
2085
+ }
2086
+ importedScan = current;
2087
+ }
2088
+ lastScan = current;
2089
+ } catch (error) {
2090
+ const message = error instanceof Error ? error.message : String(error);
2091
+ log(`[${hms(deps.now())}] refresh cycle skipped: ${message}`);
2092
+ }
2093
+ }
2094
+ log("watch stopped");
2095
+ }
2096
+
1909
2097
  // src/commands/refresh.ts
2098
+ function collectPath2(value, previous) {
2099
+ return [...previous, value];
2100
+ }
2101
+ function parseInterval(value) {
2102
+ const seconds = Number(value);
2103
+ if (!Number.isInteger(seconds) || seconds < MIN_WATCH_INTERVAL_SEC || seconds > MAX_WATCH_INTERVAL_SEC) {
2104
+ throw new InvalidArgumentError2(
2105
+ `--interval must be an integer between ${MIN_WATCH_INTERVAL_SEC} and ${MAX_WATCH_INTERVAL_SEC} (seconds).`
2106
+ );
2107
+ }
2108
+ return seconds;
2109
+ }
2110
+ function abortableSleep(ms, signal) {
2111
+ return new Promise((resolve3) => {
2112
+ if (signal.aborted) {
2113
+ resolve3();
2114
+ return;
2115
+ }
2116
+ let timer;
2117
+ const onAbort = () => {
2118
+ clearTimeout(timer);
2119
+ resolve3();
2120
+ };
2121
+ timer = setTimeout(() => {
2122
+ signal.removeEventListener("abort", onAbort);
2123
+ resolve3();
2124
+ }, ms);
2125
+ signal.addEventListener("abort", onAbort, { once: true });
2126
+ });
2127
+ }
1910
2128
  function registerRefreshCommand(program2) {
1911
2129
  program2.command("refresh").description(
1912
2130
  "Import all adapters for the project and regenerate handoff + decisions in one step"
1913
2131
  ).option(
1914
2132
  "--project <path>",
1915
- "Source project path to import (defaults to the current repository root)"
1916
- ).option("--force", "Re-import sessions already imported instead of skipping").option("--dry-run", "Preview imports and skip writing handoff / decisions").option("--json", "Output the result as JSON").option("-v, --verbose", "Show error causes").action(async (options) => {
2133
+ "Source project path to import (repeatable; defaults to the manifest source roots, then the repository root)",
2134
+ collectPath2,
2135
+ []
2136
+ ).option("--force", "Re-import sessions already imported instead of skipping").option("--dry-run", "Preview imports and skip writing handoff / decisions").option("--json", "Output the result as JSON").option(
2137
+ "--watch",
2138
+ "Keep running: re-import + regenerate when the native logs change (Ctrl-C to stop)"
2139
+ ).option(
2140
+ "--interval <seconds>",
2141
+ `Poll interval for --watch, in seconds (default ${DEFAULT_WATCH_INTERVAL_SEC}, min ${MIN_WATCH_INTERVAL_SEC})`,
2142
+ parseInterval
2143
+ ).option("-v, --verbose", "Show error causes").action(async (options) => {
1917
2144
  await runRefresh(options);
1918
2145
  });
1919
2146
  }
1920
2147
  async function runRefresh(options, ctx = {}) {
1921
2148
  try {
1922
- await doRunRefresh(options, ctx);
2149
+ if (options.watch === true) {
2150
+ await doRunRefreshWatch(options, ctx);
2151
+ } else {
2152
+ await doRunRefresh(options, ctx);
2153
+ }
1923
2154
  } catch (error) {
1924
2155
  renderCliError(error, { verbose: isVerbose(options) });
1925
2156
  process.exitCode = 1;
1926
2157
  }
1927
2158
  }
2159
+ async function doRunRefreshWatch(options, ctx) {
2160
+ if (options.dryRun === true) throw new Error("--watch cannot be combined with --dry-run.");
2161
+ if (options.json === true) throw new Error("--watch cannot be combined with --json.");
2162
+ if (options.force === true) throw new Error("--watch cannot be combined with --force.");
2163
+ const cwd = ctx.cwd ?? process.cwd();
2164
+ const repositoryRoot = await resolveRepositoryRootForRefresh(cwd);
2165
+ const paths = basouPaths7(repositoryRoot);
2166
+ await assertWorkspaceInitialized6(paths.root);
2167
+ const intervalMs = (options.interval ?? DEFAULT_WATCH_INTERVAL_SEC) * 1e3;
2168
+ const controller = new AbortController();
2169
+ const onSignal = () => controller.abort();
2170
+ process.on("SIGINT", onSignal);
2171
+ process.on("SIGTERM", onSignal);
2172
+ try {
2173
+ await runRefreshWatch({
2174
+ ctx,
2175
+ paths,
2176
+ intervalMs,
2177
+ importOptions: options.project !== void 0 && options.project.length > 0 ? { project: options.project } : {},
2178
+ now: () => ctx.nowProvider?.() ?? /* @__PURE__ */ new Date(),
2179
+ signal: controller.signal,
2180
+ sleep: abortableSleep,
2181
+ log: (line) => console.log(line)
2182
+ });
2183
+ } finally {
2184
+ process.off("SIGINT", onSignal);
2185
+ process.off("SIGTERM", onSignal);
2186
+ }
2187
+ }
1928
2188
  async function doRunRefresh(options, ctx) {
1929
2189
  const cwd = ctx.cwd ?? process.cwd();
1930
2190
  const repositoryRoot = await resolveRepositoryRootForRefresh(cwd);
@@ -1933,7 +2193,7 @@ async function doRunRefresh(options, ctx) {
1933
2193
  const nowIso = (ctx.nowProvider?.() ?? /* @__PURE__ */ new Date()).toISOString();
1934
2194
  const result = await refreshAll({
1935
2195
  options: {
1936
- ...options.project !== void 0 ? { project: options.project } : {},
2196
+ ...options.project !== void 0 && options.project.length > 0 ? { project: options.project } : {},
1937
2197
  ...options.force === true ? { force: true } : {},
1938
2198
  ...options.dryRun === true ? { dryRun: true } : {}
1939
2199
  },
@@ -1991,7 +2251,7 @@ async function assertWorkspaceInitialized6(basouRoot) {
1991
2251
  try {
1992
2252
  await assertBasouRootSafe7(basouRoot);
1993
2253
  } catch (error) {
1994
- if (findErrorCode6(error, "ENOENT")) {
2254
+ if (findErrorCode7(error, "ENOENT")) {
1995
2255
  throw new Error("Workspace not initialized. Run 'basou init' first.");
1996
2256
  }
1997
2257
  throw error;
@@ -2000,8 +2260,8 @@ async function assertWorkspaceInitialized6(basouRoot) {
2000
2260
 
2001
2261
  // src/commands/run.ts
2002
2262
  import { mkdir as mkdir2 } from "fs/promises";
2003
- import { homedir as homedir3 } from "os";
2004
- import { join as join4 } from "path";
2263
+ import { homedir as homedir4 } from "os";
2264
+ import { join as join5 } from "path";
2005
2265
  import {
2006
2266
  assertBasouRootSafe as assertBasouRootSafe8,
2007
2267
  basouPaths as basouPaths8,
@@ -2053,10 +2313,10 @@ async function runClaudeCode(args, options, ctx = {}) {
2053
2313
  await assertBasouRootSafe8(paths.root);
2054
2314
  const manifest = await readManifest4(paths);
2055
2315
  const sessionId = prefixedUlid4("ses");
2056
- const sessionDir = join4(paths.sessions, sessionId);
2316
+ const sessionDir = join5(paths.sessions, sessionId);
2057
2317
  await mkdir2(sessionDir, { recursive: true });
2058
2318
  const startedAt = now().toISOString();
2059
- const sessionYamlPath = join4(sessionDir, "session.yaml");
2319
+ const sessionYamlPath = join5(sessionDir, "session.yaml");
2060
2320
  const session = buildInitialSession2({
2061
2321
  id: sessionId,
2062
2322
  command,
@@ -2177,7 +2437,7 @@ async function runClaudeCode(args, options, ctx = {}) {
2177
2437
  const rawRelated = computeRelatedFiles(preSnapshot, postSnapshot, diff);
2178
2438
  const relatedFiles = sanitizeRelatedFiles(rawRelated, {
2179
2439
  workingDirectory: repoRoot,
2180
- homedir: homedir3()
2440
+ homedir: homedir4()
2181
2441
  }).sanitized;
2182
2442
  const finalStatus = decideFinalStatus2(result, signalReceived);
2183
2443
  await appendEvent2(sessionDir, {
@@ -2321,7 +2581,7 @@ function buildInitialSession2(input) {
2321
2581
  source: { ...claudeCodeAdapterMetadata },
2322
2582
  started_at: input.startedAt,
2323
2583
  status: "initialized",
2324
- working_directory: sanitizeWorkingDirectory2(input.cwd, { homedir: homedir3() }),
2584
+ working_directory: sanitizeWorkingDirectory2(input.cwd, { homedir: homedir4() }),
2325
2585
  invocation: {
2326
2586
  command: input.command,
2327
2587
  args: [...input.args],
@@ -2394,13 +2654,13 @@ async function resolveRepositoryRootForRun(cwd) {
2394
2654
 
2395
2655
  // src/commands/session.ts
2396
2656
  import { readFile as readFile2 } from "fs/promises";
2397
- import { basename as basename3, isAbsolute, join as join5, relative } from "path";
2657
+ import { basename as basename3, isAbsolute, join as join6, relative as relative2 } from "path";
2398
2658
  import {
2399
2659
  acquireLock as acquireLock2,
2400
2660
  appendEventToExistingSession as appendEventToExistingSession2,
2401
2661
  assertBasouRootSafe as assertBasouRootSafe9,
2402
2662
  basouPaths as basouPaths9,
2403
- findErrorCode as findErrorCode7,
2663
+ findErrorCode as findErrorCode8,
2404
2664
  importSessionFromJson as importSessionFromJson2,
2405
2665
  loadSessionEntries,
2406
2666
  readAllEvents,
@@ -2414,7 +2674,7 @@ import {
2414
2674
  SessionStatusSchema,
2415
2675
  sessionWorkStatsFromEvents
2416
2676
  } from "@basou/core";
2417
- import { InvalidArgumentError as InvalidArgumentError2 } from "commander";
2677
+ import { InvalidArgumentError as InvalidArgumentError3 } from "commander";
2418
2678
 
2419
2679
  // src/lib/format-duration.ts
2420
2680
  function formatDurationMs(ms) {
@@ -2521,14 +2781,14 @@ async function doRunSessionShow(idInput, options, ctx) {
2521
2781
  const paths = basouPaths9(repositoryRoot);
2522
2782
  await assertWorkspaceInitialized7(paths.root);
2523
2783
  const sessionId = await resolveSessionId2(paths, idInput);
2524
- const sessionDir = join5(paths.sessions, sessionId);
2525
- const sessionYamlPath = join5(sessionDir, "session.yaml");
2784
+ const sessionDir = join6(paths.sessions, sessionId);
2785
+ const sessionYamlPath = join6(sessionDir, "session.yaml");
2526
2786
  let session;
2527
2787
  try {
2528
2788
  const raw = await readYamlFile4(sessionYamlPath);
2529
2789
  session = SessionSchema3.parse(raw);
2530
2790
  } catch (error) {
2531
- if (findErrorCode7(error, "ENOENT")) {
2791
+ if (findErrorCode8(error, "ENOENT")) {
2532
2792
  throw new Error(`Session not found: ${idInput}`);
2533
2793
  }
2534
2794
  throw new Error("Failed to read session", { cause: error });
@@ -2632,6 +2892,9 @@ function formatSessionWork(session, events, now) {
2632
2892
  parts.push(`${w.commandCount} cmd / ${w.fileChangedCount} files / ${w.decisionCount} dec`);
2633
2893
  const activeBasis = w.activeTimeBasis === "engaged-turns" ? "turns" : "events";
2634
2894
  parts.push(`active ${formatDurationMs(w.activeTimeMs)} (${activeBasis})`);
2895
+ if (w.availability.machineActive) {
2896
+ parts.push(`machine ${formatDurationMs(w.machineActiveTimeMs)}`);
2897
+ }
2635
2898
  parts.push(`span ${formatDurationMs(w.sessionSpanMs)}${w.open ? " (open)" : ""}`);
2636
2899
  parts.push(
2637
2900
  w.availability.commandTime ? `command ${formatDurationMs(w.commandTimeMs)}` : "command n/a (import)"
@@ -2645,7 +2908,7 @@ function formatWorkingDir(workingDir, repositoryRoot, options) {
2645
2908
  return workingDir;
2646
2909
  }
2647
2910
  if (workingDir === repositoryRoot) return "<repository_root>";
2648
- const rel = relative(repositoryRoot, workingDir);
2911
+ const rel = relative2(repositoryRoot, workingDir);
2649
2912
  if (rel.length === 0 || rel === ".") return "<repository_root>";
2650
2913
  if (rel.startsWith("..")) return rel;
2651
2914
  return `./${rel}`;
@@ -2775,7 +3038,7 @@ async function assertWorkspaceInitialized7(basouRoot) {
2775
3038
  try {
2776
3039
  await assertBasouRootSafe9(basouRoot);
2777
3040
  } catch (error) {
2778
- if (findErrorCode7(error, "ENOENT")) {
3041
+ if (findErrorCode8(error, "ENOENT")) {
2779
3042
  throw new Error("Workspace not initialized. Run 'basou init' first.");
2780
3043
  }
2781
3044
  throw error;
@@ -2844,10 +3107,10 @@ async function readInputFile(path) {
2844
3107
  try {
2845
3108
  return await readFile2(path, "utf8");
2846
3109
  } catch (error) {
2847
- if (findErrorCode7(error, "ENOENT")) {
3110
+ if (findErrorCode8(error, "ENOENT")) {
2848
3111
  throw new Error("Import source not found", { cause: error });
2849
3112
  }
2850
- if (findErrorCode7(error, "EISDIR")) {
3113
+ if (findErrorCode8(error, "EISDIR")) {
2851
3114
  throw new Error("Import source is not a file", { cause: error });
2852
3115
  }
2853
3116
  throw new Error("Failed to read import source", { cause: error });
@@ -2862,19 +3125,19 @@ function parseJsonStrict(body) {
2862
3125
  }
2863
3126
  function parseImportFormat(raw) {
2864
3127
  if (raw !== "json") {
2865
- throw new InvalidArgumentError2(`Unsupported format: ${raw}. Valid values: json`);
3128
+ throw new InvalidArgumentError3(`Unsupported format: ${raw}. Valid values: json`);
2866
3129
  }
2867
3130
  return "json";
2868
3131
  }
2869
3132
  function parseLabelOverride(raw) {
2870
3133
  if (raw.length === 0) {
2871
- throw new InvalidArgumentError2("Label must not be empty");
3134
+ throw new InvalidArgumentError3("Label must not be empty");
2872
3135
  }
2873
3136
  return raw;
2874
3137
  }
2875
3138
  function parseTaskIdOverride(raw) {
2876
3139
  if (raw.length === 0) {
2877
- throw new InvalidArgumentError2("Task id is empty");
3140
+ throw new InvalidArgumentError3("Task id is empty");
2878
3141
  }
2879
3142
  return raw;
2880
3143
  }
@@ -2961,10 +3224,10 @@ async function readNoteFile(path) {
2961
3224
  try {
2962
3225
  return await readFile2(path, "utf8");
2963
3226
  } catch (error) {
2964
- if (findErrorCode7(error, "ENOENT")) {
3227
+ if (findErrorCode8(error, "ENOENT")) {
2965
3228
  throw new Error("Note source not found", { cause: error });
2966
3229
  }
2967
- if (findErrorCode7(error, "EISDIR")) {
3230
+ if (findErrorCode8(error, "EISDIR")) {
2968
3231
  throw new Error("Note source is not a file", { cause: error });
2969
3232
  }
2970
3233
  throw new Error("Failed to read note source", { cause: error });
@@ -2972,7 +3235,7 @@ async function readNoteFile(path) {
2972
3235
  }
2973
3236
  function parseNoteBodyOption(raw) {
2974
3237
  if (raw.length === 0) {
2975
- throw new InvalidArgumentError2("--body must not be empty");
3238
+ throw new InvalidArgumentError3("--body must not be empty");
2976
3239
  }
2977
3240
  return raw;
2978
3241
  }
@@ -2998,7 +3261,7 @@ import {
2998
3261
  assertBasouRootSafe as assertBasouRootSafe10,
2999
3262
  basouPaths as basouPaths10,
3000
3263
  computeWorkStats,
3001
- findErrorCode as findErrorCode8,
3264
+ findErrorCode as findErrorCode9,
3002
3265
  resolveRepositoryRoot as resolveRepositoryRoot11
3003
3266
  } from "@basou/core";
3004
3267
  function registerStatsCommand(program2) {
@@ -3059,6 +3322,12 @@ function printStatsText(result, bySource, byDay) {
3059
3322
  ` Summed: ${formatDurationMs(t.activeTimeMs)} (per-session sum; concurrent sessions double-counted)`
3060
3323
  );
3061
3324
  }
3325
+ if (t.machineActiveAvailable) {
3326
+ const machineSessions = result.sessions.filter((s) => s.availability.machineActive).length;
3327
+ console.log(
3328
+ ` Model working: ${formatDurationMs(t.machineActiveTimeMs)} (model compute, subset of active; Codex turn duration on ${machineSessions} of ${t.sessionCount} sessions; summed, not wall-clock-deduped)`
3329
+ );
3330
+ }
3062
3331
  const openPart = t.openSessionCount > 0 ? `; ${t.openSessionCount} open counted to now` : "";
3063
3332
  console.log(
3064
3333
  ` Span: ${formatDurationMs(t.sessionSpanMs)} (total elapsed${openPart})`
@@ -3078,8 +3347,9 @@ function printStatsText(result, bySource, byDay) {
3078
3347
  console.log("");
3079
3348
  console.log("By day (billable time x volume):");
3080
3349
  for (const d of result.byDay) {
3350
+ const machine = d.machineActiveTimeMs > 0 ? ` (model ${formatDurationMs(d.machineActiveTimeMs)})` : "";
3081
3351
  console.log(
3082
- ` ${d.date}: ${formatDurationMs(d.billableActiveTimeMs)} active, ${formatInt(d.tokens.output)} out tok, ${d.commandCount} cmd / ${d.fileChangedCount} files / ${d.decisionCount} dec`
3352
+ ` ${d.date}: ${formatDurationMs(d.billableActiveTimeMs)} active${machine}, ${formatInt(d.tokens.output)} out tok, ${d.commandCount} cmd / ${d.fileChangedCount} files / ${d.decisionCount} dec`
3083
3353
  );
3084
3354
  }
3085
3355
  }
@@ -3087,7 +3357,8 @@ function printStatsText(result, bySource, byDay) {
3087
3357
  function describeSource(s) {
3088
3358
  const cmd = s.commandTimeReliable ? formatDurationMs(s.commandTimeMs) : "n/a";
3089
3359
  const tokens = s.tokensAvailable ? `${formatInt(s.tokens.output)} out tok` : "no tokens";
3090
- return `${s.sessionCount} sessions, ${tokens}, active ${formatDurationMs(s.activeTimeMs)}, command ${cmd}`;
3360
+ const machine = s.machineActiveAvailable ? `, model ${formatDurationMs(s.machineActiveTimeMs)}` : "";
3361
+ return `${s.sessionCount} sessions, ${tokens}, active ${formatDurationMs(s.activeTimeMs)}${machine}, command ${cmd}`;
3091
3362
  }
3092
3363
  function formatInt(n) {
3093
3364
  return n.toLocaleString("en-US");
@@ -3108,7 +3379,7 @@ async function assertWorkspaceInitialized8(basouRoot) {
3108
3379
  try {
3109
3380
  await assertBasouRootSafe10(basouRoot);
3110
3381
  } catch (error) {
3111
- if (findErrorCode8(error, "ENOENT")) {
3382
+ if (findErrorCode9(error, "ENOENT")) {
3112
3383
  throw new Error("Workspace not initialized. Run 'basou init' first.");
3113
3384
  }
3114
3385
  throw error;
@@ -3120,7 +3391,7 @@ import {
3120
3391
  assertBasouRootSafe as assertBasouRootSafe11,
3121
3392
  basouPaths as basouPaths11,
3122
3393
  buildStatusSnapshot,
3123
- findErrorCode as findErrorCode9,
3394
+ findErrorCode as findErrorCode10,
3124
3395
  readManifest as readManifest6,
3125
3396
  resolveRepositoryRoot as resolveRepositoryRoot12,
3126
3397
  writeStatus
@@ -3145,7 +3416,7 @@ async function doRunStatus(options, ctx) {
3145
3416
  try {
3146
3417
  await assertBasouRootSafe11(paths.root);
3147
3418
  } catch (error) {
3148
- if (findErrorCode9(error, "ENOENT")) {
3419
+ if (findErrorCode10(error, "ENOENT")) {
3149
3420
  throw new Error("Workspace not initialized. Run 'basou init' first.");
3150
3421
  }
3151
3422
  throw error;
@@ -3154,7 +3425,7 @@ async function doRunStatus(options, ctx) {
3154
3425
  try {
3155
3426
  manifest = await readManifest6(paths);
3156
3427
  } catch (error) {
3157
- if (findErrorCode9(error, "ENOENT")) {
3428
+ if (findErrorCode10(error, "ENOENT")) {
3158
3429
  throw new Error("Workspace not initialized. Run 'basou init' first.");
3159
3430
  }
3160
3431
  throw new Error("Failed to read workspace manifest", { cause: error });
@@ -3191,7 +3462,7 @@ async function resolveRepositoryRootForStatus(cwd) {
3191
3462
 
3192
3463
  // src/commands/task.ts
3193
3464
  import { readFile as readFile3 } from "fs/promises";
3194
- import { join as join6 } from "path";
3465
+ import { join as join7 } from "path";
3195
3466
  import {
3196
3467
  archiveTask,
3197
3468
  assertBasouRootSafe as assertBasouRootSafe12,
@@ -3200,7 +3471,7 @@ import {
3200
3471
  deleteTask,
3201
3472
  editTask,
3202
3473
  enumerateArchivedTaskIds,
3203
- findErrorCode as findErrorCode10,
3474
+ findErrorCode as findErrorCode11,
3204
3475
  loadSessionEntries as loadSessionEntries2,
3205
3476
  loadTaskEntries,
3206
3477
  prefixedUlid as prefixedUlid5,
@@ -3218,7 +3489,7 @@ import {
3218
3489
  TaskWriteAfterEventError,
3219
3490
  updateTaskStatusWithEvent
3220
3491
  } from "@basou/core";
3221
- import { InvalidArgumentError as InvalidArgumentError3 } from "commander";
3492
+ import { InvalidArgumentError as InvalidArgumentError4 } from "commander";
3222
3493
  var STATUS_VALUES3 = TaskStatusSchema.options;
3223
3494
  function registerTaskCommand(program2) {
3224
3495
  const task = program2.command("task").description("Manage Basou tasks (purpose units that span sessions)");
@@ -3518,7 +3789,7 @@ async function doRunTaskShow(idInput, options, ctx) {
3518
3789
  const events = [];
3519
3790
  const linkedSessionIds = new Set(doc.task.task.linked_sessions);
3520
3791
  for (const s of sessions) {
3521
- const sessionDir = join6(paths.sessions, s.sessionId);
3792
+ const sessionDir = join7(paths.sessions, s.sessionId);
3522
3793
  try {
3523
3794
  for await (const ev of replayEvents2(sessionDir, {
3524
3795
  onWarning: (w) => printReplayWarning(w, s.sessionId)
@@ -4147,20 +4418,20 @@ async function readSingleLineFromStdin() {
4147
4418
  }
4148
4419
  function parseTitle2(raw) {
4149
4420
  if (raw.length === 0) {
4150
- throw new InvalidArgumentError3("Title must not be empty");
4421
+ throw new InvalidArgumentError4("Title must not be empty");
4151
4422
  }
4152
4423
  return raw;
4153
4424
  }
4154
4425
  function parseLabel(raw) {
4155
4426
  if (raw.length === 0) {
4156
- throw new InvalidArgumentError3("Label must not be empty");
4427
+ throw new InvalidArgumentError4("Label must not be empty");
4157
4428
  }
4158
4429
  return raw;
4159
4430
  }
4160
4431
  function parseInitialTaskStatus(raw) {
4161
4432
  const result = TaskStatusSchema.safeParse(raw);
4162
4433
  if (!result.success) {
4163
- throw new InvalidArgumentError3(
4434
+ throw new InvalidArgumentError4(
4164
4435
  `Initial task status must be one of: ${STATUS_VALUES3.join(", ")}`
4165
4436
  );
4166
4437
  }
@@ -4169,7 +4440,7 @@ function parseInitialTaskStatus(raw) {
4169
4440
  var ISO_DATE_RE = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:\d{2})$/;
4170
4441
  function parseIsoTimestampOption(raw) {
4171
4442
  if (!ISO_DATE_RE.test(raw) || Number.isNaN(Date.parse(raw))) {
4172
- throw new InvalidArgumentError3(
4443
+ throw new InvalidArgumentError4(
4173
4444
  "Invalid --completed-at value; expected ISO-8601 timestamp like 2026-05-10T12:34:56+09:00"
4174
4445
  );
4175
4446
  }
@@ -4178,7 +4449,7 @@ function parseIsoTimestampOption(raw) {
4178
4449
  function parseTaskStatusFilter(raw) {
4179
4450
  const result = TaskStatusSchema.safeParse(raw);
4180
4451
  if (!result.success) {
4181
- throw new InvalidArgumentError3(
4452
+ throw new InvalidArgumentError4(
4182
4453
  `Invalid task status: ${raw}. Valid values: ${STATUS_VALUES3.join(", ")}`
4183
4454
  );
4184
4455
  }
@@ -4193,14 +4464,14 @@ function parseTaskStatusPositional(raw) {
4193
4464
  }
4194
4465
  function parseDescriptionOption(raw) {
4195
4466
  if (raw.length === 0) {
4196
- throw new InvalidArgumentError3("Description must not be empty");
4467
+ throw new InvalidArgumentError4("Description must not be empty");
4197
4468
  }
4198
4469
  return raw;
4199
4470
  }
4200
4471
  function parsePositiveInt2(raw) {
4201
4472
  const n = Number.parseInt(raw, 10);
4202
4473
  if (!Number.isInteger(n) || n < 1 || raw.trim() !== String(n)) {
4203
- throw new InvalidArgumentError3(`Invalid number: ${raw}`);
4474
+ throw new InvalidArgumentError4(`Invalid number: ${raw}`);
4204
4475
  }
4205
4476
  return n;
4206
4477
  }
@@ -4208,10 +4479,10 @@ async function readDescriptionFile(path) {
4208
4479
  try {
4209
4480
  return await readFile3(path, "utf8");
4210
4481
  } catch (error) {
4211
- if (findErrorCode10(error, "ENOENT")) {
4482
+ if (findErrorCode11(error, "ENOENT")) {
4212
4483
  throw new Error("Description source not found", { cause: error });
4213
4484
  }
4214
- if (findErrorCode10(error, "EISDIR")) {
4485
+ if (findErrorCode11(error, "EISDIR")) {
4215
4486
  throw new Error("Description source is not a file", { cause: error });
4216
4487
  }
4217
4488
  throw new Error("Failed to read description source", { cause: error });
@@ -4234,7 +4505,7 @@ async function assertWorkspaceInitialized9(basouRoot) {
4234
4505
  try {
4235
4506
  await assertBasouRootSafe12(basouRoot);
4236
4507
  } catch (error) {
4237
- if (findErrorCode10(error, "ENOENT")) {
4508
+ if (findErrorCode11(error, "ENOENT")) {
4238
4509
  throw new Error("Workspace not initialized. Run 'basou init' first.");
4239
4510
  }
4240
4511
  throw error;
@@ -4322,16 +4593,16 @@ function maxLen3(values, floor) {
4322
4593
 
4323
4594
  // src/commands/view.ts
4324
4595
  import { spawn } from "child_process";
4325
- import { assertBasouRootSafe as assertBasouRootSafe13, basouPaths as basouPaths13, findErrorCode as findErrorCode12, resolveRepositoryRoot as resolveRepositoryRoot14 } from "@basou/core";
4326
- import { InvalidArgumentError as InvalidArgumentError4 } from "commander";
4596
+ import { assertBasouRootSafe as assertBasouRootSafe13, basouPaths as basouPaths13, findErrorCode as findErrorCode13, resolveRepositoryRoot as resolveRepositoryRoot14 } from "@basou/core";
4597
+ import { InvalidArgumentError as InvalidArgumentError5 } from "commander";
4327
4598
 
4328
4599
  // src/lib/view-server.ts
4329
4600
  import { createServer } from "http";
4330
- import { join as join7 } from "path";
4601
+ import { join as join8 } from "path";
4331
4602
  import {
4332
4603
  computeWorkStats as computeWorkStats2,
4333
4604
  enumerateApprovals as enumerateApprovals2,
4334
- findErrorCode as findErrorCode11,
4605
+ findErrorCode as findErrorCode12,
4335
4606
  isLazyExpired as isLazyExpired2,
4336
4607
  loadApproval as loadApproval2,
4337
4608
  loadSessionEntries as loadSessionEntries3,
@@ -4396,7 +4667,7 @@ var VIEW_HTML = `<!doctype html>
4396
4667
  <body>
4397
4668
  <header>
4398
4669
  <h1>basou view</h1>
4399
- <input type="text" id="project" placeholder="project path" />
4670
+ <input type="text" id="project" placeholder="source root (optional override)" />
4400
4671
  <button class="primary" id="btn-refresh">Refresh all</button>
4401
4672
  <button id="btn-import-claude">Import claude-code</button>
4402
4673
  <button id="btn-import-codex">Import codex</button>
@@ -4550,7 +4821,10 @@ var VIEW_HTML = `<!doctype html>
4550
4821
  detail.appendChild(el('p', { class: 'muted', text: 'Workspace not initialized.' }));
4551
4822
  return;
4552
4823
  }
4553
- $('project').value = $('project').value || d.repoRoot || '';
4824
+ // Leave the project field empty by default so refresh / import use the
4825
+ // manifest's import.source_roots (then the repo root) -- pre-filling the
4826
+ // repo root here would send it as an explicit --project and silently
4827
+ // override multi-root source roots. The field is an optional override.
4554
4828
  state.repoRoot = d.repoRoot || '';
4555
4829
  detail.appendChild(el('p', {}, [
4556
4830
  el('strong', { text: d.workspace.name }), ' ',
@@ -4620,6 +4894,10 @@ var VIEW_HTML = `<!doctype html>
4620
4894
  if (t.activeTimeMs !== t.billableActiveTimeMs) {
4621
4895
  timeRows.push(kvrow('summed', fmtDur(t.activeTimeMs) + ' (concurrent sessions double-counted)'));
4622
4896
  }
4897
+ if (t.machineActiveAvailable) {
4898
+ var machineSessions = sessions.filter(function (s) { return s.availability && s.availability.machineActive; }).length;
4899
+ timeRows.push(kvrow('model working', fmtDur(t.machineActiveTimeMs) + ' (model compute, subset of active; Codex turn duration on ' + machineSessions + ' of ' + t.sessionCount + ' sessions; not wall-clock-deduped)'));
4900
+ }
4623
4901
  timeRows.push(kvrow('span', fmtDur(t.sessionSpanMs) + (t.openSessionCount > 0 ? ' (' + t.openSessionCount + ' open)' : '')));
4624
4902
  timeRows.push(kvrow('command', fmtDur(t.commandTimeMs) + (t.commandTimeReliable ? '' : ' (some sessions report 0)')));
4625
4903
  detail.appendChild(el('table', { class: 'kv' }, [el('tbody', {}, timeRows)]));
@@ -4627,16 +4905,18 @@ var VIEW_HTML = `<!doctype html>
4627
4905
  detail.appendChild(el('h3', { text: 'By source' }));
4628
4906
  d.bySource.forEach(function (s) {
4629
4907
  var cmd = s.commandTimeReliable ? fmtDur(s.commandTimeMs) : 'n/a';
4908
+ var machine = s.machineActiveAvailable ? ', model ' + fmtDur(s.machineActiveTimeMs) : '';
4630
4909
  detail.appendChild(el('div', { class: 'row' }, [
4631
- el('span', { text: s.sourceKind + ': ' + s.sessionCount + ' sessions, ' + numfmt(s.tokens.output) + ' out tok, active ' + fmtDur(s.activeTimeMs) + ', command ' + cmd })
4910
+ el('span', { text: s.sourceKind + ': ' + s.sessionCount + ' sessions, ' + numfmt(s.tokens.output) + ' out tok, active ' + fmtDur(s.activeTimeMs) + machine + ', command ' + cmd })
4632
4911
  ]));
4633
4912
  });
4634
4913
  }
4635
4914
  if (d.byDay && d.byDay.length) {
4636
4915
  detail.appendChild(el('h3', { text: 'By day (billable time x volume)' }));
4637
4916
  d.byDay.forEach(function (day) {
4917
+ var dayMachine = day.machineActiveTimeMs > 0 ? ' (model ' + fmtDur(day.machineActiveTimeMs) + ')' : '';
4638
4918
  detail.appendChild(el('div', { class: 'row' }, [
4639
- el('span', { text: day.date + ': ' + fmtDur(day.billableActiveTimeMs) + ' active, ' + numfmt(day.tokens.output) + ' out tok, ' + day.commandCount + ' cmd / ' + day.fileChangedCount + ' files / ' + day.decisionCount + ' dec' })
4919
+ el('span', { text: day.date + ': ' + fmtDur(day.billableActiveTimeMs) + ' active' + dayMachine + ', ' + numfmt(day.tokens.output) + ' out tok, ' + day.commandCount + ' cmd / ' + day.fileChangedCount + ' files / ' + day.decisionCount + ' dec' })
4640
4920
  ]));
4641
4921
  });
4642
4922
  }
@@ -4795,7 +5075,7 @@ function startViewServer(opts) {
4795
5075
  };
4796
5076
  let boundPort = port;
4797
5077
  const getPort = () => boundPort;
4798
- return new Promise((resolve, reject) => {
5078
+ return new Promise((resolve3, reject) => {
4799
5079
  const server = createServer((req, res) => {
4800
5080
  handleRequest(req, res, deps, getPort, runExclusive).catch((error) => {
4801
5081
  sendError(res, error instanceof HttpError ? error.status : 500, pathlessMessage(error));
@@ -4806,7 +5086,7 @@ function startViewServer(opts) {
4806
5086
  const address = server.address();
4807
5087
  boundPort = isAddressInfo(address) ? address.port : port;
4808
5088
  server.off("error", reject);
4809
- resolve({
5089
+ resolve3({
4810
5090
  url: `http://${host}:${boundPort}`,
4811
5091
  port: boundPort,
4812
5092
  close: () => closeServer(server)
@@ -4818,8 +5098,8 @@ function isAddressInfo(value) {
4818
5098
  return value !== null && typeof value === "object";
4819
5099
  }
4820
5100
  function closeServer(server) {
4821
- return new Promise((resolve) => {
4822
- server.close(() => resolve());
5101
+ return new Promise((resolve3) => {
5102
+ server.close(() => resolve3());
4823
5103
  server.closeAllConnections();
4824
5104
  });
4825
5105
  }
@@ -4924,7 +5204,7 @@ async function overview(deps) {
4924
5204
  try {
4925
5205
  manifest = await readManifest8(deps.paths);
4926
5206
  } catch (error) {
4927
- if (findErrorCode11(error, "ENOENT")) {
5207
+ if (findErrorCode12(error, "ENOENT")) {
4928
5208
  return { initialized: false, repoRoot: deps.repoRoot };
4929
5209
  }
4930
5210
  throw error;
@@ -4979,7 +5259,7 @@ async function sessionDetail(deps, sessionId) {
4979
5259
  throw error;
4980
5260
  }
4981
5261
  try {
4982
- const events = await readAllEvents2(join7(deps.paths.sessions, sessionId));
5262
+ const events = await readAllEvents2(join8(deps.paths.sessions, sessionId));
4983
5263
  return { session, events };
4984
5264
  } catch {
4985
5265
  return { session, events: [], degraded: true };
@@ -5034,11 +5314,16 @@ async function handoffView(deps) {
5034
5314
  }
5035
5315
  function readActionOptions(body) {
5036
5316
  const options = {};
5037
- if (typeof body.project === "string" && body.project.length > 0) options.project = body.project;
5317
+ const project = normalizeProject(body.project);
5318
+ if (project.length > 0) options.project = project;
5038
5319
  if (body.force === true) options.force = true;
5039
5320
  if (body.dryRun === true) options.dryRun = true;
5040
5321
  return options;
5041
5322
  }
5323
+ function normalizeProject(value) {
5324
+ const raw = Array.isArray(value) ? value : [value];
5325
+ return raw.filter((p) => typeof p === "string" && p.length > 0);
5326
+ }
5042
5327
  function hostAllowed(req, port) {
5043
5328
  const host = req.headers.host;
5044
5329
  return host === `127.0.0.1:${port}` || host === `localhost:${port}`;
@@ -5114,7 +5399,7 @@ var DEFAULT_PORT = 4319;
5114
5399
  function parsePort(value) {
5115
5400
  const port = Number.parseInt(value, 10);
5116
5401
  if (!Number.isInteger(port) || port < 1 || port > 65535) {
5117
- throw new InvalidArgumentError4("Port must be an integer between 1 and 65535.");
5402
+ throw new InvalidArgumentError5("Port must be an integer between 1 and 65535.");
5118
5403
  }
5119
5404
  return port;
5120
5405
  }
@@ -5166,7 +5451,7 @@ async function startListening(port, deps) {
5166
5451
  try {
5167
5452
  return await startViewServer({ port, deps });
5168
5453
  } catch (error) {
5169
- if (findErrorCode12(error, "EADDRINUSE")) {
5454
+ if (findErrorCode13(error, "EADDRINUSE")) {
5170
5455
  throw new Error(`Port ${port} is already in use. Pass --port <n> to choose another.`, {
5171
5456
  cause: error
5172
5457
  });
@@ -5189,7 +5474,7 @@ function openInBrowser(url, override) {
5189
5474
  }
5190
5475
  }
5191
5476
  function waitForShutdown(signal) {
5192
- return new Promise((resolve) => {
5477
+ return new Promise((resolve3) => {
5193
5478
  const cleanup = () => {
5194
5479
  process.off("SIGINT", onSignal);
5195
5480
  process.off("SIGTERM", onSignal);
@@ -5197,18 +5482,18 @@ function waitForShutdown(signal) {
5197
5482
  };
5198
5483
  const onSignal = () => {
5199
5484
  cleanup();
5200
- resolve();
5485
+ resolve3();
5201
5486
  };
5202
5487
  const onAbort = () => {
5203
5488
  cleanup();
5204
- resolve();
5489
+ resolve3();
5205
5490
  };
5206
5491
  process.on("SIGINT", onSignal);
5207
5492
  process.on("SIGTERM", onSignal);
5208
5493
  if (signal !== void 0) {
5209
5494
  if (signal.aborted) {
5210
5495
  cleanup();
5211
- resolve();
5496
+ resolve3();
5212
5497
  return;
5213
5498
  }
5214
5499
  signal.addEventListener("abort", onAbort);
@@ -5231,7 +5516,7 @@ async function assertWorkspaceInitialized10(basouRoot) {
5231
5516
  try {
5232
5517
  await assertBasouRootSafe13(basouRoot);
5233
5518
  } catch (error) {
5234
- if (findErrorCode12(error, "ENOENT")) {
5519
+ if (findErrorCode13(error, "ENOENT")) {
5235
5520
  throw new Error("Workspace not initialized. Run 'basou init' first.");
5236
5521
  }
5237
5522
  throw error;