@basou/cli 0.3.1 → 0.5.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
@@ -1,29 +1,5 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- // src/index.ts
4
- import { createRequire } from "module";
5
- import { Command } from "commander";
6
-
7
- // src/commands/approval.ts
8
- import { unlink } from "fs/promises";
9
- import { join } from "path";
10
- import {
11
- ApprovalSchema,
12
- ApprovalStatusSchema,
13
- appendEvent,
14
- assertBasouRootSafe,
15
- basouPaths,
16
- enumerateApprovals,
17
- findErrorCode,
18
- isLazyExpired,
19
- linkYamlFile,
20
- loadApproval,
21
- prefixedUlid,
22
- readYamlFile,
23
- replayEvents,
24
- resolveRepositoryRoot
25
- } from "@basou/core";
26
-
27
3
  // src/lib/error-render.ts
28
4
  import {
29
5
  FailedToFinalizeError
@@ -129,7 +105,29 @@ function printTaskSkip(taskId, reason) {
129
105
  console.error(`Skipped ${shortTaskId(taskId)}: ${reason}`);
130
106
  }
131
107
 
108
+ // src/program.ts
109
+ import { createRequire } from "module";
110
+ import { Command } from "commander";
111
+
132
112
  // src/commands/approval.ts
113
+ import { unlink } from "fs/promises";
114
+ import { join } from "path";
115
+ import {
116
+ ApprovalSchema,
117
+ ApprovalStatusSchema,
118
+ appendEvent,
119
+ assertBasouRootSafe,
120
+ basouPaths,
121
+ enumerateApprovals,
122
+ findErrorCode,
123
+ isLazyExpired,
124
+ linkYamlFile,
125
+ loadApproval,
126
+ prefixedUlid,
127
+ readYamlFile,
128
+ replayEvents,
129
+ resolveRepositoryRoot
130
+ } from "@basou/core";
133
131
  var APPR_PREFIX = "appr_";
134
132
  var SHORT_ID_BASE_LEN = 6;
135
133
  var SHORT_ID_MAX_LEN = 26;
@@ -946,10 +944,9 @@ import { mkdir } from "fs/promises";
946
944
  import { homedir } from "os";
947
945
  import { join as join2 } from "path";
948
946
  import {
949
- ChildProcessRunner,
950
- SessionSchema,
951
947
  assertBasouRootSafe as assertBasouRootSafe4,
952
948
  basouPaths as basouPaths4,
949
+ ChildProcessRunner,
953
950
  appendEvent as coreAppendEvent,
954
951
  getSnapshot,
955
952
  overwriteYamlFile,
@@ -958,6 +955,7 @@ import {
958
955
  readManifest as readManifest2,
959
956
  readYamlFile as readYamlFile2,
960
957
  resolveRepositoryRoot as resolveRepositoryRoot4,
958
+ SessionSchema,
961
959
  sanitizeWorkingDirectory,
962
960
  writeYamlFile
963
961
  } from "@basou/core";
@@ -1327,13 +1325,396 @@ async function assertWorkspaceInitialized4(basouRoot) {
1327
1325
  }
1328
1326
  }
1329
1327
 
1328
+ // src/commands/import.ts
1329
+ import { createReadStream } from "fs";
1330
+ import { readdir, readFile, rm } from "fs/promises";
1331
+ import { homedir as homedir2 } from "os";
1332
+ import { basename, join as join3 } from "path";
1333
+ import { createInterface } from "readline";
1334
+ import {
1335
+ assertBasouRootSafe as assertBasouRootSafe6,
1336
+ basouPaths as basouPaths6,
1337
+ CLAUDE_IMPORT_SOURCE,
1338
+ CODEX_IMPORT_SOURCE,
1339
+ claudeTranscriptToImportPayload,
1340
+ codexRolloutToImportPayload,
1341
+ enumerateSessionDirs,
1342
+ findErrorCode as findErrorCode5,
1343
+ importSessionFromJson,
1344
+ readManifest as readManifest3,
1345
+ readSessionYaml,
1346
+ resolveRepositoryRoot as resolveRepositoryRoot6,
1347
+ SessionImportPayloadSchema
1348
+ } from "@basou/core";
1349
+ var SES_PREFIX2 = "ses_";
1350
+ var SHORT_ID_LEN2 = 6;
1351
+ function registerImportCommand(program2) {
1352
+ const importCmd = program2.command("import").description("Import provenance from an external AI tool's native logs");
1353
+ importCmd.command("claude-code").description("Derive Basou sessions from Claude Code native transcripts (~/.claude/projects)").option(
1354
+ "--project <path>",
1355
+ "Source project path whose transcripts to import (defaults to the current repository root)"
1356
+ ).option("--session <id>", "Import a single transcript by its Claude session id").option("--all", "Import every transcript found for the project").option(
1357
+ "--force",
1358
+ "Re-import sessions already imported: delete and replace them instead of skipping"
1359
+ ).option("--dry-run", "Validate and preview only; do not write to disk").option("--json", "Output the result as JSON").option("-v, --verbose", "Show error causes").action(async (options) => {
1360
+ await runImportClaudeCode(options);
1361
+ });
1362
+ importCmd.command("codex").description("Derive Basou sessions from OpenAI Codex native rollout logs (~/.codex/sessions)").option(
1363
+ "--project <path>",
1364
+ "Source project path whose rollouts to import (defaults to the current repository root)"
1365
+ ).option("--session <id>", "Import a single rollout by its Codex session id").option("--all", "Import every rollout found for the project").option(
1366
+ "--force",
1367
+ "Re-import sessions already imported: delete and replace them instead of skipping"
1368
+ ).option("--dry-run", "Validate and preview only; do not write to disk").option("--json", "Output the result as JSON").option("-v, --verbose", "Show error causes").action(async (options) => {
1369
+ await runImportCodex(options);
1370
+ });
1371
+ }
1372
+ async function runImportClaudeCode(options, ctx = {}) {
1373
+ try {
1374
+ await doRunImportClaudeCode(options, ctx);
1375
+ } catch (error) {
1376
+ renderCliError(error, { verbose: isVerbose(options) });
1377
+ process.exitCode = 1;
1378
+ }
1379
+ }
1380
+ async function runImportCodex(options, ctx = {}) {
1381
+ try {
1382
+ await doRunImportCodex(options, ctx);
1383
+ } catch (error) {
1384
+ renderCliError(error, { verbose: isVerbose(options) });
1385
+ process.exitCode = 1;
1386
+ }
1387
+ }
1388
+ async function doRunImportClaudeCode(options, ctx) {
1389
+ assertSelector(options);
1390
+ const { repositoryRoot, paths, manifest } = await resolveImportTarget(ctx);
1391
+ const projectPath = options.project ?? repositoryRoot;
1392
+ const projectsRoot = ctx.claudeProjectsDir ?? join3(homedir2(), ".claude", "projects");
1393
+ const transcriptDir = join3(projectsRoot, encodeProjectDir(projectPath));
1394
+ const files = await selectTranscriptFiles(transcriptDir, options);
1395
+ const candidates = files.map((file) => {
1396
+ const externalId = basename(file, ".jsonl");
1397
+ return {
1398
+ externalId,
1399
+ toPayload: async () => claudeTranscriptToImportPayload(await readJsonlRecords(file), {
1400
+ workspaceId: manifest.workspace.id,
1401
+ externalId
1402
+ })
1403
+ };
1404
+ });
1405
+ await importDerivedSessions(paths, manifest, options, CLAUDE_IMPORT_SOURCE, candidates);
1406
+ }
1407
+ async function doRunImportCodex(options, ctx) {
1408
+ assertSelector(options);
1409
+ const { repositoryRoot, paths, manifest } = await resolveImportTarget(ctx);
1410
+ const projectPath = options.project ?? repositoryRoot;
1411
+ const sessionsRoot = ctx.codexSessionsDir ?? join3(homedir2(), ".codex", "sessions");
1412
+ const rollouts = await discoverCodexRollouts(sessionsRoot, projectPath, options);
1413
+ const candidates = rollouts.map(({ file, externalId }) => ({
1414
+ externalId,
1415
+ toPayload: async () => codexRolloutToImportPayload(await readJsonlRecords(file), {
1416
+ workspaceId: manifest.workspace.id,
1417
+ externalId
1418
+ })
1419
+ }));
1420
+ await importDerivedSessions(paths, manifest, options, CODEX_IMPORT_SOURCE, candidates);
1421
+ }
1422
+ function assertSelector(options) {
1423
+ if (options.session === void 0 && options.all !== true) {
1424
+ throw new Error("Specify --session <id> or --all");
1425
+ }
1426
+ }
1427
+ async function resolveImportTarget(ctx) {
1428
+ const cwd = ctx.cwd ?? process.cwd();
1429
+ const repositoryRoot = await resolveRepositoryRootForImport(cwd);
1430
+ const paths = basouPaths6(repositoryRoot);
1431
+ await assertWorkspaceInitialized5(paths.root);
1432
+ const manifest = await readManifest3(paths);
1433
+ return { repositoryRoot, paths, manifest };
1434
+ }
1435
+ async function importDerivedSessions(paths, manifest, options, sourceKind, candidates) {
1436
+ const existingByExternalId = await loadExistingByExternalId(paths, sourceKind);
1437
+ const seenThisRun = /* @__PURE__ */ new Set();
1438
+ const results = [];
1439
+ let skippedNoAction = 0;
1440
+ let skippedExisting = 0;
1441
+ let replaced = 0;
1442
+ let sanitizedPaths = 0;
1443
+ for (const { externalId, toPayload } of candidates) {
1444
+ if (seenThisRun.has(externalId)) {
1445
+ skippedExisting++;
1446
+ continue;
1447
+ }
1448
+ const priorSessionIds = existingByExternalId.get(externalId) ?? [];
1449
+ if (priorSessionIds.length > 0 && options.force !== true) {
1450
+ skippedExisting++;
1451
+ continue;
1452
+ }
1453
+ const payload = await toPayload();
1454
+ if (payload === null) {
1455
+ skippedNoAction++;
1456
+ continue;
1457
+ }
1458
+ const parsed = SessionImportPayloadSchema.safeParse(payload);
1459
+ if (!parsed.success) {
1460
+ throw new Error("Invalid import payload", { cause: parsed.error });
1461
+ }
1462
+ if (parsed.data.schema_version !== "0.1.0") {
1463
+ throw new Error(`Unsupported import schema_version: ${parsed.data.schema_version}`);
1464
+ }
1465
+ if (priorSessionIds.length > 0 && options.force === true) {
1466
+ if (options.dryRun !== true) {
1467
+ for (const sid of priorSessionIds) {
1468
+ await rm(join3(paths.sessions, sid), { recursive: true, force: true });
1469
+ }
1470
+ }
1471
+ replaced++;
1472
+ }
1473
+ const result = await importSessionFromJson(paths, manifest, parsed.data, {
1474
+ dryRun: options.dryRun === true
1475
+ });
1476
+ results.push(result);
1477
+ seenThisRun.add(externalId);
1478
+ sanitizedPaths += result.pathSanitizeReport.relatedFiles + (result.pathSanitizeReport.workingDirectoryRewritten ? 1 : 0);
1479
+ }
1480
+ if (sanitizedPaths > 0) {
1481
+ console.error(`Imported sessions: ${sanitizedPaths} path(s) sanitized`);
1482
+ }
1483
+ printImportResult(options, results, { skippedNoAction, skippedExisting, replaced });
1484
+ }
1485
+ function encodeProjectDir(projectPath) {
1486
+ return projectPath.replaceAll("/", "-");
1487
+ }
1488
+ async function loadExistingByExternalId(paths, sourceKind) {
1489
+ const byExternalId = /* @__PURE__ */ new Map();
1490
+ const add = (externalId, sessionId) => {
1491
+ const list = byExternalId.get(externalId);
1492
+ if (list === void 0) byExternalId.set(externalId, [sessionId]);
1493
+ else list.push(sessionId);
1494
+ };
1495
+ let sessionIds;
1496
+ try {
1497
+ sessionIds = await enumerateSessionDirs(paths);
1498
+ } catch {
1499
+ return byExternalId;
1500
+ }
1501
+ for (const sessionId of sessionIds) {
1502
+ let session;
1503
+ try {
1504
+ session = await readSessionYaml(paths, sessionId);
1505
+ } catch {
1506
+ continue;
1507
+ }
1508
+ if (session.session.source.kind !== sourceKind) continue;
1509
+ const ext = session.session.source.external_id;
1510
+ if (typeof ext === "string" && ext.length > 0) {
1511
+ add(ext, sessionId);
1512
+ continue;
1513
+ }
1514
+ const label = session.session.label;
1515
+ const match = typeof label === "string" ? label.match(/^claude-code import (\S+)$/) : null;
1516
+ if (match?.[1] !== void 0) add(match[1], sessionId);
1517
+ }
1518
+ return byExternalId;
1519
+ }
1520
+ async function selectTranscriptFiles(transcriptDir, options) {
1521
+ if (options.session !== void 0) {
1522
+ return [join3(transcriptDir, `${options.session}.jsonl`)];
1523
+ }
1524
+ let entries;
1525
+ try {
1526
+ entries = await readdir(transcriptDir);
1527
+ } 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 });
1532
+ }
1533
+ return entries.filter((name) => name.endsWith(".jsonl")).sort().map((name) => join3(transcriptDir, name));
1534
+ }
1535
+ async function discoverCodexRollouts(sessionsRoot, projectPath, options) {
1536
+ const files = await findRolloutFiles(sessionsRoot);
1537
+ const matched = [];
1538
+ for (const file of files) {
1539
+ const meta = await readRolloutMeta(file);
1540
+ if (meta === void 0) continue;
1541
+ if (meta.cwd !== projectPath) continue;
1542
+ if (options.session !== void 0 && meta.id !== options.session) continue;
1543
+ matched.push({ file, externalId: meta.id });
1544
+ }
1545
+ if (options.session !== void 0 && matched.length === 0) {
1546
+ throw new Error("Codex rollout not found for session id in project");
1547
+ }
1548
+ return matched;
1549
+ }
1550
+ async function findRolloutFiles(sessionsRoot) {
1551
+ const found = [];
1552
+ const walk = async (dir, isRoot) => {
1553
+ let entries;
1554
+ try {
1555
+ entries = await readdir(dir, { withFileTypes: true });
1556
+ } catch (error) {
1557
+ if (findErrorCode5(error, "ENOENT")) {
1558
+ if (isRoot) {
1559
+ throw new Error("Codex sessions directory not found", { cause: error });
1560
+ }
1561
+ return;
1562
+ }
1563
+ throw new Error("Failed to read Codex sessions directory", { cause: error });
1564
+ }
1565
+ for (const entry of entries) {
1566
+ const full = join3(dir, entry.name);
1567
+ if (entry.isDirectory()) {
1568
+ await walk(full, false);
1569
+ } else if (entry.isFile() && entry.name.startsWith("rollout-") && entry.name.endsWith(".jsonl")) {
1570
+ found.push(full);
1571
+ }
1572
+ }
1573
+ };
1574
+ await walk(sessionsRoot, true);
1575
+ return found.sort();
1576
+ }
1577
+ async function readRolloutMeta(file) {
1578
+ const firstLine = await readFirstLine(file);
1579
+ if (firstLine === void 0) return void 0;
1580
+ let parsed;
1581
+ try {
1582
+ parsed = JSON.parse(firstLine);
1583
+ } catch {
1584
+ return void 0;
1585
+ }
1586
+ if (!isObject(parsed) || parsed.type !== "session_meta") return void 0;
1587
+ const payload = isObject(parsed.payload) ? parsed.payload : void 0;
1588
+ if (payload === void 0) return void 0;
1589
+ const id = payload.id;
1590
+ const cwd = payload.cwd;
1591
+ if (typeof id !== "string" || id.length === 0) return void 0;
1592
+ if (typeof cwd !== "string" || cwd.length === 0) return void 0;
1593
+ return { id, cwd };
1594
+ }
1595
+ async function readFirstLine(file) {
1596
+ const stream = createReadStream(file, { encoding: "utf8" });
1597
+ const rl = createInterface({ input: stream, crlfDelay: Number.POSITIVE_INFINITY });
1598
+ try {
1599
+ for await (const line of rl) {
1600
+ const trimmed = line.trim();
1601
+ if (trimmed.length > 0) return trimmed;
1602
+ }
1603
+ return void 0;
1604
+ } catch {
1605
+ return void 0;
1606
+ } finally {
1607
+ rl.close();
1608
+ stream.destroy();
1609
+ }
1610
+ }
1611
+ async function readJsonlRecords(file) {
1612
+ let body;
1613
+ try {
1614
+ body = await readFile(file, "utf8");
1615
+ } catch (error) {
1616
+ if (findErrorCode5(error, "ENOENT")) {
1617
+ throw new Error("Source log not found", { cause: error });
1618
+ }
1619
+ if (findErrorCode5(error, "EISDIR")) {
1620
+ throw new Error("Source log path is not a file", { cause: error });
1621
+ }
1622
+ throw new Error("Failed to read source log", { cause: error });
1623
+ }
1624
+ const records = [];
1625
+ for (const line of body.split("\n")) {
1626
+ const trimmed = line.trim();
1627
+ if (trimmed.length === 0) continue;
1628
+ try {
1629
+ const parsed = JSON.parse(trimmed);
1630
+ if (isObject(parsed)) {
1631
+ records.push(parsed);
1632
+ }
1633
+ } catch {
1634
+ }
1635
+ }
1636
+ return records;
1637
+ }
1638
+ function isObject(value) {
1639
+ return typeof value === "object" && value !== null && !Array.isArray(value);
1640
+ }
1641
+ function printImportResult(options, results, counts) {
1642
+ const isDry = options.dryRun === true;
1643
+ const eventTotal = results.reduce((sum, r) => sum + r.eventCount, 0);
1644
+ const { skippedNoAction, skippedExisting, replaced } = counts;
1645
+ if (options.json === true) {
1646
+ console.log(
1647
+ JSON.stringify({
1648
+ imported: results.map((r) => ({
1649
+ session_id: r.sessionId,
1650
+ event_count: r.eventCount,
1651
+ status: r.finalStatus,
1652
+ source: { kind: r.finalSourceKind, version: "0.1.0" }
1653
+ })),
1654
+ imported_count: results.length,
1655
+ replaced_count: replaced,
1656
+ skipped_no_action: skippedNoAction,
1657
+ skipped_already_imported: skippedExisting,
1658
+ event_total: eventTotal,
1659
+ dry_run: isDry
1660
+ })
1661
+ );
1662
+ return;
1663
+ }
1664
+ const skipParts = [];
1665
+ if (skippedNoAction > 0) skipParts.push(`${skippedNoAction} with no actions`);
1666
+ if (skippedExisting > 0) skipParts.push(`${skippedExisting} already imported`);
1667
+ const skipSuffix = skipParts.length > 0 ? `; skipped ${skipParts.join(", ")}` : "";
1668
+ const eventsPart = replaced > 0 ? `${eventTotal} events, ${replaced} replaced` : `${eventTotal} events`;
1669
+ if (results.length === 0) {
1670
+ console.log(
1671
+ skipParts.length > 0 ? `No new sessions imported (skipped ${skipParts.join(", ")})` : "No transcripts found to import"
1672
+ );
1673
+ return;
1674
+ }
1675
+ if (isDry) {
1676
+ console.log(`Dry run: would import ${results.length} session(s) (${eventsPart})${skipSuffix}`);
1677
+ return;
1678
+ }
1679
+ const single = results.length === 1 && results[0] !== void 0 ? ` (${shortId2(results[0].sessionId)})` : "";
1680
+ console.log(`Imported ${results.length} session(s)${single} (${eventsPart})${skipSuffix}`);
1681
+ }
1682
+ function shortId2(id) {
1683
+ if (id.startsWith(SES_PREFIX2)) {
1684
+ return id.slice(SES_PREFIX2.length, SES_PREFIX2.length + SHORT_ID_LEN2);
1685
+ }
1686
+ return id.slice(0, SHORT_ID_LEN2);
1687
+ }
1688
+ async function resolveRepositoryRootForImport(cwd) {
1689
+ try {
1690
+ return await resolveRepositoryRoot6(cwd);
1691
+ } catch (error) {
1692
+ if (error instanceof Error && error.message === "Not a git repository") {
1693
+ throw new Error("Not a git repository. Run 'git init' first, then re-run 'basou import'.", {
1694
+ cause: error
1695
+ });
1696
+ }
1697
+ throw error;
1698
+ }
1699
+ }
1700
+ async function assertWorkspaceInitialized5(basouRoot) {
1701
+ try {
1702
+ await assertBasouRootSafe6(basouRoot);
1703
+ } catch (error) {
1704
+ if (findErrorCode5(error, "ENOENT")) {
1705
+ throw new Error("Workspace not initialized. Run 'basou init' first.");
1706
+ }
1707
+ throw error;
1708
+ }
1709
+ }
1710
+
1330
1711
  // src/commands/init.ts
1331
- import { basename } from "path";
1712
+ import { basename as basename2 } from "path";
1332
1713
  import {
1333
1714
  appendBasouGitignore,
1334
1715
  createManifest,
1335
1716
  ensureBasouDirectory,
1336
- resolveRepositoryRoot as resolveRepositoryRoot6,
1717
+ resolveRepositoryRoot as resolveRepositoryRoot7,
1337
1718
  tryRemoteUrl,
1338
1719
  writeManifest
1339
1720
  } from "@basou/core";
@@ -1356,7 +1737,7 @@ async function runInit(options, ctx = {}) {
1356
1737
  async function doRunInit(options, ctx) {
1357
1738
  const cwd = ctx.cwd ?? process.cwd();
1358
1739
  const repositoryRoot = await resolveRepositoryRootForInit(cwd);
1359
- const workspaceName = options.name ?? basename(repositoryRoot);
1740
+ const workspaceName = options.name ?? basename2(repositoryRoot);
1360
1741
  let repositoryUrl;
1361
1742
  if (options.repoUrl !== void 0) {
1362
1743
  repositoryUrl = options.repoUrl === "" ? null : options.repoUrl;
@@ -1390,7 +1771,7 @@ function renderGitignoreWarning(error, verbose) {
1390
1771
  }
1391
1772
  async function resolveRepositoryRootForInit(cwd) {
1392
1773
  try {
1393
- return await resolveRepositoryRoot6(cwd);
1774
+ return await resolveRepositoryRoot7(cwd);
1394
1775
  } catch (error) {
1395
1776
  if (error instanceof Error && error.message === "Not a git repository") {
1396
1777
  throw new Error("Not a git repository. Run 'git init' first, then re-run 'basou init'.", {
@@ -1401,65 +1782,281 @@ async function resolveRepositoryRootForInit(cwd) {
1401
1782
  }
1402
1783
  }
1403
1784
 
1404
- // src/commands/run.ts
1405
- import { mkdir as mkdir2 } from "fs/promises";
1406
- import { homedir as homedir2 } from "os";
1407
- import { join as join3 } from "path";
1785
+ // src/commands/refresh.ts
1786
+ import { assertBasouRootSafe as assertBasouRootSafe7, basouPaths as basouPaths7, findErrorCode as findErrorCode6, resolveRepositoryRoot as resolveRepositoryRoot8 } from "@basou/core";
1787
+
1788
+ // src/lib/provenance-actions.ts
1408
1789
  import {
1409
- ChildProcessRunner as ChildProcessRunner2,
1410
- SessionSchema as SessionSchema2,
1411
- assertBasouRootSafe as assertBasouRootSafe6,
1412
- basouPaths as basouPaths6,
1413
- claudeCodeAdapterMetadata,
1414
- appendEvent as coreAppendEvent2,
1415
- getDiff,
1416
- getSnapshot as getSnapshot2,
1417
- overwriteYamlFile as overwriteYamlFile2,
1418
- prefixedUlid as prefixedUlid4,
1419
- readManifest as readManifest3,
1420
- readYamlFile as readYamlFile3,
1421
- resolveClaudeCodeCommand,
1422
- resolveRepositoryRoot as resolveRepositoryRoot7,
1423
- sanitizeRelatedFiles,
1424
- sanitizeWorkingDirectory as sanitizeWorkingDirectory2,
1425
- writeYamlFile as writeYamlFile2
1790
+ readMarkdownFile as readMarkdownFile3,
1791
+ renderDecisions as renderDecisions2,
1792
+ renderHandoff as renderHandoff2,
1793
+ renderWithMarkers as renderWithMarkers3,
1794
+ writeMarkdownFile as writeMarkdownFile3
1426
1795
  } from "@basou/core";
1427
- function registerRunCommand(program2, ctx = {}) {
1428
- const runCommand = program2.command("run").description("Run an AI coding tool through Basou as a tracked session").enablePositionalOptions().option("--no-snapshot", "Skip git_snapshot before/after the session").option("--cwd <path>", "Run from a Basou root other than process.cwd()").option("-v, --verbose", "Show error causes");
1429
- runCommand.command("claude-code [args...]").description("Run Claude Code CLI as a Basou-tracked session").option("--no-snapshot", "Skip git_snapshot before/after the session").option("--cwd <path>", "Run from a Basou root other than process.cwd()").option("-v, --verbose", "Show error causes").passThroughOptions().action(async (args, options, command) => {
1430
- const parentOptions = command.parent?.opts() ?? {};
1431
- const snapshotOn = parentOptions.snapshot !== false && options.snapshot !== false;
1432
- const merged = {
1433
- ...parentOptions,
1434
- ...options,
1435
- snapshot: snapshotOn
1436
- };
1796
+ async function captureImportJson(fn) {
1797
+ const stdout = [];
1798
+ const originalLog = console.log;
1799
+ const originalError = console.error;
1800
+ console.log = ((...args) => {
1801
+ stdout.push(args.map((a) => String(a)).join(" "));
1802
+ });
1803
+ console.error = (() => {
1804
+ });
1805
+ try {
1806
+ await fn();
1807
+ } finally {
1808
+ console.log = originalLog;
1809
+ console.error = originalError;
1810
+ }
1811
+ for (let i = stdout.length - 1; i >= 0; i--) {
1812
+ const line = stdout[i];
1813
+ if (line === void 0) continue;
1437
1814
  try {
1438
- const exitCode = await runClaudeCode(args, merged, ctx);
1439
- process.exit(exitCode);
1440
- } catch (error) {
1441
- renderCliError(error, { verbose: isVerbose(merged) });
1442
- process.exit(1);
1815
+ const parsed = JSON.parse(line);
1816
+ if (parsed !== null && typeof parsed === "object" && "imported_count" in parsed) {
1817
+ return parsed;
1818
+ }
1819
+ } catch {
1443
1820
  }
1444
- });
1821
+ }
1822
+ throw new Error("Import produced no parseable result");
1445
1823
  }
1446
- async function runClaudeCode(args, options, ctx = {}) {
1447
- const runner = ctx.runner ?? new ChildProcessRunner2();
1448
- const now = ctx.now ?? (() => /* @__PURE__ */ new Date());
1449
- const appendEvent2 = ctx.appendEvent ?? coreAppendEvent2;
1450
- const resolveCommand = ctx.resolveCommand ?? resolveClaudeCodeCommand;
1451
- const getDiffFn = ctx.getDiff ?? getDiff;
1452
- const { command } = await resolveCommand();
1453
- const cwd = options.cwd ?? process.cwd();
1454
- const repoRoot = await resolveRepositoryRootForRun(cwd);
1455
- const paths = basouPaths6(repoRoot);
1456
- await assertBasouRootSafe6(paths.root);
1457
- const manifest = await readManifest3(paths);
1824
+ function readCount(value) {
1825
+ return typeof value === "number" && Number.isFinite(value) ? value : 0;
1826
+ }
1827
+ function isMissingSourceDir(error) {
1828
+ if (!(error instanceof Error)) return false;
1829
+ return error.message === "Claude transcript directory not found for project" || error.message === "Codex sessions directory not found";
1830
+ }
1831
+ async function runImport(adapter, fn) {
1832
+ try {
1833
+ const json = await captureImportJson(fn);
1834
+ return {
1835
+ adapter,
1836
+ status: "ran",
1837
+ importedCount: readCount(json.imported_count),
1838
+ replacedCount: readCount(json.replaced_count),
1839
+ skippedNoAction: readCount(json.skipped_no_action),
1840
+ skippedAlreadyImported: readCount(json.skipped_already_imported),
1841
+ eventTotal: readCount(json.event_total),
1842
+ dryRun: json.dry_run === true
1843
+ };
1844
+ } catch (error) {
1845
+ if (isMissingSourceDir(error)) {
1846
+ return { adapter, status: "skipped", reason: "no source logs for this project" };
1847
+ }
1848
+ throw error;
1849
+ }
1850
+ }
1851
+ function importOptions(options) {
1852
+ return {
1853
+ all: true,
1854
+ json: true,
1855
+ ...options.project !== void 0 ? { project: options.project } : {},
1856
+ ...options.force === true ? { force: true } : {},
1857
+ ...options.dryRun === true ? { dryRun: true } : {}
1858
+ };
1859
+ }
1860
+ function importClaudeCode(options, ctx) {
1861
+ return runImport("claude-code", () => doRunImportClaudeCode(importOptions(options), ctx));
1862
+ }
1863
+ function importCodex(options, ctx) {
1864
+ return runImport("codex", () => doRunImportCodex(importOptions(options), ctx));
1865
+ }
1866
+ async function regenerateHandoff(paths, nowIso, callbacks) {
1867
+ const result = await renderHandoff2({ paths, nowIso, ...callbacks });
1868
+ const existing = await readMarkdownFile3(paths.files.handoff);
1869
+ await writeMarkdownFile3(
1870
+ paths.files.handoff,
1871
+ renderWithMarkers3(existing, result.body, "handoff.md")
1872
+ );
1873
+ return {
1874
+ sessionCount: result.sessionCount,
1875
+ taskCount: result.taskCount,
1876
+ decisionCount: result.decisionCount,
1877
+ pendingApprovalsCount: result.pendingApprovalsCount
1878
+ };
1879
+ }
1880
+ async function regenerateDecisions(paths, nowIso, callbacks) {
1881
+ const result = await renderDecisions2({ paths, nowIso, ...callbacks });
1882
+ const existing = await readMarkdownFile3(paths.files.decisions);
1883
+ await writeMarkdownFile3(
1884
+ paths.files.decisions,
1885
+ renderWithMarkers3(existing, result.body, "decisions.md")
1886
+ );
1887
+ return { decisionCount: result.decisionCount };
1888
+ }
1889
+ async function refreshAll(args) {
1890
+ const { options, ctx, paths, nowIso } = args;
1891
+ const dryRun = options.dryRun === true;
1892
+ const claudeCode = await importClaudeCode(options, ctx);
1893
+ const codex = await importCodex(options, ctx);
1894
+ if (dryRun) {
1895
+ const skipped = { status: "skipped", reason: "dry-run" };
1896
+ return { claudeCode, codex, handoff: skipped, decisions: skipped, dryRun };
1897
+ }
1898
+ const handoffCounts = await regenerateHandoff(paths, nowIso);
1899
+ const decisionCounts = await regenerateDecisions(paths, nowIso);
1900
+ return {
1901
+ claudeCode,
1902
+ codex,
1903
+ handoff: { status: "generated", ...handoffCounts },
1904
+ decisions: { status: "generated", ...decisionCounts },
1905
+ dryRun
1906
+ };
1907
+ }
1908
+
1909
+ // src/commands/refresh.ts
1910
+ function registerRefreshCommand(program2) {
1911
+ program2.command("refresh").description(
1912
+ "Import all adapters for the project and regenerate handoff + decisions in one step"
1913
+ ).option(
1914
+ "--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) => {
1917
+ await runRefresh(options);
1918
+ });
1919
+ }
1920
+ async function runRefresh(options, ctx = {}) {
1921
+ try {
1922
+ await doRunRefresh(options, ctx);
1923
+ } catch (error) {
1924
+ renderCliError(error, { verbose: isVerbose(options) });
1925
+ process.exitCode = 1;
1926
+ }
1927
+ }
1928
+ async function doRunRefresh(options, ctx) {
1929
+ const cwd = ctx.cwd ?? process.cwd();
1930
+ const repositoryRoot = await resolveRepositoryRootForRefresh(cwd);
1931
+ const paths = basouPaths7(repositoryRoot);
1932
+ await assertWorkspaceInitialized6(paths.root);
1933
+ const nowIso = (ctx.nowProvider?.() ?? /* @__PURE__ */ new Date()).toISOString();
1934
+ const result = await refreshAll({
1935
+ options: {
1936
+ ...options.project !== void 0 ? { project: options.project } : {},
1937
+ ...options.force === true ? { force: true } : {},
1938
+ ...options.dryRun === true ? { dryRun: true } : {}
1939
+ },
1940
+ ctx,
1941
+ paths,
1942
+ nowIso
1943
+ });
1944
+ if (options.json === true) {
1945
+ console.log(JSON.stringify(result));
1946
+ } else {
1947
+ printRefreshSummary(result);
1948
+ }
1949
+ return result;
1950
+ }
1951
+ function describeImport(outcome) {
1952
+ if (outcome.status === "skipped") {
1953
+ return `${outcome.adapter}: skipped (${outcome.reason})`;
1954
+ }
1955
+ const verb = outcome.dryRun ? "would import" : "imported";
1956
+ const parts = [`${outcome.importedCount} session(s)`, `${outcome.eventTotal} events`];
1957
+ if (outcome.replacedCount > 0) parts.push(`${outcome.replacedCount} replaced`);
1958
+ if (outcome.skippedAlreadyImported > 0)
1959
+ parts.push(`${outcome.skippedAlreadyImported} already imported`);
1960
+ return `${outcome.adapter}: ${verb} ${parts.join(", ")}`;
1961
+ }
1962
+ function printRefreshSummary(result) {
1963
+ console.log(describeImport(result.claudeCode));
1964
+ console.log(describeImport(result.codex));
1965
+ if (result.handoff.status === "generated") {
1966
+ console.log(
1967
+ `handoff: regenerated (sessions: ${result.handoff.sessionCount}, decisions: ${result.handoff.decisionCount})`
1968
+ );
1969
+ } else {
1970
+ console.log(`handoff: skipped (${result.handoff.reason})`);
1971
+ }
1972
+ if (result.decisions.status === "generated") {
1973
+ console.log(`decisions: regenerated (${result.decisions.decisionCount})`);
1974
+ } else {
1975
+ console.log(`decisions: skipped (${result.decisions.reason})`);
1976
+ }
1977
+ }
1978
+ async function resolveRepositoryRootForRefresh(cwd) {
1979
+ try {
1980
+ return await resolveRepositoryRoot8(cwd);
1981
+ } catch (error) {
1982
+ if (error instanceof Error && error.message === "Not a git repository") {
1983
+ throw new Error("Not a git repository. Run 'git init' first, then re-run 'basou refresh'.", {
1984
+ cause: error
1985
+ });
1986
+ }
1987
+ throw error;
1988
+ }
1989
+ }
1990
+ async function assertWorkspaceInitialized6(basouRoot) {
1991
+ try {
1992
+ await assertBasouRootSafe7(basouRoot);
1993
+ } catch (error) {
1994
+ if (findErrorCode6(error, "ENOENT")) {
1995
+ throw new Error("Workspace not initialized. Run 'basou init' first.");
1996
+ }
1997
+ throw error;
1998
+ }
1999
+ }
2000
+
2001
+ // src/commands/run.ts
2002
+ import { mkdir as mkdir2 } from "fs/promises";
2003
+ import { homedir as homedir3 } from "os";
2004
+ import { join as join4 } from "path";
2005
+ import {
2006
+ assertBasouRootSafe as assertBasouRootSafe8,
2007
+ basouPaths as basouPaths8,
2008
+ ChildProcessRunner as ChildProcessRunner2,
2009
+ claudeCodeAdapterMetadata,
2010
+ appendEvent as coreAppendEvent2,
2011
+ getDiff,
2012
+ getSnapshot as getSnapshot2,
2013
+ overwriteYamlFile as overwriteYamlFile2,
2014
+ prefixedUlid as prefixedUlid4,
2015
+ readManifest as readManifest4,
2016
+ readYamlFile as readYamlFile3,
2017
+ resolveClaudeCodeCommand,
2018
+ resolveRepositoryRoot as resolveRepositoryRoot9,
2019
+ SessionSchema as SessionSchema2,
2020
+ sanitizeRelatedFiles,
2021
+ sanitizeWorkingDirectory as sanitizeWorkingDirectory2,
2022
+ writeYamlFile as writeYamlFile2
2023
+ } from "@basou/core";
2024
+ function registerRunCommand(program2, ctx = {}) {
2025
+ const runCommand = program2.command("run").description("Run an AI coding tool through Basou as a tracked session").enablePositionalOptions().option("--no-snapshot", "Skip git_snapshot before/after the session").option("--cwd <path>", "Run from a Basou root other than process.cwd()").option("-v, --verbose", "Show error causes");
2026
+ runCommand.command("claude-code [args...]").description("Run Claude Code CLI as a Basou-tracked session").option("--no-snapshot", "Skip git_snapshot before/after the session").option("--cwd <path>", "Run from a Basou root other than process.cwd()").option("-v, --verbose", "Show error causes").passThroughOptions().action(async (args, options, command) => {
2027
+ const parentOptions = command.parent?.opts() ?? {};
2028
+ const snapshotOn = parentOptions.snapshot !== false && options.snapshot !== false;
2029
+ const merged = {
2030
+ ...parentOptions,
2031
+ ...options,
2032
+ snapshot: snapshotOn
2033
+ };
2034
+ try {
2035
+ const exitCode = await runClaudeCode(args, merged, ctx);
2036
+ process.exit(exitCode);
2037
+ } catch (error) {
2038
+ renderCliError(error, { verbose: isVerbose(merged) });
2039
+ process.exit(1);
2040
+ }
2041
+ });
2042
+ }
2043
+ async function runClaudeCode(args, options, ctx = {}) {
2044
+ const runner = ctx.runner ?? new ChildProcessRunner2();
2045
+ const now = ctx.now ?? (() => /* @__PURE__ */ new Date());
2046
+ const appendEvent2 = ctx.appendEvent ?? coreAppendEvent2;
2047
+ const resolveCommand = ctx.resolveCommand ?? resolveClaudeCodeCommand;
2048
+ const getDiffFn = ctx.getDiff ?? getDiff;
2049
+ const { command } = await resolveCommand();
2050
+ const cwd = options.cwd ?? process.cwd();
2051
+ const repoRoot = await resolveRepositoryRootForRun(cwd);
2052
+ const paths = basouPaths8(repoRoot);
2053
+ await assertBasouRootSafe8(paths.root);
2054
+ const manifest = await readManifest4(paths);
1458
2055
  const sessionId = prefixedUlid4("ses");
1459
- const sessionDir = join3(paths.sessions, sessionId);
2056
+ const sessionDir = join4(paths.sessions, sessionId);
1460
2057
  await mkdir2(sessionDir, { recursive: true });
1461
2058
  const startedAt = now().toISOString();
1462
- const sessionYamlPath = join3(sessionDir, "session.yaml");
2059
+ const sessionYamlPath = join4(sessionDir, "session.yaml");
1463
2060
  const session = buildInitialSession2({
1464
2061
  id: sessionId,
1465
2062
  command,
@@ -1580,7 +2177,7 @@ async function runClaudeCode(args, options, ctx = {}) {
1580
2177
  const rawRelated = computeRelatedFiles(preSnapshot, postSnapshot, diff);
1581
2178
  const relatedFiles = sanitizeRelatedFiles(rawRelated, {
1582
2179
  workingDirectory: repoRoot,
1583
- homedir: homedir2()
2180
+ homedir: homedir3()
1584
2181
  }).sanitized;
1585
2182
  const finalStatus = decideFinalStatus2(result, signalReceived);
1586
2183
  await appendEvent2(sessionDir, {
@@ -1724,7 +2321,7 @@ function buildInitialSession2(input) {
1724
2321
  source: { ...claudeCodeAdapterMetadata },
1725
2322
  started_at: input.startedAt,
1726
2323
  status: "initialized",
1727
- working_directory: sanitizeWorkingDirectory2(input.cwd, { homedir: homedir2() }),
2324
+ working_directory: sanitizeWorkingDirectory2(input.cwd, { homedir: homedir3() }),
1728
2325
  invocation: {
1729
2326
  command: input.command,
1730
2327
  args: [...input.args],
@@ -1784,7 +2381,7 @@ async function finalizeSessionAsFailed2(sessionDir, sessionYamlPath, sessionId,
1784
2381
  }
1785
2382
  async function resolveRepositoryRootForRun(cwd) {
1786
2383
  try {
1787
- return await resolveRepositoryRoot7(cwd);
2384
+ return await resolveRepositoryRoot9(cwd);
1788
2385
  } catch (error) {
1789
2386
  if (error instanceof Error && error.message === "Not a git repository") {
1790
2387
  throw new Error("Not a git repository. Run 'git init' first, then re-run 'basou run'.", {
@@ -1796,28 +2393,42 @@ async function resolveRepositoryRootForRun(cwd) {
1796
2393
  }
1797
2394
 
1798
2395
  // src/commands/session.ts
1799
- import { readFile } from "fs/promises";
1800
- import { basename as basename2, isAbsolute, join as join4, relative } from "path";
2396
+ import { readFile as readFile2 } from "fs/promises";
2397
+ import { basename as basename3, isAbsolute, join as join5, relative } from "path";
1801
2398
  import {
1802
- SessionImportPayloadSchema,
1803
- SessionSchema as SessionSchema3,
1804
- SessionStatusSchema,
1805
2399
  acquireLock as acquireLock2,
1806
2400
  appendEventToExistingSession as appendEventToExistingSession2,
1807
- assertBasouRootSafe as assertBasouRootSafe7,
1808
- basouPaths as basouPaths7,
1809
- findErrorCode as findErrorCode5,
1810
- importSessionFromJson,
2401
+ assertBasouRootSafe as assertBasouRootSafe9,
2402
+ basouPaths as basouPaths9,
2403
+ findErrorCode as findErrorCode7,
2404
+ importSessionFromJson as importSessionFromJson2,
1811
2405
  loadSessionEntries,
1812
2406
  readAllEvents,
1813
- readManifest as readManifest4,
2407
+ readManifest as readManifest5,
1814
2408
  readYamlFile as readYamlFile4,
1815
- resolveRepositoryRoot as resolveRepositoryRoot8,
2409
+ resolveRepositoryRoot as resolveRepositoryRoot10,
1816
2410
  resolveSessionId as resolveSessionId2,
1817
- resolveTaskId
2411
+ resolveTaskId,
2412
+ SessionImportPayloadSchema as SessionImportPayloadSchema2,
2413
+ SessionSchema as SessionSchema3,
2414
+ SessionStatusSchema,
2415
+ sessionWorkStatsFromEvents
1818
2416
  } from "@basou/core";
1819
2417
  import { InvalidArgumentError as InvalidArgumentError2 } from "commander";
1820
- var SES_PREFIX2 = "ses_";
2418
+
2419
+ // src/lib/format-duration.ts
2420
+ function formatDurationMs(ms) {
2421
+ const totalSeconds = Math.round(ms / 1e3);
2422
+ const hours = Math.floor(totalSeconds / 3600);
2423
+ const minutes = Math.floor(totalSeconds % 3600 / 60);
2424
+ const seconds = totalSeconds % 60;
2425
+ if (hours > 0) return `${hours}h ${String(minutes).padStart(2, "0")}m`;
2426
+ if (minutes > 0) return `${minutes}m ${String(seconds).padStart(2, "0")}s`;
2427
+ return `${seconds}s`;
2428
+ }
2429
+
2430
+ // src/commands/session.ts
2431
+ var SES_PREFIX3 = "ses_";
1821
2432
  var TASK_PREFIX2 = "task_";
1822
2433
  var SHORT_ID_BASE_LEN2 = 6;
1823
2434
  var SHORT_ID_MAX_LEN2 = 26;
@@ -1855,8 +2466,8 @@ async function runSessionList(options, ctx = {}) {
1855
2466
  async function doRunSessionList(options, ctx) {
1856
2467
  const cwd = ctx.cwd ?? process.cwd();
1857
2468
  const repositoryRoot = await resolveRepositoryRootForSession(cwd, "list");
1858
- const paths = basouPaths7(repositoryRoot);
1859
- await assertWorkspaceInitialized5(paths.root);
2469
+ const paths = basouPaths9(repositoryRoot);
2470
+ await assertWorkspaceInitialized7(paths.root);
1860
2471
  const now = /* @__PURE__ */ new Date();
1861
2472
  const records = (await loadSessionEntries(paths, {
1862
2473
  now,
@@ -1907,17 +2518,17 @@ async function runSessionShow(idInput, options, ctx = {}) {
1907
2518
  async function doRunSessionShow(idInput, options, ctx) {
1908
2519
  const cwd = ctx.cwd ?? process.cwd();
1909
2520
  const repositoryRoot = await resolveRepositoryRootForSession(cwd, "show");
1910
- const paths = basouPaths7(repositoryRoot);
1911
- await assertWorkspaceInitialized5(paths.root);
2521
+ const paths = basouPaths9(repositoryRoot);
2522
+ await assertWorkspaceInitialized7(paths.root);
1912
2523
  const sessionId = await resolveSessionId2(paths, idInput);
1913
- const sessionDir = join4(paths.sessions, sessionId);
1914
- const sessionYamlPath = join4(sessionDir, "session.yaml");
2524
+ const sessionDir = join5(paths.sessions, sessionId);
2525
+ const sessionYamlPath = join5(sessionDir, "session.yaml");
1915
2526
  let session;
1916
2527
  try {
1917
2528
  const raw = await readYamlFile4(sessionYamlPath);
1918
2529
  session = SessionSchema3.parse(raw);
1919
2530
  } catch (error) {
1920
- if (findErrorCode5(error, "ENOENT")) {
2531
+ if (findErrorCode7(error, "ENOENT")) {
1921
2532
  throw new Error(`Session not found: ${idInput}`);
1922
2533
  }
1923
2534
  throw new Error("Failed to read session", { cause: error });
@@ -1929,7 +2540,8 @@ async function doRunSessionShow(idInput, options, ctx) {
1929
2540
  console.log(JSON.stringify({ session: session.session, events }, null, 2));
1930
2541
  return;
1931
2542
  }
1932
- printSessionShowText(session, events, options, repositoryRoot);
2543
+ const now = ctx.nowProvider?.() ?? /* @__PURE__ */ new Date();
2544
+ printSessionShowText(session, events, options, repositoryRoot, now);
1933
2545
  }
1934
2546
  function suspectLabel(reason) {
1935
2547
  if (reason === "events_say_ended_but_yaml_running") return " \u26A0 ended (yaml stale)";
@@ -1975,7 +2587,7 @@ function printSessionListText(records) {
1975
2587
  );
1976
2588
  }
1977
2589
  }
1978
- function printSessionShowText(session, events, options, repositoryRoot) {
2590
+ function printSessionShowText(session, events, options, repositoryRoot, now) {
1979
2591
  const s = session.session;
1980
2592
  console.log(`Session: ${s.id} (status: ${s.status})`);
1981
2593
  console.log(`Source: ${s.source.kind} (v${s.source.version})`);
@@ -2000,6 +2612,8 @@ function printSessionShowText(session, events, options, repositoryRoot) {
2000
2612
  for (const [type, n] of counts) {
2001
2613
  console.log(` ${pad2(`${type}:`, 24)} ${n}`);
2002
2614
  }
2615
+ console.log("");
2616
+ console.log(`Work: ${formatSessionWork(session, events, now)}`);
2003
2617
  if (events.length === 0) return;
2004
2618
  const last = options.last ?? 5;
2005
2619
  const showAll = options.events === true && options.last === void 0;
@@ -2011,6 +2625,19 @@ function printSessionShowText(session, events, options, repositoryRoot) {
2011
2625
  console.log(` ${formatEventLine(ev)}`);
2012
2626
  }
2013
2627
  }
2628
+ function formatSessionWork(session, events, now) {
2629
+ const w = sessionWorkStatsFromEvents(session.session.id, session.session, events, now);
2630
+ const parts = [];
2631
+ if (w.tokens.output > 0) parts.push(`${w.tokens.output.toLocaleString("en-US")} output tokens`);
2632
+ parts.push(`${w.commandCount} cmd / ${w.fileChangedCount} files / ${w.decisionCount} dec`);
2633
+ const activeBasis = w.activeTimeBasis === "engaged-turns" ? "turns" : "events";
2634
+ parts.push(`active ${formatDurationMs(w.activeTimeMs)} (${activeBasis})`);
2635
+ parts.push(`span ${formatDurationMs(w.sessionSpanMs)}${w.open ? " (open)" : ""}`);
2636
+ parts.push(
2637
+ w.availability.commandTime ? `command ${formatDurationMs(w.commandTimeMs)}` : "command n/a (import)"
2638
+ );
2639
+ return parts.join(", ");
2640
+ }
2014
2641
  function formatWorkingDir(workingDir, repositoryRoot, options) {
2015
2642
  if (options.fullPath === true) return workingDir;
2016
2643
  if (!isAbsolute(workingDir)) {
@@ -2091,7 +2718,7 @@ function eventVariantSummary(ev) {
2091
2718
  return `${ev.stream} "${ev.summary}" raw_ref=${ev.raw_ref}`;
2092
2719
  }
2093
2720
  }
2094
- function shortId2(id) {
2721
+ function shortId3(id) {
2095
2722
  return sliceShort2(id, SHORT_ID_BASE_LEN2);
2096
2723
  }
2097
2724
  function shortTaskId2(id) {
@@ -2101,8 +2728,8 @@ function shortTaskId2(id) {
2101
2728
  return id.slice(0, SHORT_ID_BASE_LEN2);
2102
2729
  }
2103
2730
  function sliceShort2(id, len) {
2104
- if (id.startsWith(SES_PREFIX2)) {
2105
- return id.slice(SES_PREFIX2.length, SES_PREFIX2.length + len);
2731
+ if (id.startsWith(SES_PREFIX3)) {
2732
+ return id.slice(SES_PREFIX3.length, SES_PREFIX3.length + len);
2106
2733
  }
2107
2734
  return id.slice(0, len);
2108
2735
  }
@@ -2133,7 +2760,7 @@ function maxLen2(values, floor) {
2133
2760
  }
2134
2761
  async function resolveRepositoryRootForSession(cwd, subcmd) {
2135
2762
  try {
2136
- return await resolveRepositoryRoot8(cwd);
2763
+ return await resolveRepositoryRoot10(cwd);
2137
2764
  } catch (error) {
2138
2765
  if (error instanceof Error && error.message === "Not a git repository") {
2139
2766
  throw new Error(
@@ -2144,11 +2771,11 @@ async function resolveRepositoryRootForSession(cwd, subcmd) {
2144
2771
  throw error;
2145
2772
  }
2146
2773
  }
2147
- async function assertWorkspaceInitialized5(basouRoot) {
2774
+ async function assertWorkspaceInitialized7(basouRoot) {
2148
2775
  try {
2149
- await assertBasouRootSafe7(basouRoot);
2776
+ await assertBasouRootSafe9(basouRoot);
2150
2777
  } catch (error) {
2151
- if (findErrorCode5(error, "ENOENT")) {
2778
+ if (findErrorCode7(error, "ENOENT")) {
2152
2779
  throw new Error("Workspace not initialized. Run 'basou init' first.");
2153
2780
  }
2154
2781
  throw error;
@@ -2186,24 +2813,24 @@ async function runSessionImport(options, ctx = {}) {
2186
2813
  async function doRunSessionImport(options, ctx) {
2187
2814
  const cwd = ctx.cwd ?? process.cwd();
2188
2815
  const repositoryRoot = await resolveRepositoryRootForSession(cwd, "import");
2189
- const paths = basouPaths7(repositoryRoot);
2190
- await assertWorkspaceInitialized5(paths.root);
2191
- const manifest = await readManifest4(paths);
2816
+ const paths = basouPaths9(repositoryRoot);
2817
+ await assertWorkspaceInitialized7(paths.root);
2818
+ const manifest = await readManifest5(paths);
2192
2819
  const rawBody = await readInputFile(options.from);
2193
2820
  const json = parseJsonStrict(rawBody);
2194
- const parsed = SessionImportPayloadSchema.safeParse(json);
2821
+ const parsed = SessionImportPayloadSchema2.safeParse(json);
2195
2822
  if (!parsed.success) {
2196
2823
  throw new Error("Invalid import payload", { cause: parsed.error });
2197
2824
  }
2198
2825
  if (parsed.data.schema_version !== "0.1.0") {
2199
2826
  throw new Error(`Unsupported import schema_version: ${parsed.data.schema_version}`);
2200
2827
  }
2201
- const importOptions = { dryRun: options.dryRun === true };
2202
- if (options.label !== void 0) importOptions.labelOverride = options.label;
2828
+ const importOptions2 = { dryRun: options.dryRun === true };
2829
+ if (options.label !== void 0) importOptions2.labelOverride = options.label;
2203
2830
  if (options.task !== void 0) {
2204
- importOptions.taskIdOverride = await resolveTaskId(paths, options.task);
2831
+ importOptions2.taskIdOverride = await resolveTaskId(paths, options.task);
2205
2832
  }
2206
- const result = await importSessionFromJson(paths, manifest, parsed.data, importOptions);
2833
+ const result = await importSessionFromJson2(paths, manifest, parsed.data, importOptions2);
2207
2834
  const sanitizeReport = result.pathSanitizeReport;
2208
2835
  if (sanitizeReport.relatedFiles > 0 || sanitizeReport.workingDirectoryRewritten) {
2209
2836
  const wdCount = sanitizeReport.workingDirectoryRewritten ? 1 : 0;
@@ -2215,12 +2842,12 @@ async function doRunSessionImport(options, ctx) {
2215
2842
  }
2216
2843
  async function readInputFile(path) {
2217
2844
  try {
2218
- return await readFile(path, "utf8");
2845
+ return await readFile2(path, "utf8");
2219
2846
  } catch (error) {
2220
- if (findErrorCode5(error, "ENOENT")) {
2847
+ if (findErrorCode7(error, "ENOENT")) {
2221
2848
  throw new Error("Import source not found", { cause: error });
2222
2849
  }
2223
- if (findErrorCode5(error, "EISDIR")) {
2850
+ if (findErrorCode7(error, "EISDIR")) {
2224
2851
  throw new Error("Import source is not a file", { cause: error });
2225
2852
  }
2226
2853
  throw new Error("Failed to read import source", { cause: error });
@@ -2253,7 +2880,7 @@ function parseTaskIdOverride(raw) {
2253
2880
  }
2254
2881
  function printSessionImportResult(options, result) {
2255
2882
  const isDry = options.dryRun === true;
2256
- const sid = shortId2(result.sessionId);
2883
+ const sid = shortId3(result.sessionId);
2257
2884
  if (options.json === true) {
2258
2885
  console.log(
2259
2886
  JSON.stringify({
@@ -2273,7 +2900,7 @@ function printSessionImportResult(options, result) {
2273
2900
  return;
2274
2901
  }
2275
2902
  console.log(
2276
- `Imported session ${sid} (${result.eventCount} events) from ${basename2(options.from)}`
2903
+ `Imported session ${sid} (${result.eventCount} events) from ${basename3(options.from)}`
2277
2904
  );
2278
2905
  }
2279
2906
  var NOTE_BODY_PREVIEW_LIMIT = 80;
@@ -2300,8 +2927,8 @@ async function doRunSessionNote(sessionIdInput, options, ctx) {
2300
2927
  }
2301
2928
  const cwd = ctx.cwd ?? process.cwd();
2302
2929
  const repositoryRoot = await resolveRepositoryRootForSession(cwd, "note");
2303
- const paths = basouPaths7(repositoryRoot);
2304
- await assertWorkspaceInitialized5(paths.root);
2930
+ const paths = basouPaths9(repositoryRoot);
2931
+ await assertWorkspaceInitialized7(paths.root);
2305
2932
  const sessionId = await resolveSessionId2(paths, sessionIdInput);
2306
2933
  const body = hasBody ? options.body : await readNoteFile(options.fromFile);
2307
2934
  if (body.length === 0) {
@@ -2332,12 +2959,12 @@ async function doRunSessionNote(sessionIdInput, options, ctx) {
2332
2959
  }
2333
2960
  async function readNoteFile(path) {
2334
2961
  try {
2335
- return await readFile(path, "utf8");
2962
+ return await readFile2(path, "utf8");
2336
2963
  } catch (error) {
2337
- if (findErrorCode5(error, "ENOENT")) {
2964
+ if (findErrorCode7(error, "ENOENT")) {
2338
2965
  throw new Error("Note source not found", { cause: error });
2339
2966
  }
2340
- if (findErrorCode5(error, "EISDIR")) {
2967
+ if (findErrorCode7(error, "EISDIR")) {
2341
2968
  throw new Error("Note source is not a file", { cause: error });
2342
2969
  }
2343
2970
  throw new Error("Failed to read note source", { cause: error });
@@ -2350,7 +2977,7 @@ function parseNoteBodyOption(raw) {
2350
2977
  return raw;
2351
2978
  }
2352
2979
  function printSessionNoteResult(options, sessionId, eventId, sessionStatus, body) {
2353
- const sid = shortId2(sessionId);
2980
+ const sid = shortId3(sessionId);
2354
2981
  if (options.json === true) {
2355
2982
  console.log(
2356
2983
  JSON.stringify({
@@ -2366,14 +2993,136 @@ function printSessionNoteResult(options, sessionId, eventId, sessionStatus, body
2366
2993
  console.log(`Added note to session ${sid} (${sessionStatus}): ${preview}`);
2367
2994
  }
2368
2995
 
2996
+ // src/commands/stats.ts
2997
+ import {
2998
+ assertBasouRootSafe as assertBasouRootSafe10,
2999
+ basouPaths as basouPaths10,
3000
+ computeWorkStats,
3001
+ findErrorCode as findErrorCode8,
3002
+ resolveRepositoryRoot as resolveRepositoryRoot11
3003
+ } from "@basou/core";
3004
+ function registerStatsCommand(program2) {
3005
+ program2.command("stats").description("Report how much the AI worked (output volume + time proxies) across sessions").option("--by-source", "Break the totals down by session source kind").option("--by-day", "Break billable time and volume down by calendar day").option("--json", "Output the full stats as JSON").option("-v, --verbose", "Show error causes").action(async (options) => {
3006
+ await runStats(options);
3007
+ });
3008
+ }
3009
+ async function runStats(options, ctx = {}) {
3010
+ try {
3011
+ await doRunStats(options, ctx);
3012
+ } catch (error) {
3013
+ renderCliError(error, { verbose: isVerbose(options) });
3014
+ process.exitCode = 1;
3015
+ }
3016
+ }
3017
+ async function doRunStats(options, ctx) {
3018
+ const cwd = ctx.cwd ?? process.cwd();
3019
+ const repositoryRoot = await resolveRepositoryRootForStats(cwd);
3020
+ const paths = basouPaths10(repositoryRoot);
3021
+ await assertWorkspaceInitialized8(paths.root);
3022
+ const now = ctx.nowProvider?.() ?? /* @__PURE__ */ new Date();
3023
+ const result = await computeWorkStats({
3024
+ paths,
3025
+ now,
3026
+ onWarning: (w, sid) => printReplayWarning(w, sid),
3027
+ onSessionSkip: (sid, reason) => printSessionSkip(sid, reason)
3028
+ });
3029
+ if (options.json === true) {
3030
+ console.log(JSON.stringify(result, null, 2));
3031
+ return;
3032
+ }
3033
+ printStatsText(result, options.bySource === true, options.byDay === true);
3034
+ }
3035
+ function printStatsText(result, bySource, byDay) {
3036
+ const t = result.totals;
3037
+ const statusPart = result.byStatus.length > 0 ? ` (${result.byStatus.map((s) => `${s.status} ${s.count}`).join(", ")})` : "";
3038
+ console.log(`Sessions: ${t.sessionCount}${statusPart}`);
3039
+ console.log("");
3040
+ console.log("Volume (what the AI produced):");
3041
+ const tokenSessions = result.sessions.filter((s) => s.availability.tokens).length;
3042
+ const tokenCaveat = t.tokensAvailable && tokenSessions < t.sessionCount ? ` (token data on ${tokenSessions} of ${t.sessionCount} sessions)` : t.tokensAvailable ? "" : " (no token data captured; re-import to backfill)";
3043
+ console.log(` Output tokens: ${formatInt(t.tokens.output)}${tokenCaveat}`);
3044
+ if (t.tokens.reasoning > 0) {
3045
+ console.log(` Reasoning tokens: ${formatInt(t.tokens.reasoning)} (Codex)`);
3046
+ }
3047
+ console.log(
3048
+ ` Actions: ${t.commandCount} commands, ${t.fileChangedCount} files, ${t.decisionCount} decisions`
3049
+ );
3050
+ console.log("");
3051
+ console.log("Time (proxies for human harness labor; active = billing primary):");
3052
+ const turnSessions = result.sessions.filter((s) => s.activeTimeBasis === "engaged-turns").length;
3053
+ const basisCaveat = turnSessions === t.sessionCount ? "engaged turns" : turnSessions === 0 ? "event stream; re-import to capture conversation" : `engaged turns on ${turnSessions} of ${t.sessionCount} sessions, event stream on the rest`;
3054
+ console.log(
3055
+ ` Billable active: ${formatDurationMs(t.billableActiveTimeMs)} (union; ${basisCaveat}; idle gaps > 5m excluded; tz ${result.timeZone})`
3056
+ );
3057
+ if (t.activeTimeMs !== t.billableActiveTimeMs) {
3058
+ console.log(
3059
+ ` Summed: ${formatDurationMs(t.activeTimeMs)} (per-session sum; concurrent sessions double-counted)`
3060
+ );
3061
+ }
3062
+ const openPart = t.openSessionCount > 0 ? `; ${t.openSessionCount} open counted to now` : "";
3063
+ console.log(
3064
+ ` Span: ${formatDurationMs(t.sessionSpanMs)} (total elapsed${openPart})`
3065
+ );
3066
+ const cmdCaveat = t.commandTimeReliable ? "" : "; some sessions (e.g. claude-code-import) report 0 shell time";
3067
+ console.log(
3068
+ ` Command: ${formatDurationMs(t.commandTimeMs)} (real shell execution${cmdCaveat})`
3069
+ );
3070
+ if (bySource && result.bySource.length > 0) {
3071
+ console.log("");
3072
+ console.log("By source:");
3073
+ for (const s of result.bySource) {
3074
+ console.log(` ${s.sourceKind}: ${describeSource(s)}`);
3075
+ }
3076
+ }
3077
+ if (byDay && result.byDay.length > 0) {
3078
+ console.log("");
3079
+ console.log("By day (billable time x volume):");
3080
+ for (const d of result.byDay) {
3081
+ console.log(
3082
+ ` ${d.date}: ${formatDurationMs(d.billableActiveTimeMs)} active, ${formatInt(d.tokens.output)} out tok, ${d.commandCount} cmd / ${d.fileChangedCount} files / ${d.decisionCount} dec`
3083
+ );
3084
+ }
3085
+ }
3086
+ }
3087
+ function describeSource(s) {
3088
+ const cmd = s.commandTimeReliable ? formatDurationMs(s.commandTimeMs) : "n/a";
3089
+ const tokens = s.tokensAvailable ? `${formatInt(s.tokens.output)} out tok` : "no tokens";
3090
+ return `${s.sessionCount} sessions, ${tokens}, active ${formatDurationMs(s.activeTimeMs)}, command ${cmd}`;
3091
+ }
3092
+ function formatInt(n) {
3093
+ return n.toLocaleString("en-US");
3094
+ }
3095
+ async function resolveRepositoryRootForStats(cwd) {
3096
+ try {
3097
+ return await resolveRepositoryRoot11(cwd);
3098
+ } catch (error) {
3099
+ if (error instanceof Error && error.message === "Not a git repository") {
3100
+ throw new Error("Not a git repository. Run 'git init' first, then re-run 'basou stats'.", {
3101
+ cause: error
3102
+ });
3103
+ }
3104
+ throw error;
3105
+ }
3106
+ }
3107
+ async function assertWorkspaceInitialized8(basouRoot) {
3108
+ try {
3109
+ await assertBasouRootSafe10(basouRoot);
3110
+ } catch (error) {
3111
+ if (findErrorCode8(error, "ENOENT")) {
3112
+ throw new Error("Workspace not initialized. Run 'basou init' first.");
3113
+ }
3114
+ throw error;
3115
+ }
3116
+ }
3117
+
2369
3118
  // src/commands/status.ts
2370
3119
  import {
2371
- assertBasouRootSafe as assertBasouRootSafe8,
2372
- basouPaths as basouPaths8,
3120
+ assertBasouRootSafe as assertBasouRootSafe11,
3121
+ basouPaths as basouPaths11,
2373
3122
  buildStatusSnapshot,
2374
- findErrorCode as findErrorCode6,
2375
- readManifest as readManifest5,
2376
- resolveRepositoryRoot as resolveRepositoryRoot9,
3123
+ findErrorCode as findErrorCode9,
3124
+ readManifest as readManifest6,
3125
+ resolveRepositoryRoot as resolveRepositoryRoot12,
2377
3126
  writeStatus
2378
3127
  } from "@basou/core";
2379
3128
  function registerStatusCommand(program2) {
@@ -2392,20 +3141,20 @@ async function runStatus(options, ctx = {}) {
2392
3141
  async function doRunStatus(options, ctx) {
2393
3142
  const cwd = ctx.cwd ?? process.cwd();
2394
3143
  const repositoryRoot = await resolveRepositoryRootForStatus(cwd);
2395
- const paths = basouPaths8(repositoryRoot);
3144
+ const paths = basouPaths11(repositoryRoot);
2396
3145
  try {
2397
- await assertBasouRootSafe8(paths.root);
3146
+ await assertBasouRootSafe11(paths.root);
2398
3147
  } catch (error) {
2399
- if (findErrorCode6(error, "ENOENT")) {
3148
+ if (findErrorCode9(error, "ENOENT")) {
2400
3149
  throw new Error("Workspace not initialized. Run 'basou init' first.");
2401
3150
  }
2402
3151
  throw error;
2403
3152
  }
2404
3153
  let manifest;
2405
3154
  try {
2406
- manifest = await readManifest5(paths);
3155
+ manifest = await readManifest6(paths);
2407
3156
  } catch (error) {
2408
- if (findErrorCode6(error, "ENOENT")) {
3157
+ if (findErrorCode9(error, "ENOENT")) {
2409
3158
  throw new Error("Workspace not initialized. Run 'basou init' first.");
2410
3159
  }
2411
3160
  throw new Error("Failed to read workspace manifest", { cause: error });
@@ -2429,7 +3178,7 @@ function renderTextStatus(s) {
2429
3178
  }
2430
3179
  async function resolveRepositoryRootForStatus(cwd) {
2431
3180
  try {
2432
- return await resolveRepositoryRoot9(cwd);
3181
+ return await resolveRepositoryRoot12(cwd);
2433
3182
  } catch (error) {
2434
3183
  if (error instanceof Error && error.message === "Not a git repository") {
2435
3184
  throw new Error("Not a git repository. Run 'git init' first, then re-run 'basou status'.", {
@@ -2441,32 +3190,32 @@ async function resolveRepositoryRootForStatus(cwd) {
2441
3190
  }
2442
3191
 
2443
3192
  // src/commands/task.ts
2444
- import { readFile as readFile2 } from "fs/promises";
2445
- import { join as join5 } from "path";
3193
+ import { readFile as readFile3 } from "fs/promises";
3194
+ import { join as join6 } from "path";
2446
3195
  import {
2447
- TaskStatusSchema,
2448
- TaskWriteAfterEventError,
2449
3196
  archiveTask,
2450
- assertBasouRootSafe as assertBasouRootSafe9,
2451
- basouPaths as basouPaths9,
3197
+ assertBasouRootSafe as assertBasouRootSafe12,
3198
+ basouPaths as basouPaths12,
2452
3199
  createTaskWithEvent,
2453
3200
  deleteTask,
2454
3201
  editTask,
2455
3202
  enumerateArchivedTaskIds,
2456
- findErrorCode as findErrorCode7,
3203
+ findErrorCode as findErrorCode10,
2457
3204
  loadSessionEntries as loadSessionEntries2,
2458
3205
  loadTaskEntries,
2459
3206
  prefixedUlid as prefixedUlid5,
2460
- readManifest as readManifest6,
3207
+ readManifest as readManifest7,
2461
3208
  readTaskFile,
2462
3209
  readTaskFileWithArchiveFallback,
2463
3210
  reconcileAllTasks,
2464
3211
  reconcileTask,
2465
3212
  refreshTaskLinkedSessions,
2466
3213
  replayEvents as replayEvents2,
2467
- resolveRepositoryRoot as resolveRepositoryRoot10,
3214
+ resolveRepositoryRoot as resolveRepositoryRoot13,
2468
3215
  resolveSessionId as resolveSessionId3,
2469
3216
  resolveTaskId as resolveTaskId2,
3217
+ TaskStatusSchema,
3218
+ TaskWriteAfterEventError,
2470
3219
  updateTaskStatusWithEvent
2471
3220
  } from "@basou/core";
2472
3221
  import { InvalidArgumentError as InvalidArgumentError3 } from "commander";
@@ -2548,8 +3297,8 @@ async function doRunTaskNew(options, ctx) {
2548
3297
  }
2549
3298
  const cwd = ctx.cwd ?? process.cwd();
2550
3299
  const repositoryRoot = await resolveRepositoryRootForTask(cwd, "new");
2551
- const paths = basouPaths9(repositoryRoot);
2552
- await assertWorkspaceInitialized6(paths.root);
3300
+ const paths = basouPaths12(repositoryRoot);
3301
+ await assertWorkspaceInitialized9(paths.root);
2553
3302
  const description = options.description !== void 0 ? options.description : options.fromFile !== void 0 ? await readDescriptionFile(options.fromFile) : "";
2554
3303
  const now = ctx.nowProvider !== void 0 ? ctx.nowProvider() : /* @__PURE__ */ new Date();
2555
3304
  const occurredAt = now.toISOString();
@@ -2583,7 +3332,7 @@ async function doRunTaskNew(options, ctx) {
2583
3332
  });
2584
3333
  return;
2585
3334
  }
2586
- const manifest = await readManifest6(paths);
3335
+ const manifest = await readManifest7(paths);
2587
3336
  const result = await createTaskWithEvent({
2588
3337
  mode: "ad-hoc",
2589
3338
  paths,
@@ -2657,8 +3406,8 @@ async function runTaskList(options, ctx = {}) {
2657
3406
  async function doRunTaskList(options, ctx) {
2658
3407
  const cwd = ctx.cwd ?? process.cwd();
2659
3408
  const repositoryRoot = await resolveRepositoryRootForTask(cwd, "list");
2660
- const paths = basouPaths9(repositoryRoot);
2661
- await assertWorkspaceInitialized6(paths.root);
3409
+ const paths = basouPaths12(repositoryRoot);
3410
+ await assertWorkspaceInitialized9(paths.root);
2662
3411
  const entries = await loadTaskEntries(paths, {
2663
3412
  onSkip: (id, reason) => printTaskSkip(id, reason)
2664
3413
  });
@@ -2761,15 +3510,15 @@ async function runTaskShow(idInput, options, ctx = {}) {
2761
3510
  async function doRunTaskShow(idInput, options, ctx) {
2762
3511
  const cwd = ctx.cwd ?? process.cwd();
2763
3512
  const repositoryRoot = await resolveRepositoryRootForTask(cwd, "show");
2764
- const paths = basouPaths9(repositoryRoot);
2765
- await assertWorkspaceInitialized6(paths.root);
3513
+ const paths = basouPaths12(repositoryRoot);
3514
+ await assertWorkspaceInitialized9(paths.root);
2766
3515
  const taskId = await resolveTaskId2(paths, idInput, { includeArchived: true });
2767
3516
  const { doc, archived } = await readTaskFileWithArchiveFallback(paths, taskId);
2768
3517
  const sessions = await loadSessionEntries2(paths, { now: /* @__PURE__ */ new Date() });
2769
3518
  const events = [];
2770
3519
  const linkedSessionIds = new Set(doc.task.task.linked_sessions);
2771
3520
  for (const s of sessions) {
2772
- const sessionDir = join5(paths.sessions, s.sessionId);
3521
+ const sessionDir = join6(paths.sessions, s.sessionId);
2773
3522
  try {
2774
3523
  for await (const ev of replayEvents2(sessionDir, {
2775
3524
  onWarning: (w) => printReplayWarning(w, s.sessionId)
@@ -2905,8 +3654,8 @@ async function doRunTaskStatus(taskIdInput, newStatusInput, options, ctx) {
2905
3654
  const newStatus = parseTaskStatusPositional(newStatusInput);
2906
3655
  const cwd = ctx.cwd ?? process.cwd();
2907
3656
  const repositoryRoot = await resolveRepositoryRootForTask(cwd, "status");
2908
- const paths = basouPaths9(repositoryRoot);
2909
- await assertWorkspaceInitialized6(paths.root);
3657
+ const paths = basouPaths12(repositoryRoot);
3658
+ await assertWorkspaceInitialized9(paths.root);
2910
3659
  const taskId = await resolveTaskId2(paths, taskIdInput);
2911
3660
  const now = ctx.nowProvider !== void 0 ? ctx.nowProvider() : /* @__PURE__ */ new Date();
2912
3661
  const occurredAt = now.toISOString();
@@ -2931,7 +3680,7 @@ async function doRunTaskStatus(taskIdInput, newStatusInput, options, ctx) {
2931
3680
  });
2932
3681
  return;
2933
3682
  }
2934
- const manifest = await readManifest6(paths);
3683
+ const manifest = await readManifest7(paths);
2935
3684
  const result = await updateTaskStatusWithEvent({
2936
3685
  mode: "ad-hoc",
2937
3686
  paths,
@@ -2982,9 +3731,9 @@ async function runTaskReconcile(options, ctx = {}) {
2982
3731
  async function doRunTaskReconcile(options, ctx) {
2983
3732
  const cwd = ctx.cwd ?? process.cwd();
2984
3733
  const repositoryRoot = await resolveRepositoryRootForTask(cwd, "reconcile");
2985
- const paths = basouPaths9(repositoryRoot);
2986
- await assertWorkspaceInitialized6(paths.root);
2987
- const manifest = await readManifest6(paths);
3734
+ const paths = basouPaths12(repositoryRoot);
3735
+ await assertWorkspaceInitialized9(paths.root);
3736
+ const manifest = await readManifest7(paths);
2988
3737
  const nowProvider = ctx.nowProvider ?? (() => /* @__PURE__ */ new Date());
2989
3738
  const write = options.write === true;
2990
3739
  const verbose = isVerbose(options);
@@ -3162,9 +3911,9 @@ async function doRunTaskRefreshLinkage(taskIdInput, options, ctx) {
3162
3911
  }
3163
3912
  const cwd = ctx.cwd ?? process.cwd();
3164
3913
  const repositoryRoot = await resolveRepositoryRootForTask(cwd, "refresh-linkage");
3165
- const paths = basouPaths9(repositoryRoot);
3166
- await assertWorkspaceInitialized6(paths.root);
3167
- const manifest = await readManifest6(paths);
3914
+ const paths = basouPaths12(repositoryRoot);
3915
+ await assertWorkspaceInitialized9(paths.root);
3916
+ const manifest = await readManifest7(paths);
3168
3917
  const taskId = await resolveTaskId2(paths, taskIdInput);
3169
3918
  const nowProvider = ctx.nowProvider ?? (() => /* @__PURE__ */ new Date());
3170
3919
  const write = options.write === true;
@@ -3242,9 +3991,9 @@ async function doRunTaskEdit(taskIdInput, options, ctx) {
3242
3991
  }
3243
3992
  const cwd = ctx.cwd ?? process.cwd();
3244
3993
  const repositoryRoot = await resolveRepositoryRootForTask(cwd, "edit");
3245
- const paths = basouPaths9(repositoryRoot);
3246
- await assertWorkspaceInitialized6(paths.root);
3247
- const manifest = await readManifest6(paths);
3994
+ const paths = basouPaths12(repositoryRoot);
3995
+ await assertWorkspaceInitialized9(paths.root);
3996
+ const manifest = await readManifest7(paths);
3248
3997
  const taskId = await resolveTaskId2(paths, taskIdInput);
3249
3998
  const now = ctx.nowProvider !== void 0 ? ctx.nowProvider() : /* @__PURE__ */ new Date();
3250
3999
  const occurredAt = now.toISOString();
@@ -3298,9 +4047,9 @@ async function doRunTaskDelete(taskIdInput, options, ctx) {
3298
4047
  }
3299
4048
  const cwd = ctx.cwd ?? process.cwd();
3300
4049
  const repositoryRoot = await resolveRepositoryRootForTask(cwd, "delete");
3301
- const paths = basouPaths9(repositoryRoot);
3302
- await assertWorkspaceInitialized6(paths.root);
3303
- const manifest = await readManifest6(paths);
4050
+ const paths = basouPaths12(repositoryRoot);
4051
+ await assertWorkspaceInitialized9(paths.root);
4052
+ const manifest = await readManifest7(paths);
3304
4053
  const taskId = await resolveTaskId2(paths, taskIdInput);
3305
4054
  if (options.yes !== true) {
3306
4055
  await confirmDestructiveAction("delete", taskId);
@@ -3343,9 +4092,9 @@ async function doRunTaskArchive(taskIdInput, options, ctx) {
3343
4092
  }
3344
4093
  const cwd = ctx.cwd ?? process.cwd();
3345
4094
  const repositoryRoot = await resolveRepositoryRootForTask(cwd, "archive");
3346
- const paths = basouPaths9(repositoryRoot);
3347
- await assertWorkspaceInitialized6(paths.root);
3348
- const manifest = await readManifest6(paths);
4095
+ const paths = basouPaths12(repositoryRoot);
4096
+ await assertWorkspaceInitialized9(paths.root);
4097
+ const manifest = await readManifest7(paths);
3349
4098
  const taskId = await resolveTaskId2(paths, taskIdInput);
3350
4099
  if (options.yes !== true) {
3351
4100
  await confirmDestructiveAction("archive", taskId);
@@ -3387,8 +4136,8 @@ async function confirmDestructiveAction(action, taskId) {
3387
4136
  }
3388
4137
  }
3389
4138
  async function readSingleLineFromStdin() {
3390
- const { createInterface } = await import("readline/promises");
3391
- const rl = createInterface({ input: process.stdin, output: process.stdout });
4139
+ const { createInterface: createInterface2 } = await import("readline/promises");
4140
+ const rl = createInterface2({ input: process.stdin, output: process.stdout });
3392
4141
  try {
3393
4142
  const line = await rl.question("");
3394
4143
  return line;
@@ -3457,12 +4206,12 @@ function parsePositiveInt2(raw) {
3457
4206
  }
3458
4207
  async function readDescriptionFile(path) {
3459
4208
  try {
3460
- return await readFile2(path, "utf8");
4209
+ return await readFile3(path, "utf8");
3461
4210
  } catch (error) {
3462
- if (findErrorCode7(error, "ENOENT")) {
4211
+ if (findErrorCode10(error, "ENOENT")) {
3463
4212
  throw new Error("Description source not found", { cause: error });
3464
4213
  }
3465
- if (findErrorCode7(error, "EISDIR")) {
4214
+ if (findErrorCode10(error, "EISDIR")) {
3466
4215
  throw new Error("Description source is not a file", { cause: error });
3467
4216
  }
3468
4217
  throw new Error("Failed to read description source", { cause: error });
@@ -3470,7 +4219,7 @@ async function readDescriptionFile(path) {
3470
4219
  }
3471
4220
  async function resolveRepositoryRootForTask(cwd, subcmd) {
3472
4221
  try {
3473
- return await resolveRepositoryRoot10(cwd);
4222
+ return await resolveRepositoryRoot13(cwd);
3474
4223
  } catch (error) {
3475
4224
  if (error instanceof Error && error.message === "Not a git repository") {
3476
4225
  throw new Error(
@@ -3481,11 +4230,11 @@ async function resolveRepositoryRootForTask(cwd, subcmd) {
3481
4230
  throw error;
3482
4231
  }
3483
4232
  }
3484
- async function assertWorkspaceInitialized6(basouRoot) {
4233
+ async function assertWorkspaceInitialized9(basouRoot) {
3485
4234
  try {
3486
- await assertBasouRootSafe9(basouRoot);
4235
+ await assertBasouRootSafe12(basouRoot);
3487
4236
  } catch (error) {
3488
- if (findErrorCode7(error, "ENOENT")) {
4237
+ if (findErrorCode10(error, "ENOENT")) {
3489
4238
  throw new Error("Workspace not initialized. Run 'basou init' first.");
3490
4239
  }
3491
4240
  throw error;
@@ -3571,22 +4320,950 @@ function maxLen3(values, floor) {
3571
4320
  return max;
3572
4321
  }
3573
4322
 
3574
- // src/index.ts
4323
+ // src/commands/view.ts
4324
+ 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";
4327
+
4328
+ // src/lib/view-server.ts
4329
+ import { createServer } from "http";
4330
+ import { join as join7 } from "path";
4331
+ import {
4332
+ computeWorkStats as computeWorkStats2,
4333
+ enumerateApprovals as enumerateApprovals2,
4334
+ findErrorCode as findErrorCode11,
4335
+ isLazyExpired as isLazyExpired2,
4336
+ loadApproval as loadApproval2,
4337
+ loadSessionEntries as loadSessionEntries3,
4338
+ loadTaskEntries as loadTaskEntries2,
4339
+ readAllEvents as readAllEvents2,
4340
+ readManifest as readManifest8,
4341
+ readMarkdownFile as readMarkdownFile4,
4342
+ readSessionYaml as readSessionYaml2,
4343
+ readTaskFile as readTaskFile2,
4344
+ renderDecisions as renderDecisions3,
4345
+ renderHandoff as renderHandoff3
4346
+ } from "@basou/core";
4347
+
4348
+ // src/lib/view-ui.ts
4349
+ var VIEW_HTML = `<!doctype html>
4350
+ <html lang="en">
4351
+ <head>
4352
+ <meta charset="utf-8" />
4353
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
4354
+ <title>basou view</title>
4355
+ <style>
4356
+ :root { color-scheme: light dark; }
4357
+ * { box-sizing: border-box; }
4358
+ body { margin: 0; font: 14px/1.5 system-ui, -apple-system, Segoe UI, sans-serif; }
4359
+ header { padding: 10px 16px; border-bottom: 1px solid #8884; display: flex; flex-wrap: wrap; gap: 8px 12px; align-items: center; }
4360
+ header h1 { font-size: 15px; margin: 0 12px 0 0; font-weight: 700; }
4361
+ header .grow { flex: 1; }
4362
+ input[type=text] { padding: 4px 8px; border: 1px solid #8886; border-radius: 6px; min-width: 280px; font: inherit; }
4363
+ button { padding: 4px 10px; border: 1px solid #8886; border-radius: 6px; background: #8881; cursor: pointer; font: inherit; }
4364
+ button.primary { background: #2563eb; color: #fff; border-color: #2563eb; }
4365
+ button:disabled { opacity: .5; cursor: default; }
4366
+ label.chk { font-size: 13px; opacity: .85; }
4367
+ #status { padding: 6px 16px; font-size: 13px; min-height: 20px; border-bottom: 1px solid #8884; white-space: pre-wrap; }
4368
+ #status.err { color: #dc2626; }
4369
+ nav { display: flex; gap: 2px; padding: 6px 12px; border-bottom: 1px solid #8884; flex-wrap: wrap; }
4370
+ nav button { border: none; border-radius: 6px; background: transparent; }
4371
+ nav button.active { background: #2563eb22; font-weight: 600; }
4372
+ main { display: grid; grid-template-columns: minmax(220px, 320px) 1fr; min-height: 60vh; }
4373
+ main.single { grid-template-columns: 1fr; }
4374
+ #list { border-right: 1px solid #8884; overflow: auto; max-height: 80vh; }
4375
+ #list .row { padding: 8px 12px; border-bottom: 1px solid #8883; cursor: pointer; }
4376
+ #list .row:hover { background: #8881; }
4377
+ #list .row.active { background: #2563eb22; }
4378
+ #list .row .meta { font-size: 12px; opacity: .7; }
4379
+ #detail { padding: 12px 16px; overflow: auto; max-height: 80vh; }
4380
+ .badge { display: inline-block; padding: 0 6px; border-radius: 6px; background: #8882; font-size: 12px; }
4381
+ .badge.warn { background: #f59e0b33; }
4382
+ pre { background: #8881; padding: 12px; border-radius: 8px; overflow: auto; white-space: pre-wrap; word-break: break-word; }
4383
+ table.kv { border-collapse: collapse; }
4384
+ table.kv td { padding: 2px 10px 2px 0; vertical-align: top; }
4385
+ table.kv td.k { opacity: .7; }
4386
+ .cards { display: flex; flex-wrap: wrap; gap: 10px; }
4387
+ .card { border: 1px solid #8884; border-radius: 8px; padding: 10px 14px; min-width: 120px; }
4388
+ .card .n { font-size: 22px; font-weight: 700; }
4389
+ .card .l { font-size: 12px; opacity: .7; }
4390
+ .tl { border-left: 2px solid #8885; margin-left: 6px; padding-left: 12px; }
4391
+ .tl .ev { margin-bottom: 8px; }
4392
+ .tl .ev .t { font-size: 12px; opacity: .65; }
4393
+ .muted { opacity: .6; }
4394
+ </style>
4395
+ </head>
4396
+ <body>
4397
+ <header>
4398
+ <h1>basou view</h1>
4399
+ <input type="text" id="project" placeholder="project path" />
4400
+ <button class="primary" id="btn-refresh">Refresh all</button>
4401
+ <button id="btn-import-claude">Import claude-code</button>
4402
+ <button id="btn-import-codex">Import codex</button>
4403
+ <button id="btn-gen-handoff">Regenerate handoff</button>
4404
+ <button id="btn-gen-decisions">Regenerate decisions</button>
4405
+ <span class="grow"></span>
4406
+ <label class="chk"><input type="checkbox" id="opt-force" /> force</label>
4407
+ <label class="chk"><input type="checkbox" id="opt-dry" /> dry-run</label>
4408
+ </header>
4409
+ <div id="status"></div>
4410
+ <nav id="tabs"></nav>
4411
+ <main id="main">
4412
+ <div id="list"></div>
4413
+ <div id="detail"></div>
4414
+ </main>
4415
+ <script>
4416
+ (function () {
4417
+ var TABS = ['overview', 'stats', 'sessions', 'tasks', 'decisions', 'approvals', 'handoff'];
4418
+ var state = { tab: 'overview', repoRoot: '' };
4419
+
4420
+ function $(id) { return document.getElementById(id); }
4421
+ function clear(node) { while (node.firstChild) node.removeChild(node.firstChild); }
4422
+
4423
+ function el(tag, attrs, children) {
4424
+ var node = document.createElement(tag);
4425
+ if (attrs) {
4426
+ for (var k in attrs) {
4427
+ if (!Object.prototype.hasOwnProperty.call(attrs, k)) continue;
4428
+ if (k === 'class') node.className = attrs[k];
4429
+ else if (k === 'text') node.textContent = attrs[k];
4430
+ else if (k.slice(0, 2) === 'on') node.addEventListener(k.slice(2), attrs[k]);
4431
+ else node.setAttribute(k, attrs[k]);
4432
+ }
4433
+ }
4434
+ if (children) {
4435
+ for (var i = 0; i < children.length; i++) {
4436
+ var c = children[i];
4437
+ if (c === null || c === undefined) continue;
4438
+ node.appendChild(typeof c === 'string' ? document.createTextNode(c) : c);
4439
+ }
4440
+ }
4441
+ return node;
4442
+ }
4443
+
4444
+ function setStatus(msg, isErr) {
4445
+ var s = $('status');
4446
+ s.textContent = msg || '';
4447
+ s.className = isErr ? 'err' : '';
4448
+ }
4449
+
4450
+ function fetchJson(path, opts) {
4451
+ return fetch(path, opts).then(function (res) {
4452
+ return res.text().then(function (text) {
4453
+ var data = null;
4454
+ try { data = text ? JSON.parse(text) : null; } catch (e) { data = null; }
4455
+ if (!res.ok) {
4456
+ var m = data && data.error ? data.error : ('HTTP ' + res.status);
4457
+ throw new Error(m);
4458
+ }
4459
+ return data;
4460
+ });
4461
+ });
4462
+ }
4463
+
4464
+ function single(on) { $('main').className = on ? 'single' : ''; if (on) clear($('list')); }
4465
+
4466
+ // --- action bar ---------------------------------------------------------
4467
+
4468
+ function actionBody() {
4469
+ var body = {};
4470
+ var project = $('project').value.trim();
4471
+ if (project) body.project = project;
4472
+ if ($('opt-force').checked) body.force = true;
4473
+ if ($('opt-dry').checked) body.dryRun = true;
4474
+ return body;
4475
+ }
4476
+
4477
+ function setBusy(busy) {
4478
+ var ids = ['btn-refresh', 'btn-import-claude', 'btn-import-codex', 'btn-gen-handoff', 'btn-gen-decisions'];
4479
+ for (var i = 0; i < ids.length; i++) $(ids[i]).disabled = busy;
4480
+ }
4481
+
4482
+ function post(path, label) {
4483
+ setBusy(true);
4484
+ setStatus(label + '...', false);
4485
+ fetchJson(path, {
4486
+ method: 'POST',
4487
+ headers: { 'Content-Type': 'application/json' },
4488
+ body: JSON.stringify(actionBody())
4489
+ }).then(function (data) {
4490
+ setStatus(label + ' done: ' + summarize(data), false);
4491
+ loadTab(state.tab);
4492
+ }).catch(function (err) {
4493
+ setStatus(label + ' failed: ' + err.message, true);
4494
+ }).then(function () { setBusy(false); });
4495
+ }
4496
+
4497
+ function summarize(data) {
4498
+ if (!data) return 'ok';
4499
+ if (data.claudeCode || data.codex) {
4500
+ return 'claude-code ' + imp(data.claudeCode) + ', codex ' + imp(data.codex)
4501
+ + (data.handoff && data.handoff.status === 'generated' ? '; handoff+decisions regenerated' : '');
4502
+ }
4503
+ if (data.status === 'ran') return imp(data);
4504
+ if (data.status === 'skipped') return 'skipped (' + data.reason + ')';
4505
+ if (typeof data.sessionCount === 'number') return 'sessions ' + data.sessionCount + ', decisions ' + data.decisionCount;
4506
+ if (typeof data.decisionCount === 'number') return 'decisions ' + data.decisionCount;
4507
+ return 'ok';
4508
+ }
4509
+ function imp(o) {
4510
+ if (!o) return '-';
4511
+ if (o.status === 'skipped') return 'skipped';
4512
+ return (o.dryRun ? 'would import ' : 'imported ') + o.importedCount + ' (' + o.eventTotal + ' events)';
4513
+ }
4514
+
4515
+ // --- tabs ---------------------------------------------------------------
4516
+
4517
+ function buildTabs() {
4518
+ var nav = $('tabs');
4519
+ clear(nav);
4520
+ TABS.forEach(function (name) {
4521
+ nav.appendChild(el('button', {
4522
+ class: name === state.tab ? 'active' : '',
4523
+ text: name,
4524
+ onclick: function () { loadTab(name); }
4525
+ }));
4526
+ });
4527
+ }
4528
+
4529
+ function loadTab(name) {
4530
+ state.tab = name;
4531
+ buildTabs();
4532
+ clear($('detail'));
4533
+ clear($('list'));
4534
+ if (name === 'overview') return loadOverview();
4535
+ if (name === 'stats') return loadStats();
4536
+ if (name === 'sessions') return loadSessions();
4537
+ if (name === 'tasks') return loadTasks();
4538
+ if (name === 'decisions') return loadMarkdown('/api/decisions', 'decisions');
4539
+ if (name === 'approvals') return loadApprovals();
4540
+ if (name === 'handoff') return loadMarkdown('/api/handoff', 'handoff');
4541
+ }
4542
+
4543
+ function fail(err) { setStatus(err.message, true); }
4544
+
4545
+ function loadOverview() {
4546
+ single(true);
4547
+ fetchJson('/api/overview').then(function (d) {
4548
+ var detail = $('detail');
4549
+ if (!d || d.initialized === false) {
4550
+ detail.appendChild(el('p', { class: 'muted', text: 'Workspace not initialized.' }));
4551
+ return;
4552
+ }
4553
+ $('project').value = $('project').value || d.repoRoot || '';
4554
+ state.repoRoot = d.repoRoot || '';
4555
+ detail.appendChild(el('p', {}, [
4556
+ el('strong', { text: d.workspace.name }), ' ',
4557
+ el('span', { class: 'muted', text: d.workspace.id })
4558
+ ]));
4559
+ var c = d.counts;
4560
+ var cards = el('div', { class: 'cards' }, [
4561
+ card(c.sessions, 'sessions'),
4562
+ card(c.suspectSessions, 'suspect'),
4563
+ card(c.tasks, 'tasks'),
4564
+ card(c.pendingTasks, 'pending tasks'),
4565
+ card(c.decisions, 'decisions'),
4566
+ card(c.approvalsPending, 'approvals pending')
4567
+ ]);
4568
+ detail.appendChild(cards);
4569
+ detail.appendChild(el('p', { class: 'muted', text: 'repo: ' + d.repoRoot }));
4570
+ }).catch(fail);
4571
+ }
4572
+ function card(n, label) {
4573
+ return el('div', { class: 'card' }, [
4574
+ el('div', { class: 'n', text: String(n) }),
4575
+ el('div', { class: 'l', text: label })
4576
+ ]);
4577
+ }
4578
+
4579
+ function numfmt(n) { return (n || 0).toLocaleString('en-US'); }
4580
+ function fmtDur(ms) {
4581
+ var s = Math.round((ms || 0) / 1000);
4582
+ var h = Math.floor(s / 3600), m = Math.floor((s % 3600) / 60), sec = s % 60;
4583
+ if (h > 0) return h + 'h ' + (m < 10 ? '0' : '') + m + 'm';
4584
+ if (m > 0) return m + 'm ' + (sec < 10 ? '0' : '') + sec + 's';
4585
+ return sec + 's';
4586
+ }
4587
+ function kvrow(k, v) {
4588
+ return el('tr', {}, [el('td', { class: 'k', text: k }), el('td', { text: v })]);
4589
+ }
4590
+
4591
+ function loadStats() {
4592
+ single(true);
4593
+ fetchJson('/api/stats').then(function (d) {
4594
+ var detail = $('detail');
4595
+ var t = d.totals;
4596
+ detail.appendChild(el('p', { text: 'Sessions: ' + t.sessionCount }));
4597
+ detail.appendChild(el('h3', { text: 'Volume (what the AI produced)' }));
4598
+ detail.appendChild(el('div', { class: 'cards' }, [
4599
+ card(numfmt(t.tokens.output), 'output tokens'),
4600
+ (t.tokens.reasoning > 0 ? card(numfmt(t.tokens.reasoning), 'reasoning tokens') : null),
4601
+ card(t.commandCount, 'commands'),
4602
+ card(t.fileChangedCount, 'files'),
4603
+ card(t.decisionCount, 'decisions')
4604
+ ]));
4605
+ var sessions = d.sessions || [];
4606
+ var tokenSessions = sessions.filter(function (s) { return s.availability && s.availability.tokens; }).length;
4607
+ if (!t.tokensAvailable) {
4608
+ detail.appendChild(el('p', { class: 'muted', text: 'No token data captured; re-import to backfill.' }));
4609
+ } else if (tokenSessions < t.sessionCount) {
4610
+ detail.appendChild(el('p', { class: 'muted', text: 'Token data on ' + tokenSessions + ' of ' + t.sessionCount + ' sessions; re-import to backfill the rest.' }));
4611
+ }
4612
+ var degraded = sessions.filter(function (s) { return s.eventsUnreadable; }).length;
4613
+ if (degraded > 0) {
4614
+ detail.appendChild(el('p', { class: 'muted', text: degraded + ' session(s) had unreadable event logs; their counts are incomplete.' }));
4615
+ }
4616
+ detail.appendChild(el('h3', { text: 'Time (human harness labor; active = billing primary)' }));
4617
+ var turnSessions = sessions.filter(function (s) { return s.activeTimeBasis === 'engaged-turns'; }).length;
4618
+ var basisNote = turnSessions === t.sessionCount ? 'engaged turns' : (turnSessions === 0 ? 'event stream; re-import to capture conversation' : 'engaged turns on ' + turnSessions + ' of ' + t.sessionCount + ' sessions');
4619
+ var timeRows = [kvrow('billable active', fmtDur(t.billableActiveTimeMs) + ' (union; ' + basisNote + '; idle gaps > 5m excluded; tz ' + d.timeZone + ')')];
4620
+ if (t.activeTimeMs !== t.billableActiveTimeMs) {
4621
+ timeRows.push(kvrow('summed', fmtDur(t.activeTimeMs) + ' (concurrent sessions double-counted)'));
4622
+ }
4623
+ timeRows.push(kvrow('span', fmtDur(t.sessionSpanMs) + (t.openSessionCount > 0 ? ' (' + t.openSessionCount + ' open)' : '')));
4624
+ timeRows.push(kvrow('command', fmtDur(t.commandTimeMs) + (t.commandTimeReliable ? '' : ' (some sessions report 0)')));
4625
+ detail.appendChild(el('table', { class: 'kv' }, [el('tbody', {}, timeRows)]));
4626
+ if (d.bySource && d.bySource.length) {
4627
+ detail.appendChild(el('h3', { text: 'By source' }));
4628
+ d.bySource.forEach(function (s) {
4629
+ var cmd = s.commandTimeReliable ? fmtDur(s.commandTimeMs) : 'n/a';
4630
+ 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 })
4632
+ ]));
4633
+ });
4634
+ }
4635
+ if (d.byDay && d.byDay.length) {
4636
+ detail.appendChild(el('h3', { text: 'By day (billable time x volume)' }));
4637
+ d.byDay.forEach(function (day) {
4638
+ 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' })
4640
+ ]));
4641
+ });
4642
+ }
4643
+ }).catch(fail);
4644
+ }
4645
+
4646
+ function loadSessions() {
4647
+ single(false);
4648
+ fetchJson('/api/sessions').then(function (d) {
4649
+ var list = $('list');
4650
+ var rows = (d && d.sessions) || [];
4651
+ if (rows.length === 0) { list.appendChild(el('div', { class: 'row muted', text: 'no sessions' })); return; }
4652
+ rows.forEach(function (s) {
4653
+ var row = el('div', { class: 'row', onclick: function () { selectSession(row, s.sessionId); } }, [
4654
+ el('div', { text: s.label || s.sessionId }),
4655
+ el('div', { class: 'meta', text: s.sourceKind + ' ' + s.status + (s.suspect ? ' suspect' : '') })
4656
+ ]);
4657
+ list.appendChild(row);
4658
+ });
4659
+ }).catch(fail);
4660
+ }
4661
+ function selectSession(row, id) {
4662
+ var rows = $('list').querySelectorAll('.row');
4663
+ for (var i = 0; i < rows.length; i++) rows[i].classList.remove('active');
4664
+ row.classList.add('active');
4665
+ var detail = $('detail');
4666
+ clear(detail);
4667
+ fetchJson('/api/sessions/' + encodeURIComponent(id)).then(function (d) {
4668
+ var s = d.session.session;
4669
+ detail.appendChild(el('h3', { text: s.label || id }));
4670
+ detail.appendChild(kv([
4671
+ ['status', s.status], ['source', s.source.kind], ['started', s.started_at],
4672
+ ['ended', s.ended_at || '-'], ['workdir', s.working_directory]
4673
+ ]));
4674
+ if (d.degraded) detail.appendChild(el('p', { class: 'badge warn', text: 'events unreadable' }));
4675
+ var events = d.events || [];
4676
+ detail.appendChild(el('p', { class: 'muted', text: events.length + ' events' }));
4677
+ var tl = el('div', { class: 'tl' }, []);
4678
+ events.forEach(function (ev) {
4679
+ tl.appendChild(el('div', { class: 'ev' }, [
4680
+ el('div', { class: 't', text: ev.occurred_at + ' ' + ev.type }),
4681
+ el('div', { text: eventSummary(ev) })
4682
+ ]));
4683
+ });
4684
+ detail.appendChild(tl);
4685
+ }).catch(fail);
4686
+ }
4687
+ function eventSummary(ev) {
4688
+ if (ev.type === 'command_executed') {
4689
+ var cmd = (ev.args && ev.args.length) ? ev.args.join(' ') : ev.command;
4690
+ var ex = (ev.exit_code === null || ev.exit_code === undefined) ? '' : ' (exit ' + ev.exit_code + ')';
4691
+ return cmd + ex;
4692
+ }
4693
+ if (ev.type === 'file_changed') return ev.path + ' [' + ev.change_type + ']';
4694
+ if (ev.type === 'decision_recorded') return ev.title || '';
4695
+ return '';
4696
+ }
4697
+
4698
+ function loadTasks() {
4699
+ single(false);
4700
+ fetchJson('/api/tasks').then(function (d) {
4701
+ var list = $('list');
4702
+ var rows = (d && d.tasks) || [];
4703
+ if (rows.length === 0) { list.appendChild(el('div', { class: 'row muted', text: 'no tasks' })); return; }
4704
+ rows.forEach(function (t) {
4705
+ var row = el('div', { class: 'row', onclick: function () { selectTask(row, t.id); } }, [
4706
+ el('div', { text: t.title || t.label || t.id }),
4707
+ el('div', { class: 'meta', text: String(t.status || '') })
4708
+ ]);
4709
+ list.appendChild(row);
4710
+ });
4711
+ }).catch(fail);
4712
+ }
4713
+ function selectTask(row, id) {
4714
+ var rows = $('list').querySelectorAll('.row');
4715
+ for (var i = 0; i < rows.length; i++) rows[i].classList.remove('active');
4716
+ row.classList.add('active');
4717
+ var detail = $('detail');
4718
+ clear(detail);
4719
+ fetchJson('/api/tasks/' + encodeURIComponent(id)).then(function (d) {
4720
+ detail.appendChild(el('h3', { text: (d.task && (d.task.title || d.task.label)) || id }));
4721
+ detail.appendChild(el('pre', { text: JSON.stringify(d.task, null, 2) }));
4722
+ if (d.body) detail.appendChild(el('pre', { text: d.body }));
4723
+ }).catch(fail);
4724
+ }
4725
+
4726
+ function loadMarkdown(path, label) {
4727
+ single(true);
4728
+ fetchJson(path).then(function (d) {
4729
+ var detail = $('detail');
4730
+ var count = (typeof d.decisionCount === 'number') ? (' (' + d.decisionCount + ' decisions)') : '';
4731
+ detail.appendChild(el('p', { class: 'muted', text: label + count }));
4732
+ detail.appendChild(el('pre', { text: (d && d.body) || '(empty)' }));
4733
+ }).catch(fail);
4734
+ }
4735
+
4736
+ function loadApprovals() {
4737
+ single(true);
4738
+ fetchJson('/api/approvals').then(function (d) {
4739
+ var detail = $('detail');
4740
+ var groups = [['pending', d.pending || []], ['resolved', d.resolved || []]];
4741
+ groups.forEach(function (g) {
4742
+ detail.appendChild(el('h3', { text: g[0] + ' (' + g[1].length + ')' }));
4743
+ if (g[1].length === 0) { detail.appendChild(el('p', { class: 'muted', text: 'none' })); return; }
4744
+ g[1].forEach(function (a) {
4745
+ detail.appendChild(el('div', { class: 'row' }, [
4746
+ el('span', { text: a.id + ' ' }),
4747
+ el('span', { class: a.expired ? 'badge warn' : 'badge', text: a.expired ? 'expired' : (a.approval && a.approval.status) || '' })
4748
+ ]));
4749
+ });
4750
+ });
4751
+ }).catch(fail);
4752
+ }
4753
+
4754
+ function kv(pairs) {
4755
+ var tbody = el('tbody', {}, pairs.map(function (p) {
4756
+ return el('tr', {}, [el('td', { class: 'k', text: p[0] }), el('td', { text: String(p[1]) })]);
4757
+ }));
4758
+ return el('table', { class: 'kv' }, [tbody]);
4759
+ }
4760
+
4761
+ // --- wire up ------------------------------------------------------------
4762
+
4763
+ $('btn-refresh').addEventListener('click', function () { post('/api/refresh', 'Refresh all'); });
4764
+ $('btn-import-claude').addEventListener('click', function () { post('/api/import/claude-code', 'Import claude-code'); });
4765
+ $('btn-import-codex').addEventListener('click', function () { post('/api/import/codex', 'Import codex'); });
4766
+ $('btn-gen-handoff').addEventListener('click', function () { post('/api/handoff/generate', 'Regenerate handoff'); });
4767
+ $('btn-gen-decisions').addEventListener('click', function () { post('/api/decisions/generate', 'Regenerate decisions'); });
4768
+
4769
+ buildTabs();
4770
+ loadTab('overview');
4771
+ })();
4772
+ </script>
4773
+ </body>
4774
+ </html>`;
4775
+
4776
+ // src/lib/view-server.ts
4777
+ var HttpError = class extends Error {
4778
+ constructor(status, message) {
4779
+ super(message);
4780
+ this.status = status;
4781
+ }
4782
+ status;
4783
+ };
4784
+ var MAX_BODY_BYTES = 64 * 1024;
4785
+ function startViewServer(opts) {
4786
+ const { port, host = "127.0.0.1", deps } = opts;
4787
+ let actionQueue = Promise.resolve();
4788
+ const runExclusive = (fn) => {
4789
+ const result = actionQueue.then(fn, fn);
4790
+ actionQueue = result.then(
4791
+ () => void 0,
4792
+ () => void 0
4793
+ );
4794
+ return result;
4795
+ };
4796
+ let boundPort = port;
4797
+ const getPort = () => boundPort;
4798
+ return new Promise((resolve, reject) => {
4799
+ const server = createServer((req, res) => {
4800
+ handleRequest(req, res, deps, getPort, runExclusive).catch((error) => {
4801
+ sendError(res, error instanceof HttpError ? error.status : 500, pathlessMessage(error));
4802
+ });
4803
+ });
4804
+ server.on("error", reject);
4805
+ server.listen(port, host, () => {
4806
+ const address = server.address();
4807
+ boundPort = isAddressInfo(address) ? address.port : port;
4808
+ server.off("error", reject);
4809
+ resolve({
4810
+ url: `http://${host}:${boundPort}`,
4811
+ port: boundPort,
4812
+ close: () => closeServer(server)
4813
+ });
4814
+ });
4815
+ });
4816
+ }
4817
+ function isAddressInfo(value) {
4818
+ return value !== null && typeof value === "object";
4819
+ }
4820
+ function closeServer(server) {
4821
+ return new Promise((resolve) => {
4822
+ server.close(() => resolve());
4823
+ server.closeAllConnections();
4824
+ });
4825
+ }
4826
+ async function handleRequest(req, res, deps, getPort, runExclusive) {
4827
+ const method = req.method ?? "GET";
4828
+ const url = new URL(req.url ?? "/", "http://localhost");
4829
+ const pathname = url.pathname;
4830
+ if (!hostAllowed(req, getPort())) {
4831
+ sendError(res, 403, "Forbidden: host not allowed");
4832
+ return;
4833
+ }
4834
+ if (method === "GET") {
4835
+ await handleGet(res, pathname, deps);
4836
+ return;
4837
+ }
4838
+ if (method === "POST") {
4839
+ if (!originAllowed(req, getPort())) {
4840
+ sendError(res, 403, "Forbidden: cross-origin request");
4841
+ return;
4842
+ }
4843
+ const body = await readBody(req);
4844
+ await handlePost(res, pathname, body, deps, runExclusive);
4845
+ return;
4846
+ }
4847
+ sendError(res, 405, "Method not allowed");
4848
+ }
4849
+ async function handleGet(res, pathname, deps) {
4850
+ if (pathname === "/") {
4851
+ sendHtml(res, VIEW_HTML);
4852
+ return;
4853
+ }
4854
+ if (pathname === "/api/overview") {
4855
+ sendJson(res, 200, await overview(deps));
4856
+ return;
4857
+ }
4858
+ if (pathname === "/api/sessions") {
4859
+ sendJson(res, 200, await sessionsList(deps));
4860
+ return;
4861
+ }
4862
+ const sessionId = matchId(pathname, "/api/sessions/");
4863
+ if (sessionId !== null) {
4864
+ sendJson(res, 200, await sessionDetail(deps, sessionId));
4865
+ return;
4866
+ }
4867
+ if (pathname === "/api/tasks") {
4868
+ sendJson(res, 200, await tasksList(deps));
4869
+ return;
4870
+ }
4871
+ const taskId = matchId(pathname, "/api/tasks/");
4872
+ if (taskId !== null) {
4873
+ sendJson(res, 200, await taskDetail(deps, taskId));
4874
+ return;
4875
+ }
4876
+ if (pathname === "/api/decisions") {
4877
+ sendJson(res, 200, await decisionsView(deps));
4878
+ return;
4879
+ }
4880
+ if (pathname === "/api/approvals") {
4881
+ sendJson(res, 200, await approvalsView(deps));
4882
+ return;
4883
+ }
4884
+ if (pathname === "/api/handoff") {
4885
+ sendJson(res, 200, await handoffView(deps));
4886
+ return;
4887
+ }
4888
+ if (pathname === "/api/stats") {
4889
+ sendJson(res, 200, await computeWorkStats2({ paths: deps.paths, now: deps.nowProvider() }));
4890
+ return;
4891
+ }
4892
+ sendError(res, 404, "Not found");
4893
+ }
4894
+ async function handlePost(res, pathname, body, deps, runExclusive) {
4895
+ const nowIso = deps.nowProvider().toISOString();
4896
+ const actionOptions = readActionOptions(body);
4897
+ if (pathname === "/api/refresh") {
4898
+ const result = await runExclusive(
4899
+ () => refreshAll({ options: actionOptions, ctx: deps.importCtx, paths: deps.paths, nowIso })
4900
+ );
4901
+ sendJson(res, 200, result);
4902
+ return;
4903
+ }
4904
+ if (pathname === "/api/import/claude-code") {
4905
+ sendJson(res, 200, await runExclusive(() => importClaudeCode(actionOptions, deps.importCtx)));
4906
+ return;
4907
+ }
4908
+ if (pathname === "/api/import/codex") {
4909
+ sendJson(res, 200, await runExclusive(() => importCodex(actionOptions, deps.importCtx)));
4910
+ return;
4911
+ }
4912
+ if (pathname === "/api/handoff/generate") {
4913
+ sendJson(res, 200, await runExclusive(() => regenerateHandoff(deps.paths, nowIso)));
4914
+ return;
4915
+ }
4916
+ if (pathname === "/api/decisions/generate") {
4917
+ sendJson(res, 200, await runExclusive(() => regenerateDecisions(deps.paths, nowIso)));
4918
+ return;
4919
+ }
4920
+ sendError(res, 404, "Not found");
4921
+ }
4922
+ async function overview(deps) {
4923
+ let manifest;
4924
+ try {
4925
+ manifest = await readManifest8(deps.paths);
4926
+ } catch (error) {
4927
+ if (findErrorCode11(error, "ENOENT")) {
4928
+ return { initialized: false, repoRoot: deps.repoRoot };
4929
+ }
4930
+ throw error;
4931
+ }
4932
+ const nowIso = deps.nowProvider().toISOString();
4933
+ const handoff = await renderHandoff3({ paths: deps.paths, nowIso });
4934
+ const approvals = await enumerateApprovals2(deps.paths);
4935
+ return {
4936
+ initialized: true,
4937
+ repoRoot: deps.repoRoot,
4938
+ workspace: {
4939
+ id: manifest.workspace.id,
4940
+ name: manifest.workspace.name,
4941
+ basouVersion: manifest.basou_version
4942
+ },
4943
+ counts: {
4944
+ sessions: handoff.sessionCount,
4945
+ suspectSessions: handoff.suspectCount,
4946
+ tasks: handoff.taskCount,
4947
+ pendingTasks: handoff.pendingTaskCount,
4948
+ decisions: handoff.decisionCount,
4949
+ approvalsPending: approvals.pending.length,
4950
+ approvalsResolved: approvals.resolved.length
4951
+ },
4952
+ generatedAt: nowIso
4953
+ };
4954
+ }
4955
+ async function sessionsList(deps) {
4956
+ const entries = await loadSessionEntries3(deps.paths, { now: deps.nowProvider() });
4957
+ const sessions = entries.map((entry) => ({
4958
+ sessionId: entry.sessionId,
4959
+ label: entry.session.session.label ?? null,
4960
+ status: entry.session.session.status,
4961
+ sourceKind: entry.session.session.source.kind,
4962
+ startedAt: entry.session.session.started_at,
4963
+ endedAt: entry.session.session.ended_at ?? null,
4964
+ suspect: entry.suspect,
4965
+ suspectReason: entry.suspectReason,
4966
+ taskId: entry.session.session.task_id ?? null,
4967
+ relatedFilesCount: entry.session.session.related_files.length
4968
+ })).reverse();
4969
+ return { sessions };
4970
+ }
4971
+ async function sessionDetail(deps, sessionId) {
4972
+ let session;
4973
+ try {
4974
+ session = await readSessionYaml2(deps.paths, sessionId);
4975
+ } catch (error) {
4976
+ if (error instanceof Error && error.message === "YAML file not found") {
4977
+ throw new HttpError(404, "Session not found");
4978
+ }
4979
+ throw error;
4980
+ }
4981
+ try {
4982
+ const events = await readAllEvents2(join7(deps.paths.sessions, sessionId));
4983
+ return { session, events };
4984
+ } catch {
4985
+ return { session, events: [], degraded: true };
4986
+ }
4987
+ }
4988
+ async function tasksList(deps) {
4989
+ const entries = await loadTaskEntries2(deps.paths);
4990
+ return { tasks: entries.map((entry) => entry.task).reverse() };
4991
+ }
4992
+ async function taskDetail(deps, taskId) {
4993
+ try {
4994
+ const doc = await readTaskFile2(deps.paths, taskId);
4995
+ return { task: doc.task, body: doc.body };
4996
+ } catch (error) {
4997
+ if (error instanceof Error && error.message === "Task file not found") {
4998
+ throw new HttpError(404, "Task not found");
4999
+ }
5000
+ throw error;
5001
+ }
5002
+ }
5003
+ async function decisionsView(deps) {
5004
+ const fromDisk = await readMarkdownFile4(deps.paths.files.decisions);
5005
+ if (fromDisk !== null) {
5006
+ return { body: fromDisk, fromDisk: true };
5007
+ }
5008
+ const nowIso = deps.nowProvider().toISOString();
5009
+ const result = await renderDecisions3({ paths: deps.paths, nowIso });
5010
+ return { body: result.body, decisionCount: result.decisionCount, fromDisk: false };
5011
+ }
5012
+ async function approvalsView(deps) {
5013
+ const now = deps.nowProvider();
5014
+ const ids = await enumerateApprovals2(deps.paths);
5015
+ const toViews = async (list) => {
5016
+ const views = [];
5017
+ for (const id of list) {
5018
+ const loaded = await loadApproval2(deps.paths, id);
5019
+ if (loaded === null) continue;
5020
+ views.push({ id, expired: isLazyExpired2(loaded.approval, now), approval: loaded.approval });
5021
+ }
5022
+ return views;
5023
+ };
5024
+ return { pending: await toViews(ids.pending), resolved: await toViews(ids.resolved) };
5025
+ }
5026
+ async function handoffView(deps) {
5027
+ const fromDisk = await readMarkdownFile4(deps.paths.files.handoff);
5028
+ if (fromDisk !== null) {
5029
+ return { body: fromDisk, fromDisk: true };
5030
+ }
5031
+ const nowIso = deps.nowProvider().toISOString();
5032
+ const result = await renderHandoff3({ paths: deps.paths, nowIso });
5033
+ return { body: result.body, fromDisk: false };
5034
+ }
5035
+ function readActionOptions(body) {
5036
+ const options = {};
5037
+ if (typeof body.project === "string" && body.project.length > 0) options.project = body.project;
5038
+ if (body.force === true) options.force = true;
5039
+ if (body.dryRun === true) options.dryRun = true;
5040
+ return options;
5041
+ }
5042
+ function hostAllowed(req, port) {
5043
+ const host = req.headers.host;
5044
+ return host === `127.0.0.1:${port}` || host === `localhost:${port}`;
5045
+ }
5046
+ function originAllowed(req, port) {
5047
+ const origin = req.headers.origin;
5048
+ if (origin === void 0) return true;
5049
+ return origin === `http://127.0.0.1:${port}` || origin === `http://localhost:${port}`;
5050
+ }
5051
+ async function readBody(req) {
5052
+ const chunks = [];
5053
+ let size = 0;
5054
+ for await (const chunk of req) {
5055
+ const buf = Buffer.isBuffer(chunk) ? chunk : Buffer.from(String(chunk));
5056
+ size += buf.length;
5057
+ if (size > MAX_BODY_BYTES) throw new HttpError(413, "Request body too large");
5058
+ chunks.push(buf);
5059
+ }
5060
+ const raw = Buffer.concat(chunks).toString("utf8").trim();
5061
+ if (raw.length === 0) return {};
5062
+ try {
5063
+ const parsed = JSON.parse(raw);
5064
+ if (parsed !== null && typeof parsed === "object" && !Array.isArray(parsed)) {
5065
+ return parsed;
5066
+ }
5067
+ } catch {
5068
+ }
5069
+ throw new HttpError(400, "Invalid JSON body");
5070
+ }
5071
+ function matchId(pathname, prefix) {
5072
+ if (!pathname.startsWith(prefix)) return null;
5073
+ const encoded = pathname.slice(prefix.length);
5074
+ if (encoded.length === 0 || encoded.includes("/")) return null;
5075
+ let id;
5076
+ try {
5077
+ id = decodeURIComponent(encoded);
5078
+ } catch {
5079
+ return null;
5080
+ }
5081
+ if (id.length === 0 || id.includes("/") || id.includes("\\") || id.includes("\0") || id === "." || id === "..") {
5082
+ return null;
5083
+ }
5084
+ return id;
5085
+ }
5086
+ function sendJson(res, status, data) {
5087
+ const body = JSON.stringify(data);
5088
+ res.writeHead(status, {
5089
+ "Content-Type": "application/json; charset=utf-8",
5090
+ "Cache-Control": "no-store"
5091
+ });
5092
+ res.end(body);
5093
+ }
5094
+ function sendHtml(res, html) {
5095
+ res.writeHead(200, {
5096
+ "Content-Type": "text/html; charset=utf-8",
5097
+ "Cache-Control": "no-store"
5098
+ });
5099
+ res.end(html);
5100
+ }
5101
+ function sendError(res, status, message) {
5102
+ if (res.headersSent) {
5103
+ res.end();
5104
+ return;
5105
+ }
5106
+ sendJson(res, status, { error: message });
5107
+ }
5108
+ function pathlessMessage(error) {
5109
+ return error instanceof Error ? error.message : "Internal error";
5110
+ }
5111
+
5112
+ // src/commands/view.ts
5113
+ var DEFAULT_PORT = 4319;
5114
+ function parsePort(value) {
5115
+ const port = Number.parseInt(value, 10);
5116
+ if (!Number.isInteger(port) || port < 1 || port > 65535) {
5117
+ throw new InvalidArgumentError4("Port must be an integer between 1 and 65535.");
5118
+ }
5119
+ return port;
5120
+ }
5121
+ function registerViewCommand(program2) {
5122
+ program2.command("view").description("Open a local web UI to browse provenance and run imports (localhost only)").option("--port <number>", "Port to listen on (default 4319)", parsePort).option("--no-open", "Do not open the browser automatically").option("-v, --verbose", "Show error causes").action(async (options) => {
5123
+ await runView(options);
5124
+ });
5125
+ }
5126
+ async function runView(options, ctx = {}) {
5127
+ try {
5128
+ await doRunView(options, ctx);
5129
+ } catch (error) {
5130
+ renderCliError(error, { verbose: isVerbose(options) });
5131
+ process.exitCode = 1;
5132
+ }
5133
+ }
5134
+ async function doRunView(options, ctx) {
5135
+ const cwd = ctx.cwd ?? process.cwd();
5136
+ const repositoryRoot = await resolveRepositoryRootForView(cwd);
5137
+ const paths = basouPaths13(repositoryRoot);
5138
+ await assertWorkspaceInitialized10(paths.root);
5139
+ const deps = {
5140
+ paths,
5141
+ repoRoot: repositoryRoot,
5142
+ importCtx: {
5143
+ cwd: repositoryRoot,
5144
+ ...ctx.claudeProjectsDir !== void 0 ? { claudeProjectsDir: ctx.claudeProjectsDir } : {},
5145
+ ...ctx.codexSessionsDir !== void 0 ? { codexSessionsDir: ctx.codexSessionsDir } : {}
5146
+ },
5147
+ nowProvider: ctx.nowProvider ?? (() => /* @__PURE__ */ new Date())
5148
+ };
5149
+ const port = options.port ?? DEFAULT_PORT;
5150
+ const handle = await startListening(port, deps);
5151
+ try {
5152
+ console.log(`basou view running at ${handle.url}`);
5153
+ console.log(
5154
+ "Localhost only, no authentication. Do not expose this port beyond your machine. Press Ctrl+C to stop."
5155
+ );
5156
+ if (options.open !== false) {
5157
+ openInBrowser(handle.url, ctx.openBrowser);
5158
+ }
5159
+ ctx.onListening?.(handle);
5160
+ await waitForShutdown(ctx.signal);
5161
+ } finally {
5162
+ await handle.close();
5163
+ }
5164
+ }
5165
+ async function startListening(port, deps) {
5166
+ try {
5167
+ return await startViewServer({ port, deps });
5168
+ } catch (error) {
5169
+ if (findErrorCode12(error, "EADDRINUSE")) {
5170
+ throw new Error(`Port ${port} is already in use. Pass --port <n> to choose another.`, {
5171
+ cause: error
5172
+ });
5173
+ }
5174
+ throw error;
5175
+ }
5176
+ }
5177
+ function openInBrowser(url, override) {
5178
+ if (override !== void 0) {
5179
+ override(url);
5180
+ return;
5181
+ }
5182
+ if (process.platform !== "darwin") return;
5183
+ try {
5184
+ const child = spawn("open", [url], { stdio: "ignore", detached: true });
5185
+ child.on("error", () => {
5186
+ });
5187
+ child.unref();
5188
+ } catch {
5189
+ }
5190
+ }
5191
+ function waitForShutdown(signal) {
5192
+ return new Promise((resolve) => {
5193
+ const cleanup = () => {
5194
+ process.off("SIGINT", onSignal);
5195
+ process.off("SIGTERM", onSignal);
5196
+ signal?.removeEventListener("abort", onAbort);
5197
+ };
5198
+ const onSignal = () => {
5199
+ cleanup();
5200
+ resolve();
5201
+ };
5202
+ const onAbort = () => {
5203
+ cleanup();
5204
+ resolve();
5205
+ };
5206
+ process.on("SIGINT", onSignal);
5207
+ process.on("SIGTERM", onSignal);
5208
+ if (signal !== void 0) {
5209
+ if (signal.aborted) {
5210
+ cleanup();
5211
+ resolve();
5212
+ return;
5213
+ }
5214
+ signal.addEventListener("abort", onAbort);
5215
+ }
5216
+ });
5217
+ }
5218
+ async function resolveRepositoryRootForView(cwd) {
5219
+ try {
5220
+ return await resolveRepositoryRoot14(cwd);
5221
+ } catch (error) {
5222
+ if (error instanceof Error && error.message === "Not a git repository") {
5223
+ throw new Error("Not a git repository. Run 'git init' first, then re-run 'basou view'.", {
5224
+ cause: error
5225
+ });
5226
+ }
5227
+ throw error;
5228
+ }
5229
+ }
5230
+ async function assertWorkspaceInitialized10(basouRoot) {
5231
+ try {
5232
+ await assertBasouRootSafe13(basouRoot);
5233
+ } catch (error) {
5234
+ if (findErrorCode12(error, "ENOENT")) {
5235
+ throw new Error("Workspace not initialized. Run 'basou init' first.");
5236
+ }
5237
+ throw error;
5238
+ }
5239
+ }
5240
+
5241
+ // src/program.ts
3575
5242
  var require2 = createRequire(import.meta.url);
3576
5243
  var pkg = require2("../package.json");
3577
5244
  var BASOU_CLI_VERSION = pkg.version;
3578
- var program = new Command();
3579
- program.name("basou").description("Provenance layer for AI development").version(BASOU_CLI_VERSION).enablePositionalOptions();
3580
- registerInitCommand(program);
3581
- registerStatusCommand(program);
3582
- registerExecCommand(program);
3583
- registerRunCommand(program);
3584
- registerSessionCommand(program);
3585
- registerApprovalCommand(program);
3586
- registerDecisionCommand(program);
3587
- registerTaskCommand(program);
3588
- registerHandoffCommand(program);
3589
- registerDecisionsCommand(program);
5245
+ function buildProgram() {
5246
+ const program2 = new Command();
5247
+ program2.name("basou").description("Provenance layer for AI development").version(BASOU_CLI_VERSION).enablePositionalOptions();
5248
+ registerInitCommand(program2);
5249
+ registerStatusCommand(program2);
5250
+ registerStatsCommand(program2);
5251
+ registerExecCommand(program2);
5252
+ registerRunCommand(program2);
5253
+ registerSessionCommand(program2);
5254
+ registerImportCommand(program2);
5255
+ registerRefreshCommand(program2);
5256
+ registerViewCommand(program2);
5257
+ registerApprovalCommand(program2);
5258
+ registerDecisionCommand(program2);
5259
+ registerTaskCommand(program2);
5260
+ registerHandoffCommand(program2);
5261
+ registerDecisionsCommand(program2);
5262
+ return program2;
5263
+ }
5264
+
5265
+ // src/index.ts
5266
+ var program = buildProgram();
3590
5267
  program.parseAsync(process.argv).catch((err) => {
3591
5268
  renderCliError(err, { verbose: isVerbose(void 0) });
3592
5269
  process.exit(1);