@basou/cli 0.6.0 → 0.8.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/program.js CHANGED
@@ -1327,9 +1327,9 @@ async function assertWorkspaceInitialized4(basouRoot) {
1327
1327
 
1328
1328
  // src/commands/import.ts
1329
1329
  import { createReadStream } from "fs";
1330
- import { readdir, readFile, rm } from "fs/promises";
1330
+ import { readdir, readFile, rm, stat } from "fs/promises";
1331
1331
  import { homedir as homedir2 } from "os";
1332
- import { basename, join as join3 } from "path";
1332
+ import { basename, join as join3, resolve } from "path";
1333
1333
  import { createInterface } from "readline";
1334
1334
  import {
1335
1335
  assertBasouRootSafe as assertBasouRootSafe6,
@@ -1343,16 +1343,22 @@ import {
1343
1343
  importSessionFromJson,
1344
1344
  readManifest as readManifest3,
1345
1345
  readSessionYaml,
1346
+ reimportPreservingId,
1346
1347
  resolveRepositoryRoot as resolveRepositoryRoot6,
1347
1348
  SessionImportPayloadSchema
1348
1349
  } from "@basou/core";
1349
1350
  var SES_PREFIX2 = "ses_";
1350
1351
  var SHORT_ID_LEN2 = 6;
1352
+ function collectPath(value, previous) {
1353
+ return [...previous, value];
1354
+ }
1351
1355
  function registerImportCommand(program) {
1352
1356
  const importCmd = program.command("import").description("Import provenance from an external AI tool's native logs");
1353
1357
  importCmd.command("claude-code").description("Derive Basou sessions from Claude Code native transcripts (~/.claude/projects)").option(
1354
1358
  "--project <path>",
1355
- "Source project path whose transcripts to import (defaults to the current repository root)"
1359
+ "Source project path whose transcripts to import (repeatable; defaults to the manifest source roots, then the repository root)",
1360
+ collectPath,
1361
+ []
1356
1362
  ).option("--session <id>", "Import a single transcript by its Claude session id").option("--all", "Import every transcript found for the project").option(
1357
1363
  "--force",
1358
1364
  "Re-import sessions already imported: delete and replace them instead of skipping"
@@ -1361,7 +1367,9 @@ function registerImportCommand(program) {
1361
1367
  });
1362
1368
  importCmd.command("codex").description("Derive Basou sessions from OpenAI Codex native rollout logs (~/.codex/sessions)").option(
1363
1369
  "--project <path>",
1364
- "Source project path whose rollouts to import (defaults to the current repository root)"
1370
+ "Source project path whose rollouts to import (repeatable; defaults to the manifest source roots, then the repository root)",
1371
+ collectPath,
1372
+ []
1365
1373
  ).option("--session <id>", "Import a single rollout by its Codex session id").option("--all", "Import every rollout found for the project").option(
1366
1374
  "--force",
1367
1375
  "Re-import sessions already imported: delete and replace them instead of skipping"
@@ -1385,21 +1393,41 @@ async function runImportCodex(options, ctx = {}) {
1385
1393
  process.exitCode = 1;
1386
1394
  }
1387
1395
  }
1396
+ function resolveSourceRoots(args) {
1397
+ const { projectFlags, manifest, repoRoot, cwd } = args;
1398
+ let resolved;
1399
+ if (projectFlags.length > 0) {
1400
+ resolved = projectFlags.map((p) => resolve(cwd, p));
1401
+ } else {
1402
+ const roots = manifest.import?.source_roots;
1403
+ resolved = roots !== void 0 && roots.length > 0 ? roots.map((r) => resolve(repoRoot, r)) : [repoRoot];
1404
+ }
1405
+ return [...new Set(resolved)];
1406
+ }
1388
1407
  async function doRunImportClaudeCode(options, ctx) {
1389
1408
  assertSelector(options);
1390
1409
  const { repositoryRoot, paths, manifest } = await resolveImportTarget(ctx);
1391
- const projectPath = options.project ?? repositoryRoot;
1410
+ const projectPaths = resolveSourceRoots({
1411
+ projectFlags: options.project ?? [],
1412
+ manifest,
1413
+ repoRoot: repositoryRoot,
1414
+ cwd: ctx.cwd ?? process.cwd()
1415
+ });
1392
1416
  const projectsRoot = ctx.claudeProjectsDir ?? join3(homedir2(), ".claude", "projects");
1393
- const transcriptDir = join3(projectsRoot, encodeProjectDir(projectPath));
1394
- const files = await selectTranscriptFiles(transcriptDir, options);
1417
+ const files = await selectTranscriptFiles(projectsRoot, projectPaths, options);
1395
1418
  const candidates = files.map((file) => {
1396
1419
  const externalId = basename(file, ".jsonl");
1397
1420
  return {
1398
1421
  externalId,
1399
- toPayload: async () => claudeTranscriptToImportPayload(await readJsonlRecords(file), {
1400
- workspaceId: manifest.workspace.id,
1401
- externalId
1402
- })
1422
+ sourcePath: file,
1423
+ toPayload: async () => {
1424
+ const { records, sizeBytes } = await readJsonlRecords(file);
1425
+ return claudeTranscriptToImportPayload(records, {
1426
+ workspaceId: manifest.workspace.id,
1427
+ externalId,
1428
+ sourceSizeBytes: sizeBytes
1429
+ });
1430
+ }
1403
1431
  };
1404
1432
  });
1405
1433
  await importDerivedSessions(paths, manifest, options, CLAUDE_IMPORT_SOURCE, candidates);
@@ -1407,19 +1435,32 @@ async function doRunImportClaudeCode(options, ctx) {
1407
1435
  async function doRunImportCodex(options, ctx) {
1408
1436
  assertSelector(options);
1409
1437
  const { repositoryRoot, paths, manifest } = await resolveImportTarget(ctx);
1410
- const projectPath = options.project ?? repositoryRoot;
1438
+ const projectPaths = resolveSourceRoots({
1439
+ projectFlags: options.project ?? [],
1440
+ manifest,
1441
+ repoRoot: repositoryRoot,
1442
+ cwd: ctx.cwd ?? process.cwd()
1443
+ });
1411
1444
  const sessionsRoot = ctx.codexSessionsDir ?? join3(homedir2(), ".codex", "sessions");
1412
- const rollouts = await discoverCodexRollouts(sessionsRoot, projectPath, options);
1445
+ const rollouts = await discoverCodexRollouts(sessionsRoot, projectPaths, options);
1413
1446
  const candidates = rollouts.map(({ file, externalId }) => ({
1414
1447
  externalId,
1415
- toPayload: async () => codexRolloutToImportPayload(await readJsonlRecords(file), {
1416
- workspaceId: manifest.workspace.id,
1417
- externalId
1418
- })
1448
+ sourcePath: file,
1449
+ toPayload: async () => {
1450
+ const { records, sizeBytes } = await readJsonlRecords(file);
1451
+ return codexRolloutToImportPayload(records, {
1452
+ workspaceId: manifest.workspace.id,
1453
+ externalId,
1454
+ sourceSizeBytes: sizeBytes
1455
+ });
1456
+ }
1419
1457
  }));
1420
1458
  await importDerivedSessions(paths, manifest, options, CODEX_IMPORT_SOURCE, candidates);
1421
1459
  }
1422
1460
  function assertSelector(options) {
1461
+ if (options.session !== void 0 && options.all === true) {
1462
+ throw new Error("Specify either --session <id> or --all, not both");
1463
+ }
1423
1464
  if (options.session === void 0 && options.all !== true) {
1424
1465
  throw new Error("Specify --session <id> or --all");
1425
1466
  }
@@ -1436,41 +1477,76 @@ async function importDerivedSessions(paths, manifest, options, sourceKind, candi
1436
1477
  const existingByExternalId = await loadExistingByExternalId(paths, sourceKind);
1437
1478
  const seenThisRun = /* @__PURE__ */ new Set();
1438
1479
  const results = [];
1439
- let skippedNoAction = 0;
1440
- let skippedExisting = 0;
1441
- let replaced = 0;
1480
+ const counts = {
1481
+ skippedNoAction: 0,
1482
+ skippedExisting: 0,
1483
+ replaced: 0,
1484
+ reimported: 0,
1485
+ skippedLegacy: 0,
1486
+ skippedDecreased: 0,
1487
+ skippedDuplicate: 0
1488
+ };
1442
1489
  let sanitizedPaths = 0;
1443
- for (const { externalId, toPayload } of candidates) {
1490
+ const validate = (payload) => {
1491
+ if (payload === null) return null;
1492
+ const parsed = SessionImportPayloadSchema.safeParse(payload);
1493
+ if (!parsed.success) {
1494
+ throw new Error("Invalid import payload", { cause: parsed.error });
1495
+ }
1496
+ if (parsed.data.schema_version !== "0.1.0") {
1497
+ throw new Error(`Unsupported import schema_version: ${parsed.data.schema_version}`);
1498
+ }
1499
+ return parsed.data;
1500
+ };
1501
+ for (const { externalId, sourcePath, toPayload } of candidates) {
1444
1502
  if (seenThisRun.has(externalId)) {
1445
- skippedExisting++;
1503
+ counts.skippedExisting++;
1446
1504
  continue;
1447
1505
  }
1448
- const priorSessionIds = existingByExternalId.get(externalId) ?? [];
1449
- if (priorSessionIds.length > 0 && options.force !== true) {
1450
- skippedExisting++;
1506
+ const priors = existingByExternalId.get(externalId) ?? [];
1507
+ if (priors.length > 0 && options.force !== true) {
1508
+ const prior = await classifyReimport(priors, sourcePath, externalId, counts);
1509
+ if (prior === null) continue;
1510
+ const payload2 = validate(await toPayload());
1511
+ if (payload2 === null) {
1512
+ counts.skippedNoAction++;
1513
+ continue;
1514
+ }
1515
+ const readSize = payload2.session.source.source_size_bytes;
1516
+ if (prior.sourceSizeBytes !== void 0 && readSize !== void 0 && readSize <= prior.sourceSizeBytes) {
1517
+ console.error(
1518
+ `Import: ${externalId} source changed during read (now ${readSize} <= ${prior.sourceSizeBytes} bytes); re-import skipped`
1519
+ );
1520
+ counts.skippedDecreased++;
1521
+ continue;
1522
+ }
1523
+ const outcome = await reimportPreservingId(paths, manifest, prior.sessionId, payload2, {
1524
+ dryRun: options.dryRun === true
1525
+ });
1526
+ if (outcome.status === "skipped") {
1527
+ const detail = outcome.reason === "prior_events_unreadable" ? "prior events.jsonl has unreadable lines" : "source changed in a non-append way (derived events would be dropped)";
1528
+ console.error(`Import: ${externalId} ${detail}; re-import skipped`);
1529
+ counts.skippedNoAction++;
1530
+ continue;
1531
+ }
1532
+ counts.reimported++;
1533
+ seenThisRun.add(externalId);
1451
1534
  continue;
1452
1535
  }
1453
- const payload = await toPayload();
1536
+ const payload = validate(await toPayload());
1454
1537
  if (payload === null) {
1455
- skippedNoAction++;
1538
+ counts.skippedNoAction++;
1456
1539
  continue;
1457
1540
  }
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) {
1541
+ if (priors.length > 0 && options.force === true) {
1466
1542
  if (options.dryRun !== true) {
1467
- for (const sid of priorSessionIds) {
1468
- await rm(join3(paths.sessions, sid), { recursive: true, force: true });
1543
+ for (const { sessionId } of priors) {
1544
+ await rm(join3(paths.sessions, sessionId), { recursive: true, force: true });
1469
1545
  }
1470
1546
  }
1471
- replaced++;
1547
+ counts.replaced++;
1472
1548
  }
1473
- const result = await importSessionFromJson(paths, manifest, parsed.data, {
1549
+ const result = await importSessionFromJson(paths, manifest, payload, {
1474
1550
  dryRun: options.dryRun === true
1475
1551
  });
1476
1552
  results.push(result);
@@ -1480,17 +1556,52 @@ async function importDerivedSessions(paths, manifest, options, sourceKind, candi
1480
1556
  if (sanitizedPaths > 0) {
1481
1557
  console.error(`Imported sessions: ${sanitizedPaths} path(s) sanitized`);
1482
1558
  }
1483
- printImportResult(options, results, { skippedNoAction, skippedExisting, replaced });
1559
+ printImportResult(options, results, counts);
1560
+ }
1561
+ async function classifyReimport(priors, sourcePath, externalId, counts) {
1562
+ if (priors.length > 1) {
1563
+ console.error(
1564
+ `Import: ${externalId} has ${priors.length} prior sessions; re-import skipped (use --force)`
1565
+ );
1566
+ counts.skippedDuplicate++;
1567
+ return null;
1568
+ }
1569
+ const prior = priors[0];
1570
+ if (prior === void 0) {
1571
+ counts.skippedExisting++;
1572
+ return null;
1573
+ }
1574
+ const currentSize = await statSize(sourcePath);
1575
+ if (currentSize === void 0) {
1576
+ counts.skippedExisting++;
1577
+ return null;
1578
+ }
1579
+ if (prior.sourceSizeBytes === void 0) {
1580
+ counts.skippedLegacy++;
1581
+ return null;
1582
+ }
1583
+ if (currentSize === prior.sourceSizeBytes) {
1584
+ counts.skippedExisting++;
1585
+ return null;
1586
+ }
1587
+ if (currentSize < prior.sourceSizeBytes) {
1588
+ console.error(
1589
+ `Import: ${externalId} source shrank (${currentSize} < ${prior.sourceSizeBytes} bytes); re-import skipped (use --force to replace)`
1590
+ );
1591
+ counts.skippedDecreased++;
1592
+ return null;
1593
+ }
1594
+ return prior;
1484
1595
  }
1485
1596
  function encodeProjectDir(projectPath) {
1486
1597
  return projectPath.replaceAll("/", "-");
1487
1598
  }
1488
1599
  async function loadExistingByExternalId(paths, sourceKind) {
1489
1600
  const byExternalId = /* @__PURE__ */ new Map();
1490
- const add = (externalId, sessionId) => {
1601
+ const add = (externalId, prior) => {
1491
1602
  const list = byExternalId.get(externalId);
1492
- if (list === void 0) byExternalId.set(externalId, [sessionId]);
1493
- else list.push(sessionId);
1603
+ if (list === void 0) byExternalId.set(externalId, [prior]);
1604
+ else list.push(prior);
1494
1605
  };
1495
1606
  let sessionIds;
1496
1607
  try {
@@ -1506,39 +1617,77 @@ async function loadExistingByExternalId(paths, sourceKind) {
1506
1617
  continue;
1507
1618
  }
1508
1619
  if (session.session.source.kind !== sourceKind) continue;
1620
+ const sourceSizeBytes = session.session.source.source_size_bytes;
1621
+ const prior = sourceSizeBytes !== void 0 ? { sessionId, sourceSizeBytes } : { sessionId };
1509
1622
  const ext = session.session.source.external_id;
1510
1623
  if (typeof ext === "string" && ext.length > 0) {
1511
- add(ext, sessionId);
1624
+ add(ext, prior);
1512
1625
  continue;
1513
1626
  }
1514
1627
  const label = session.session.label;
1515
1628
  const match = typeof label === "string" ? label.match(/^claude-code import (\S+)$/) : null;
1516
- if (match?.[1] !== void 0) add(match[1], sessionId);
1629
+ if (match?.[1] !== void 0) add(match[1], prior);
1517
1630
  }
1518
1631
  return byExternalId;
1519
1632
  }
1520
- async function selectTranscriptFiles(transcriptDir, options) {
1633
+ async function selectTranscriptFiles(projectsRoot, projectPaths, options) {
1521
1634
  if (options.session !== void 0) {
1522
- return [join3(transcriptDir, `${options.session}.jsonl`)];
1635
+ const matches = [];
1636
+ for (const projectPath of projectPaths) {
1637
+ const file = join3(projectsRoot, encodeProjectDir(projectPath), `${options.session}.jsonl`);
1638
+ if (await pathExists(file)) matches.push(file);
1639
+ }
1640
+ if (matches.length === 0) {
1641
+ throw new Error("Claude transcript not found for session id in project");
1642
+ }
1643
+ return [...new Set(matches)];
1644
+ }
1645
+ const files = [];
1646
+ let anyDirFound = false;
1647
+ for (const projectPath of projectPaths) {
1648
+ const transcriptDir = join3(projectsRoot, encodeProjectDir(projectPath));
1649
+ let entries;
1650
+ try {
1651
+ entries = await readdir(transcriptDir);
1652
+ } catch (error) {
1653
+ if (findErrorCode5(error, "ENOENT")) continue;
1654
+ throw new Error("Failed to read Claude transcript directory", { cause: error });
1655
+ }
1656
+ anyDirFound = true;
1657
+ for (const name of entries) {
1658
+ if (name.endsWith(".jsonl")) files.push(join3(transcriptDir, name));
1659
+ }
1660
+ }
1661
+ if (!anyDirFound) {
1662
+ throw new Error("Claude transcript directory not found for project");
1523
1663
  }
1524
- let entries;
1664
+ return [...new Set(files)].sort();
1665
+ }
1666
+ async function pathExists(file) {
1525
1667
  try {
1526
- entries = await readdir(transcriptDir);
1668
+ await stat(file);
1669
+ return true;
1527
1670
  } 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 });
1671
+ if (findErrorCode5(error, "ENOENT")) return false;
1672
+ throw error;
1673
+ }
1674
+ }
1675
+ async function statSize(file) {
1676
+ try {
1677
+ return (await stat(file)).size;
1678
+ } catch (error) {
1679
+ if (findErrorCode5(error, "ENOENT")) return void 0;
1680
+ throw error;
1532
1681
  }
1533
- return entries.filter((name) => name.endsWith(".jsonl")).sort().map((name) => join3(transcriptDir, name));
1534
1682
  }
1535
- async function discoverCodexRollouts(sessionsRoot, projectPath, options) {
1683
+ async function discoverCodexRollouts(sessionsRoot, projectPaths, options) {
1684
+ const projectSet = new Set(projectPaths);
1536
1685
  const files = await findRolloutFiles(sessionsRoot);
1537
1686
  const matched = [];
1538
1687
  for (const file of files) {
1539
1688
  const meta = await readRolloutMeta(file);
1540
1689
  if (meta === void 0) continue;
1541
- if (meta.cwd !== projectPath) continue;
1690
+ if (!projectSet.has(meta.cwd)) continue;
1542
1691
  if (options.session !== void 0 && meta.id !== options.session) continue;
1543
1692
  matched.push({ file, externalId: meta.id });
1544
1693
  }
@@ -1609,9 +1758,9 @@ async function readFirstLine(file) {
1609
1758
  }
1610
1759
  }
1611
1760
  async function readJsonlRecords(file) {
1612
- let body;
1761
+ let buffer;
1613
1762
  try {
1614
- body = await readFile(file, "utf8");
1763
+ buffer = await readFile(file);
1615
1764
  } catch (error) {
1616
1765
  if (findErrorCode5(error, "ENOENT")) {
1617
1766
  throw new Error("Source log not found", { cause: error });
@@ -1622,7 +1771,7 @@ async function readJsonlRecords(file) {
1622
1771
  throw new Error("Failed to read source log", { cause: error });
1623
1772
  }
1624
1773
  const records = [];
1625
- for (const line of body.split("\n")) {
1774
+ for (const line of buffer.toString("utf8").split("\n")) {
1626
1775
  const trimmed = line.trim();
1627
1776
  if (trimmed.length === 0) continue;
1628
1777
  try {
@@ -1633,7 +1782,7 @@ async function readJsonlRecords(file) {
1633
1782
  } catch {
1634
1783
  }
1635
1784
  }
1636
- return records;
1785
+ return { records, sizeBytes: buffer.length };
1637
1786
  }
1638
1787
  function isObject(value) {
1639
1788
  return typeof value === "object" && value !== null && !Array.isArray(value);
@@ -1641,7 +1790,15 @@ function isObject(value) {
1641
1790
  function printImportResult(options, results, counts) {
1642
1791
  const isDry = options.dryRun === true;
1643
1792
  const eventTotal = results.reduce((sum, r) => sum + r.eventCount, 0);
1644
- const { skippedNoAction, skippedExisting, replaced } = counts;
1793
+ const {
1794
+ skippedNoAction,
1795
+ skippedExisting,
1796
+ replaced,
1797
+ reimported,
1798
+ skippedLegacy,
1799
+ skippedDecreased,
1800
+ skippedDuplicate
1801
+ } = counts;
1645
1802
  if (options.json === true) {
1646
1803
  console.log(
1647
1804
  JSON.stringify({
@@ -1653,8 +1810,12 @@ function printImportResult(options, results, counts) {
1653
1810
  })),
1654
1811
  imported_count: results.length,
1655
1812
  replaced_count: replaced,
1813
+ reimported_count: reimported,
1656
1814
  skipped_no_action: skippedNoAction,
1657
1815
  skipped_already_imported: skippedExisting,
1816
+ skipped_legacy_untracked: skippedLegacy,
1817
+ skipped_decreased: skippedDecreased,
1818
+ skipped_duplicate: skippedDuplicate,
1658
1819
  event_total: eventTotal,
1659
1820
  dry_run: isDry
1660
1821
  })
@@ -1664,20 +1825,36 @@ function printImportResult(options, results, counts) {
1664
1825
  const skipParts = [];
1665
1826
  if (skippedNoAction > 0) skipParts.push(`${skippedNoAction} with no actions`);
1666
1827
  if (skippedExisting > 0) skipParts.push(`${skippedExisting} already imported`);
1828
+ if (skippedLegacy > 0) skipParts.push(`${skippedLegacy} legacy (untracked size)`);
1829
+ if (skippedDecreased > 0) skipParts.push(`${skippedDecreased} shrank`);
1830
+ if (skippedDuplicate > 0) skipParts.push(`${skippedDuplicate} duplicated`);
1667
1831
  const skipSuffix = skipParts.length > 0 ? `; skipped ${skipParts.join(", ")}` : "";
1668
1832
  const eventsPart = replaced > 0 ? `${eventTotal} events, ${replaced} replaced` : `${eventTotal} events`;
1669
- if (results.length === 0) {
1833
+ if (isDry) {
1834
+ const parts = [];
1835
+ if (results.length > 0) parts.push(`import ${results.length} session(s) (${eventsPart})`);
1836
+ if (reimported > 0) parts.push(`re-import ${reimported} changed session(s)`);
1837
+ const head = parts.length > 0 ? `Dry run: would ${parts.join(", ")}` : "Dry run: no changes";
1838
+ console.log(`${head}${skipSuffix}`);
1839
+ return;
1840
+ }
1841
+ if (results.length === 0 && reimported === 0) {
1670
1842
  console.log(
1671
1843
  skipParts.length > 0 ? `No new sessions imported (skipped ${skipParts.join(", ")})` : "No transcripts found to import"
1672
1844
  );
1673
1845
  return;
1674
1846
  }
1675
- if (isDry) {
1676
- console.log(`Dry run: would import ${results.length} session(s) (${eventsPart})${skipSuffix}`);
1677
- return;
1847
+ const segments = [];
1848
+ if (results.length > 0) {
1849
+ const single = results.length === 1 && results[0] !== void 0 ? ` (${shortId2(results[0].sessionId)})` : "";
1850
+ segments.push(`Imported ${results.length} session(s)${single} (${eventsPart})`);
1851
+ }
1852
+ if (reimported > 0) {
1853
+ segments.push(
1854
+ `${results.length > 0 ? "re-imported" : "Re-imported"} ${reimported} changed session(s)`
1855
+ );
1678
1856
  }
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}`);
1857
+ console.log(`${segments.join(", ")}${skipSuffix}`);
1681
1858
  }
1682
1859
  function shortId2(id) {
1683
1860
  if (id.startsWith(SES_PREFIX2)) {
@@ -1709,7 +1886,7 @@ async function assertWorkspaceInitialized5(basouRoot) {
1709
1886
  }
1710
1887
 
1711
1888
  // src/commands/init.ts
1712
- import { basename as basename2 } from "path";
1889
+ import { basename as basename2, relative, resolve as resolve2 } from "path";
1713
1890
  import {
1714
1891
  appendBasouGitignore,
1715
1892
  createManifest,
@@ -1718,10 +1895,18 @@ import {
1718
1895
  tryRemoteUrl,
1719
1896
  writeManifest
1720
1897
  } from "@basou/core";
1898
+ function collectValue(value, previous) {
1899
+ return [...previous, value];
1900
+ }
1721
1901
  function registerInitCommand(program) {
1722
1902
  program.command("init").description("Initialize a Basou workspace at the current Git repository root").option("--name <name>", "Workspace name (defaults to the repository directory name)").option("--project-name <name>", "Project display name").option("--project-description <description>", "Project description").option(
1723
1903
  "--repo-url <url>",
1724
1904
  "Repository URL (defaults to git remote.origin.url; pass empty string for null)"
1905
+ ).option(
1906
+ "--source-root <path>",
1907
+ "Extra import source root, relative to the repo root (repeatable; aggregates sibling repos into this workspace)",
1908
+ collectValue,
1909
+ []
1725
1910
  ).option("-f, --force", "Overwrite an existing manifest").option("-v, --verbose", "Show error causes").action(async (options) => {
1726
1911
  await runInit(options);
1727
1912
  });
@@ -1744,12 +1929,17 @@ async function doRunInit(options, ctx) {
1744
1929
  } else {
1745
1930
  repositoryUrl = await tryRemoteUrl(repositoryRoot);
1746
1931
  }
1932
+ const sourceRoots = (options.sourceRoot ?? []).map((p) => {
1933
+ const rel = relative(repositoryRoot, resolve2(cwd, p));
1934
+ return rel === "" ? "." : rel;
1935
+ });
1747
1936
  const paths = await ensureBasouDirectory(repositoryRoot);
1748
1937
  const manifest = createManifest({
1749
1938
  workspaceName,
1750
1939
  ...options.projectName !== void 0 ? { projectName: options.projectName } : {},
1751
1940
  ...options.projectDescription !== void 0 ? { projectDescription: options.projectDescription } : {},
1752
- ...repositoryUrl !== void 0 ? { repositoryUrl } : {}
1941
+ ...repositoryUrl !== void 0 ? { repositoryUrl } : {},
1942
+ ...sourceRoots.length > 0 ? { sourceRoots } : {}
1753
1943
  });
1754
1944
  await writeManifest(paths, manifest, { force: options.force === true });
1755
1945
  try {
@@ -1783,7 +1973,8 @@ async function resolveRepositoryRootForInit(cwd) {
1783
1973
  }
1784
1974
 
1785
1975
  // src/commands/refresh.ts
1786
- import { assertBasouRootSafe as assertBasouRootSafe7, basouPaths as basouPaths7, findErrorCode as findErrorCode6, resolveRepositoryRoot as resolveRepositoryRoot8 } from "@basou/core";
1976
+ import { assertBasouRootSafe as assertBasouRootSafe7, basouPaths as basouPaths7, findErrorCode as findErrorCode7, resolveRepositoryRoot as resolveRepositoryRoot8 } from "@basou/core";
1977
+ import { InvalidArgumentError as InvalidArgumentError2 } from "commander";
1787
1978
 
1788
1979
  // src/lib/provenance-actions.ts
1789
1980
  import {
@@ -1836,8 +2027,10 @@ async function runImport(adapter, fn) {
1836
2027
  status: "ran",
1837
2028
  importedCount: readCount(json.imported_count),
1838
2029
  replacedCount: readCount(json.replaced_count),
2030
+ reimportedCount: readCount(json.reimported_count),
1839
2031
  skippedNoAction: readCount(json.skipped_no_action),
1840
2032
  skippedAlreadyImported: readCount(json.skipped_already_imported),
2033
+ skippedLegacyUntracked: readCount(json.skipped_legacy_untracked),
1841
2034
  eventTotal: readCount(json.event_total),
1842
2035
  dryRun: json.dry_run === true
1843
2036
  };
@@ -1906,25 +2099,215 @@ async function refreshAll(args) {
1906
2099
  };
1907
2100
  }
1908
2101
 
2102
+ // src/commands/refresh-watch.ts
2103
+ import { readdir as readdir2, stat as stat2 } from "fs/promises";
2104
+ import { homedir as homedir3 } from "os";
2105
+ import { join as join4 } from "path";
2106
+ import { findErrorCode as findErrorCode6 } from "@basou/core";
2107
+ var DEFAULT_WATCH_INTERVAL_SEC = 30;
2108
+ var MIN_WATCH_INTERVAL_SEC = 5;
2109
+ var MAX_WATCH_INTERVAL_SEC = 86400;
2110
+ function watchedRoots(ctx) {
2111
+ return [
2112
+ ctx.codexSessionsDir ?? join4(homedir3(), ".codex", "sessions"),
2113
+ ctx.claudeProjectsDir ?? join4(homedir3(), ".claude", "projects")
2114
+ ];
2115
+ }
2116
+ async function scanSourceLogs(roots) {
2117
+ const out = /* @__PURE__ */ new Map();
2118
+ const walk = async (dir) => {
2119
+ let entries;
2120
+ try {
2121
+ entries = await readdir2(dir, { withFileTypes: true });
2122
+ } catch (error) {
2123
+ if (findErrorCode6(error, "ENOENT") || findErrorCode6(error, "ENOTDIR")) return;
2124
+ throw new Error("Failed to read a source log directory", { cause: error });
2125
+ }
2126
+ for (const entry of entries) {
2127
+ const full = join4(dir, entry.name);
2128
+ if (entry.isDirectory()) {
2129
+ await walk(full);
2130
+ } else if (entry.isFile() && entry.name.endsWith(".jsonl")) {
2131
+ try {
2132
+ const info = await stat2(full);
2133
+ out.set(full, { mtimeMs: info.mtimeMs, size: info.size });
2134
+ } catch (error) {
2135
+ if (findErrorCode6(error, "ENOENT")) continue;
2136
+ throw new Error("Failed to stat a source log file", { cause: error });
2137
+ }
2138
+ }
2139
+ }
2140
+ };
2141
+ for (const root of roots) await walk(root);
2142
+ return out;
2143
+ }
2144
+ function scansEqual(a, b) {
2145
+ if (a.size !== b.size) return false;
2146
+ for (const [path, sig] of a) {
2147
+ const other = b.get(path);
2148
+ if (other === void 0 || other.mtimeMs !== sig.mtimeMs || other.size !== sig.size) {
2149
+ return false;
2150
+ }
2151
+ }
2152
+ return true;
2153
+ }
2154
+ function changedCount(outcome) {
2155
+ return outcome.status === "ran" ? outcome.importedCount + outcome.reimportedCount + outcome.replacedCount : 0;
2156
+ }
2157
+ function describeOutcome(outcome) {
2158
+ if (outcome.status !== "ran") return `${outcome.adapter} skipped`;
2159
+ const reimported = outcome.reimportedCount > 0 ? ` ~${outcome.reimportedCount}` : "";
2160
+ return `${outcome.adapter} +${outcome.importedCount}${reimported}`;
2161
+ }
2162
+ function hms(date) {
2163
+ return date.toISOString().slice(11, 19);
2164
+ }
2165
+ async function runImports(deps) {
2166
+ const claude = await importClaudeCode(deps.importOptions, deps.ctx);
2167
+ const codex = await importCodex(deps.importOptions, deps.ctx);
2168
+ return { claude, codex, changed: changedCount(claude) + changedCount(codex) };
2169
+ }
2170
+ async function regenerate(deps) {
2171
+ const nowIso = deps.now().toISOString();
2172
+ const handoff = await regenerateHandoff(deps.paths, nowIso);
2173
+ await regenerateDecisions(deps.paths, nowIso);
2174
+ return handoff.sessionCount;
2175
+ }
2176
+ async function runRefreshWatch(deps) {
2177
+ const { intervalMs, ctx, signal, sleep, log } = deps;
2178
+ const roots = watchedRoots(ctx);
2179
+ log(
2180
+ `watching ${roots.join(", ")} every ${Math.round(intervalMs / 1e3)}s (imports on change; Ctrl-C to stop)`
2181
+ );
2182
+ let lastScan = await scanSourceLogs(roots);
2183
+ let importedScan = lastScan;
2184
+ const initial = await runImports(deps);
2185
+ const initialSessions = await regenerate(deps);
2186
+ log(
2187
+ `[${hms(deps.now())}] refreshed: ${describeOutcome(initial.codex)}, ${describeOutcome(initial.claude)} (sessions: ${initialSessions})`
2188
+ );
2189
+ if (signal.aborted) {
2190
+ log("watch stopped");
2191
+ return;
2192
+ }
2193
+ let pendingRegen = false;
2194
+ while (!signal.aborted) {
2195
+ await sleep(intervalMs, signal);
2196
+ if (signal.aborted) break;
2197
+ try {
2198
+ const current = await scanSourceLogs(roots);
2199
+ if (scansEqual(current, lastScan) && !scansEqual(current, importedScan)) {
2200
+ const { claude, codex, changed } = await runImports(deps);
2201
+ if (changed > 0) pendingRegen = true;
2202
+ if (pendingRegen) {
2203
+ const sessions = await regenerate(deps);
2204
+ pendingRegen = false;
2205
+ log(
2206
+ `[${hms(deps.now())}] refreshed: ${describeOutcome(codex)}, ${describeOutcome(claude)} (sessions: ${sessions})`
2207
+ );
2208
+ }
2209
+ importedScan = current;
2210
+ }
2211
+ lastScan = current;
2212
+ } catch (error) {
2213
+ const message = error instanceof Error ? error.message : String(error);
2214
+ log(`[${hms(deps.now())}] refresh cycle skipped: ${message}`);
2215
+ }
2216
+ }
2217
+ log("watch stopped");
2218
+ }
2219
+
1909
2220
  // src/commands/refresh.ts
2221
+ function collectPath2(value, previous) {
2222
+ return [...previous, value];
2223
+ }
2224
+ function parseInterval(value) {
2225
+ const seconds = Number(value);
2226
+ if (!Number.isInteger(seconds) || seconds < MIN_WATCH_INTERVAL_SEC || seconds > MAX_WATCH_INTERVAL_SEC) {
2227
+ throw new InvalidArgumentError2(
2228
+ `--interval must be an integer between ${MIN_WATCH_INTERVAL_SEC} and ${MAX_WATCH_INTERVAL_SEC} (seconds).`
2229
+ );
2230
+ }
2231
+ return seconds;
2232
+ }
2233
+ function abortableSleep(ms, signal) {
2234
+ return new Promise((resolve3) => {
2235
+ if (signal.aborted) {
2236
+ resolve3();
2237
+ return;
2238
+ }
2239
+ let timer;
2240
+ const onAbort = () => {
2241
+ clearTimeout(timer);
2242
+ resolve3();
2243
+ };
2244
+ timer = setTimeout(() => {
2245
+ signal.removeEventListener("abort", onAbort);
2246
+ resolve3();
2247
+ }, ms);
2248
+ signal.addEventListener("abort", onAbort, { once: true });
2249
+ });
2250
+ }
1910
2251
  function registerRefreshCommand(program) {
1911
2252
  program.command("refresh").description(
1912
2253
  "Import all adapters for the project and regenerate handoff + decisions in one step"
1913
2254
  ).option(
1914
2255
  "--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) => {
2256
+ "Source project path to import (repeatable; defaults to the manifest source roots, then the repository root)",
2257
+ collectPath2,
2258
+ []
2259
+ ).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(
2260
+ "--watch",
2261
+ "Keep running: re-import + regenerate when the native logs change (Ctrl-C to stop)"
2262
+ ).option(
2263
+ "--interval <seconds>",
2264
+ `Poll interval for --watch, in seconds (default ${DEFAULT_WATCH_INTERVAL_SEC}, min ${MIN_WATCH_INTERVAL_SEC})`,
2265
+ parseInterval
2266
+ ).option("-v, --verbose", "Show error causes").action(async (options) => {
1917
2267
  await runRefresh(options);
1918
2268
  });
1919
2269
  }
1920
2270
  async function runRefresh(options, ctx = {}) {
1921
2271
  try {
1922
- await doRunRefresh(options, ctx);
2272
+ if (options.watch === true) {
2273
+ await doRunRefreshWatch(options, ctx);
2274
+ } else {
2275
+ await doRunRefresh(options, ctx);
2276
+ }
1923
2277
  } catch (error) {
1924
2278
  renderCliError(error, { verbose: isVerbose(options) });
1925
2279
  process.exitCode = 1;
1926
2280
  }
1927
2281
  }
2282
+ async function doRunRefreshWatch(options, ctx) {
2283
+ if (options.dryRun === true) throw new Error("--watch cannot be combined with --dry-run.");
2284
+ if (options.json === true) throw new Error("--watch cannot be combined with --json.");
2285
+ if (options.force === true) throw new Error("--watch cannot be combined with --force.");
2286
+ const cwd = ctx.cwd ?? process.cwd();
2287
+ const repositoryRoot = await resolveRepositoryRootForRefresh(cwd);
2288
+ const paths = basouPaths7(repositoryRoot);
2289
+ await assertWorkspaceInitialized6(paths.root);
2290
+ const intervalMs = (options.interval ?? DEFAULT_WATCH_INTERVAL_SEC) * 1e3;
2291
+ const controller = new AbortController();
2292
+ const onSignal = () => controller.abort();
2293
+ process.on("SIGINT", onSignal);
2294
+ process.on("SIGTERM", onSignal);
2295
+ try {
2296
+ await runRefreshWatch({
2297
+ ctx,
2298
+ paths,
2299
+ intervalMs,
2300
+ importOptions: options.project !== void 0 && options.project.length > 0 ? { project: options.project } : {},
2301
+ now: () => ctx.nowProvider?.() ?? /* @__PURE__ */ new Date(),
2302
+ signal: controller.signal,
2303
+ sleep: abortableSleep,
2304
+ log: (line) => console.log(line)
2305
+ });
2306
+ } finally {
2307
+ process.off("SIGINT", onSignal);
2308
+ process.off("SIGTERM", onSignal);
2309
+ }
2310
+ }
1928
2311
  async function doRunRefresh(options, ctx) {
1929
2312
  const cwd = ctx.cwd ?? process.cwd();
1930
2313
  const repositoryRoot = await resolveRepositoryRootForRefresh(cwd);
@@ -1933,7 +2316,7 @@ async function doRunRefresh(options, ctx) {
1933
2316
  const nowIso = (ctx.nowProvider?.() ?? /* @__PURE__ */ new Date()).toISOString();
1934
2317
  const result = await refreshAll({
1935
2318
  options: {
1936
- ...options.project !== void 0 ? { project: options.project } : {},
2319
+ ...options.project !== void 0 && options.project.length > 0 ? { project: options.project } : {},
1937
2320
  ...options.force === true ? { force: true } : {},
1938
2321
  ...options.dryRun === true ? { dryRun: true } : {}
1939
2322
  },
@@ -1954,9 +2337,11 @@ function describeImport(outcome) {
1954
2337
  }
1955
2338
  const verb = outcome.dryRun ? "would import" : "imported";
1956
2339
  const parts = [`${outcome.importedCount} session(s)`, `${outcome.eventTotal} events`];
2340
+ if (outcome.reimportedCount > 0) parts.push(`${outcome.reimportedCount} re-imported`);
1957
2341
  if (outcome.replacedCount > 0) parts.push(`${outcome.replacedCount} replaced`);
1958
2342
  if (outcome.skippedAlreadyImported > 0)
1959
2343
  parts.push(`${outcome.skippedAlreadyImported} already imported`);
2344
+ if (outcome.skippedLegacyUntracked > 0) parts.push(`${outcome.skippedLegacyUntracked} legacy`);
1960
2345
  return `${outcome.adapter}: ${verb} ${parts.join(", ")}`;
1961
2346
  }
1962
2347
  function printRefreshSummary(result) {
@@ -1991,7 +2376,7 @@ async function assertWorkspaceInitialized6(basouRoot) {
1991
2376
  try {
1992
2377
  await assertBasouRootSafe7(basouRoot);
1993
2378
  } catch (error) {
1994
- if (findErrorCode6(error, "ENOENT")) {
2379
+ if (findErrorCode7(error, "ENOENT")) {
1995
2380
  throw new Error("Workspace not initialized. Run 'basou init' first.");
1996
2381
  }
1997
2382
  throw error;
@@ -2000,8 +2385,8 @@ async function assertWorkspaceInitialized6(basouRoot) {
2000
2385
 
2001
2386
  // src/commands/run.ts
2002
2387
  import { mkdir as mkdir2 } from "fs/promises";
2003
- import { homedir as homedir3 } from "os";
2004
- import { join as join4 } from "path";
2388
+ import { homedir as homedir4 } from "os";
2389
+ import { join as join5 } from "path";
2005
2390
  import {
2006
2391
  assertBasouRootSafe as assertBasouRootSafe8,
2007
2392
  basouPaths as basouPaths8,
@@ -2053,10 +2438,10 @@ async function runClaudeCode(args, options, ctx = {}) {
2053
2438
  await assertBasouRootSafe8(paths.root);
2054
2439
  const manifest = await readManifest4(paths);
2055
2440
  const sessionId = prefixedUlid4("ses");
2056
- const sessionDir = join4(paths.sessions, sessionId);
2441
+ const sessionDir = join5(paths.sessions, sessionId);
2057
2442
  await mkdir2(sessionDir, { recursive: true });
2058
2443
  const startedAt = now().toISOString();
2059
- const sessionYamlPath = join4(sessionDir, "session.yaml");
2444
+ const sessionYamlPath = join5(sessionDir, "session.yaml");
2060
2445
  const session = buildInitialSession2({
2061
2446
  id: sessionId,
2062
2447
  command,
@@ -2177,7 +2562,7 @@ async function runClaudeCode(args, options, ctx = {}) {
2177
2562
  const rawRelated = computeRelatedFiles(preSnapshot, postSnapshot, diff);
2178
2563
  const relatedFiles = sanitizeRelatedFiles(rawRelated, {
2179
2564
  workingDirectory: repoRoot,
2180
- homedir: homedir3()
2565
+ homedir: homedir4()
2181
2566
  }).sanitized;
2182
2567
  const finalStatus = decideFinalStatus2(result, signalReceived);
2183
2568
  await appendEvent2(sessionDir, {
@@ -2321,7 +2706,7 @@ function buildInitialSession2(input) {
2321
2706
  source: { ...claudeCodeAdapterMetadata },
2322
2707
  started_at: input.startedAt,
2323
2708
  status: "initialized",
2324
- working_directory: sanitizeWorkingDirectory2(input.cwd, { homedir: homedir3() }),
2709
+ working_directory: sanitizeWorkingDirectory2(input.cwd, { homedir: homedir4() }),
2325
2710
  invocation: {
2326
2711
  command: input.command,
2327
2712
  args: [...input.args],
@@ -2394,13 +2779,13 @@ async function resolveRepositoryRootForRun(cwd) {
2394
2779
 
2395
2780
  // src/commands/session.ts
2396
2781
  import { readFile as readFile2 } from "fs/promises";
2397
- import { basename as basename3, isAbsolute, join as join5, relative } from "path";
2782
+ import { basename as basename3, isAbsolute, join as join6, relative as relative2 } from "path";
2398
2783
  import {
2399
2784
  acquireLock as acquireLock2,
2400
2785
  appendEventToExistingSession as appendEventToExistingSession2,
2401
2786
  assertBasouRootSafe as assertBasouRootSafe9,
2402
2787
  basouPaths as basouPaths9,
2403
- findErrorCode as findErrorCode7,
2788
+ findErrorCode as findErrorCode8,
2404
2789
  importSessionFromJson as importSessionFromJson2,
2405
2790
  loadSessionEntries,
2406
2791
  readAllEvents,
@@ -2414,7 +2799,7 @@ import {
2414
2799
  SessionStatusSchema,
2415
2800
  sessionWorkStatsFromEvents
2416
2801
  } from "@basou/core";
2417
- import { InvalidArgumentError as InvalidArgumentError2 } from "commander";
2802
+ import { InvalidArgumentError as InvalidArgumentError3 } from "commander";
2418
2803
 
2419
2804
  // src/lib/format-duration.ts
2420
2805
  function formatDurationMs(ms) {
@@ -2521,14 +2906,14 @@ async function doRunSessionShow(idInput, options, ctx) {
2521
2906
  const paths = basouPaths9(repositoryRoot);
2522
2907
  await assertWorkspaceInitialized7(paths.root);
2523
2908
  const sessionId = await resolveSessionId2(paths, idInput);
2524
- const sessionDir = join5(paths.sessions, sessionId);
2525
- const sessionYamlPath = join5(sessionDir, "session.yaml");
2909
+ const sessionDir = join6(paths.sessions, sessionId);
2910
+ const sessionYamlPath = join6(sessionDir, "session.yaml");
2526
2911
  let session;
2527
2912
  try {
2528
2913
  const raw = await readYamlFile4(sessionYamlPath);
2529
2914
  session = SessionSchema3.parse(raw);
2530
2915
  } catch (error) {
2531
- if (findErrorCode7(error, "ENOENT")) {
2916
+ if (findErrorCode8(error, "ENOENT")) {
2532
2917
  throw new Error(`Session not found: ${idInput}`);
2533
2918
  }
2534
2919
  throw new Error("Failed to read session", { cause: error });
@@ -2648,7 +3033,7 @@ function formatWorkingDir(workingDir, repositoryRoot, options) {
2648
3033
  return workingDir;
2649
3034
  }
2650
3035
  if (workingDir === repositoryRoot) return "<repository_root>";
2651
- const rel = relative(repositoryRoot, workingDir);
3036
+ const rel = relative2(repositoryRoot, workingDir);
2652
3037
  if (rel.length === 0 || rel === ".") return "<repository_root>";
2653
3038
  if (rel.startsWith("..")) return rel;
2654
3039
  return `./${rel}`;
@@ -2778,7 +3163,7 @@ async function assertWorkspaceInitialized7(basouRoot) {
2778
3163
  try {
2779
3164
  await assertBasouRootSafe9(basouRoot);
2780
3165
  } catch (error) {
2781
- if (findErrorCode7(error, "ENOENT")) {
3166
+ if (findErrorCode8(error, "ENOENT")) {
2782
3167
  throw new Error("Workspace not initialized. Run 'basou init' first.");
2783
3168
  }
2784
3169
  throw error;
@@ -2847,10 +3232,10 @@ async function readInputFile(path) {
2847
3232
  try {
2848
3233
  return await readFile2(path, "utf8");
2849
3234
  } catch (error) {
2850
- if (findErrorCode7(error, "ENOENT")) {
3235
+ if (findErrorCode8(error, "ENOENT")) {
2851
3236
  throw new Error("Import source not found", { cause: error });
2852
3237
  }
2853
- if (findErrorCode7(error, "EISDIR")) {
3238
+ if (findErrorCode8(error, "EISDIR")) {
2854
3239
  throw new Error("Import source is not a file", { cause: error });
2855
3240
  }
2856
3241
  throw new Error("Failed to read import source", { cause: error });
@@ -2865,19 +3250,19 @@ function parseJsonStrict(body) {
2865
3250
  }
2866
3251
  function parseImportFormat(raw) {
2867
3252
  if (raw !== "json") {
2868
- throw new InvalidArgumentError2(`Unsupported format: ${raw}. Valid values: json`);
3253
+ throw new InvalidArgumentError3(`Unsupported format: ${raw}. Valid values: json`);
2869
3254
  }
2870
3255
  return "json";
2871
3256
  }
2872
3257
  function parseLabelOverride(raw) {
2873
3258
  if (raw.length === 0) {
2874
- throw new InvalidArgumentError2("Label must not be empty");
3259
+ throw new InvalidArgumentError3("Label must not be empty");
2875
3260
  }
2876
3261
  return raw;
2877
3262
  }
2878
3263
  function parseTaskIdOverride(raw) {
2879
3264
  if (raw.length === 0) {
2880
- throw new InvalidArgumentError2("Task id is empty");
3265
+ throw new InvalidArgumentError3("Task id is empty");
2881
3266
  }
2882
3267
  return raw;
2883
3268
  }
@@ -2964,10 +3349,10 @@ async function readNoteFile(path) {
2964
3349
  try {
2965
3350
  return await readFile2(path, "utf8");
2966
3351
  } catch (error) {
2967
- if (findErrorCode7(error, "ENOENT")) {
3352
+ if (findErrorCode8(error, "ENOENT")) {
2968
3353
  throw new Error("Note source not found", { cause: error });
2969
3354
  }
2970
- if (findErrorCode7(error, "EISDIR")) {
3355
+ if (findErrorCode8(error, "EISDIR")) {
2971
3356
  throw new Error("Note source is not a file", { cause: error });
2972
3357
  }
2973
3358
  throw new Error("Failed to read note source", { cause: error });
@@ -2975,7 +3360,7 @@ async function readNoteFile(path) {
2975
3360
  }
2976
3361
  function parseNoteBodyOption(raw) {
2977
3362
  if (raw.length === 0) {
2978
- throw new InvalidArgumentError2("--body must not be empty");
3363
+ throw new InvalidArgumentError3("--body must not be empty");
2979
3364
  }
2980
3365
  return raw;
2981
3366
  }
@@ -3001,7 +3386,7 @@ import {
3001
3386
  assertBasouRootSafe as assertBasouRootSafe10,
3002
3387
  basouPaths as basouPaths10,
3003
3388
  computeWorkStats,
3004
- findErrorCode as findErrorCode8,
3389
+ findErrorCode as findErrorCode9,
3005
3390
  resolveRepositoryRoot as resolveRepositoryRoot11
3006
3391
  } from "@basou/core";
3007
3392
  function registerStatsCommand(program) {
@@ -3119,7 +3504,7 @@ async function assertWorkspaceInitialized8(basouRoot) {
3119
3504
  try {
3120
3505
  await assertBasouRootSafe10(basouRoot);
3121
3506
  } catch (error) {
3122
- if (findErrorCode8(error, "ENOENT")) {
3507
+ if (findErrorCode9(error, "ENOENT")) {
3123
3508
  throw new Error("Workspace not initialized. Run 'basou init' first.");
3124
3509
  }
3125
3510
  throw error;
@@ -3131,7 +3516,7 @@ import {
3131
3516
  assertBasouRootSafe as assertBasouRootSafe11,
3132
3517
  basouPaths as basouPaths11,
3133
3518
  buildStatusSnapshot,
3134
- findErrorCode as findErrorCode9,
3519
+ findErrorCode as findErrorCode10,
3135
3520
  readManifest as readManifest6,
3136
3521
  resolveRepositoryRoot as resolveRepositoryRoot12,
3137
3522
  writeStatus
@@ -3156,7 +3541,7 @@ async function doRunStatus(options, ctx) {
3156
3541
  try {
3157
3542
  await assertBasouRootSafe11(paths.root);
3158
3543
  } catch (error) {
3159
- if (findErrorCode9(error, "ENOENT")) {
3544
+ if (findErrorCode10(error, "ENOENT")) {
3160
3545
  throw new Error("Workspace not initialized. Run 'basou init' first.");
3161
3546
  }
3162
3547
  throw error;
@@ -3165,7 +3550,7 @@ async function doRunStatus(options, ctx) {
3165
3550
  try {
3166
3551
  manifest = await readManifest6(paths);
3167
3552
  } catch (error) {
3168
- if (findErrorCode9(error, "ENOENT")) {
3553
+ if (findErrorCode10(error, "ENOENT")) {
3169
3554
  throw new Error("Workspace not initialized. Run 'basou init' first.");
3170
3555
  }
3171
3556
  throw new Error("Failed to read workspace manifest", { cause: error });
@@ -3202,7 +3587,7 @@ async function resolveRepositoryRootForStatus(cwd) {
3202
3587
 
3203
3588
  // src/commands/task.ts
3204
3589
  import { readFile as readFile3 } from "fs/promises";
3205
- import { join as join6 } from "path";
3590
+ import { join as join7 } from "path";
3206
3591
  import {
3207
3592
  archiveTask,
3208
3593
  assertBasouRootSafe as assertBasouRootSafe12,
@@ -3211,7 +3596,7 @@ import {
3211
3596
  deleteTask,
3212
3597
  editTask,
3213
3598
  enumerateArchivedTaskIds,
3214
- findErrorCode as findErrorCode10,
3599
+ findErrorCode as findErrorCode11,
3215
3600
  loadSessionEntries as loadSessionEntries2,
3216
3601
  loadTaskEntries,
3217
3602
  prefixedUlid as prefixedUlid5,
@@ -3229,7 +3614,7 @@ import {
3229
3614
  TaskWriteAfterEventError,
3230
3615
  updateTaskStatusWithEvent
3231
3616
  } from "@basou/core";
3232
- import { InvalidArgumentError as InvalidArgumentError3 } from "commander";
3617
+ import { InvalidArgumentError as InvalidArgumentError4 } from "commander";
3233
3618
  var STATUS_VALUES3 = TaskStatusSchema.options;
3234
3619
  function registerTaskCommand(program) {
3235
3620
  const task = program.command("task").description("Manage Basou tasks (purpose units that span sessions)");
@@ -3529,7 +3914,7 @@ async function doRunTaskShow(idInput, options, ctx) {
3529
3914
  const events = [];
3530
3915
  const linkedSessionIds = new Set(doc.task.task.linked_sessions);
3531
3916
  for (const s of sessions) {
3532
- const sessionDir = join6(paths.sessions, s.sessionId);
3917
+ const sessionDir = join7(paths.sessions, s.sessionId);
3533
3918
  try {
3534
3919
  for await (const ev of replayEvents2(sessionDir, {
3535
3920
  onWarning: (w) => printReplayWarning(w, s.sessionId)
@@ -4158,20 +4543,20 @@ async function readSingleLineFromStdin() {
4158
4543
  }
4159
4544
  function parseTitle2(raw) {
4160
4545
  if (raw.length === 0) {
4161
- throw new InvalidArgumentError3("Title must not be empty");
4546
+ throw new InvalidArgumentError4("Title must not be empty");
4162
4547
  }
4163
4548
  return raw;
4164
4549
  }
4165
4550
  function parseLabel(raw) {
4166
4551
  if (raw.length === 0) {
4167
- throw new InvalidArgumentError3("Label must not be empty");
4552
+ throw new InvalidArgumentError4("Label must not be empty");
4168
4553
  }
4169
4554
  return raw;
4170
4555
  }
4171
4556
  function parseInitialTaskStatus(raw) {
4172
4557
  const result = TaskStatusSchema.safeParse(raw);
4173
4558
  if (!result.success) {
4174
- throw new InvalidArgumentError3(
4559
+ throw new InvalidArgumentError4(
4175
4560
  `Initial task status must be one of: ${STATUS_VALUES3.join(", ")}`
4176
4561
  );
4177
4562
  }
@@ -4180,7 +4565,7 @@ function parseInitialTaskStatus(raw) {
4180
4565
  var ISO_DATE_RE = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:\d{2})$/;
4181
4566
  function parseIsoTimestampOption(raw) {
4182
4567
  if (!ISO_DATE_RE.test(raw) || Number.isNaN(Date.parse(raw))) {
4183
- throw new InvalidArgumentError3(
4568
+ throw new InvalidArgumentError4(
4184
4569
  "Invalid --completed-at value; expected ISO-8601 timestamp like 2026-05-10T12:34:56+09:00"
4185
4570
  );
4186
4571
  }
@@ -4189,7 +4574,7 @@ function parseIsoTimestampOption(raw) {
4189
4574
  function parseTaskStatusFilter(raw) {
4190
4575
  const result = TaskStatusSchema.safeParse(raw);
4191
4576
  if (!result.success) {
4192
- throw new InvalidArgumentError3(
4577
+ throw new InvalidArgumentError4(
4193
4578
  `Invalid task status: ${raw}. Valid values: ${STATUS_VALUES3.join(", ")}`
4194
4579
  );
4195
4580
  }
@@ -4204,14 +4589,14 @@ function parseTaskStatusPositional(raw) {
4204
4589
  }
4205
4590
  function parseDescriptionOption(raw) {
4206
4591
  if (raw.length === 0) {
4207
- throw new InvalidArgumentError3("Description must not be empty");
4592
+ throw new InvalidArgumentError4("Description must not be empty");
4208
4593
  }
4209
4594
  return raw;
4210
4595
  }
4211
4596
  function parsePositiveInt2(raw) {
4212
4597
  const n = Number.parseInt(raw, 10);
4213
4598
  if (!Number.isInteger(n) || n < 1 || raw.trim() !== String(n)) {
4214
- throw new InvalidArgumentError3(`Invalid number: ${raw}`);
4599
+ throw new InvalidArgumentError4(`Invalid number: ${raw}`);
4215
4600
  }
4216
4601
  return n;
4217
4602
  }
@@ -4219,10 +4604,10 @@ async function readDescriptionFile(path) {
4219
4604
  try {
4220
4605
  return await readFile3(path, "utf8");
4221
4606
  } catch (error) {
4222
- if (findErrorCode10(error, "ENOENT")) {
4607
+ if (findErrorCode11(error, "ENOENT")) {
4223
4608
  throw new Error("Description source not found", { cause: error });
4224
4609
  }
4225
- if (findErrorCode10(error, "EISDIR")) {
4610
+ if (findErrorCode11(error, "EISDIR")) {
4226
4611
  throw new Error("Description source is not a file", { cause: error });
4227
4612
  }
4228
4613
  throw new Error("Failed to read description source", { cause: error });
@@ -4245,7 +4630,7 @@ async function assertWorkspaceInitialized9(basouRoot) {
4245
4630
  try {
4246
4631
  await assertBasouRootSafe12(basouRoot);
4247
4632
  } catch (error) {
4248
- if (findErrorCode10(error, "ENOENT")) {
4633
+ if (findErrorCode11(error, "ENOENT")) {
4249
4634
  throw new Error("Workspace not initialized. Run 'basou init' first.");
4250
4635
  }
4251
4636
  throw error;
@@ -4333,16 +4718,16 @@ function maxLen3(values, floor) {
4333
4718
 
4334
4719
  // src/commands/view.ts
4335
4720
  import { spawn } from "child_process";
4336
- import { assertBasouRootSafe as assertBasouRootSafe13, basouPaths as basouPaths13, findErrorCode as findErrorCode12, resolveRepositoryRoot as resolveRepositoryRoot14 } from "@basou/core";
4337
- import { InvalidArgumentError as InvalidArgumentError4 } from "commander";
4721
+ import { assertBasouRootSafe as assertBasouRootSafe13, basouPaths as basouPaths13, findErrorCode as findErrorCode13, resolveRepositoryRoot as resolveRepositoryRoot14 } from "@basou/core";
4722
+ import { InvalidArgumentError as InvalidArgumentError5 } from "commander";
4338
4723
 
4339
4724
  // src/lib/view-server.ts
4340
4725
  import { createServer } from "http";
4341
- import { join as join7 } from "path";
4726
+ import { join as join8 } from "path";
4342
4727
  import {
4343
4728
  computeWorkStats as computeWorkStats2,
4344
4729
  enumerateApprovals as enumerateApprovals2,
4345
- findErrorCode as findErrorCode11,
4730
+ findErrorCode as findErrorCode12,
4346
4731
  isLazyExpired as isLazyExpired2,
4347
4732
  loadApproval as loadApproval2,
4348
4733
  loadSessionEntries as loadSessionEntries3,
@@ -4407,7 +4792,7 @@ var VIEW_HTML = `<!doctype html>
4407
4792
  <body>
4408
4793
  <header>
4409
4794
  <h1>basou view</h1>
4410
- <input type="text" id="project" placeholder="project path" />
4795
+ <input type="text" id="project" placeholder="source root (optional override)" />
4411
4796
  <button class="primary" id="btn-refresh">Refresh all</button>
4412
4797
  <button id="btn-import-claude">Import claude-code</button>
4413
4798
  <button id="btn-import-codex">Import codex</button>
@@ -4561,7 +4946,10 @@ var VIEW_HTML = `<!doctype html>
4561
4946
  detail.appendChild(el('p', { class: 'muted', text: 'Workspace not initialized.' }));
4562
4947
  return;
4563
4948
  }
4564
- $('project').value = $('project').value || d.repoRoot || '';
4949
+ // Leave the project field empty by default so refresh / import use the
4950
+ // manifest's import.source_roots (then the repo root) -- pre-filling the
4951
+ // repo root here would send it as an explicit --project and silently
4952
+ // override multi-root source roots. The field is an optional override.
4565
4953
  state.repoRoot = d.repoRoot || '';
4566
4954
  detail.appendChild(el('p', {}, [
4567
4955
  el('strong', { text: d.workspace.name }), ' ',
@@ -4812,7 +5200,7 @@ function startViewServer(opts) {
4812
5200
  };
4813
5201
  let boundPort = port;
4814
5202
  const getPort = () => boundPort;
4815
- return new Promise((resolve, reject) => {
5203
+ return new Promise((resolve3, reject) => {
4816
5204
  const server = createServer((req, res) => {
4817
5205
  handleRequest(req, res, deps, getPort, runExclusive).catch((error) => {
4818
5206
  sendError(res, error instanceof HttpError ? error.status : 500, pathlessMessage(error));
@@ -4823,7 +5211,7 @@ function startViewServer(opts) {
4823
5211
  const address = server.address();
4824
5212
  boundPort = isAddressInfo(address) ? address.port : port;
4825
5213
  server.off("error", reject);
4826
- resolve({
5214
+ resolve3({
4827
5215
  url: `http://${host}:${boundPort}`,
4828
5216
  port: boundPort,
4829
5217
  close: () => closeServer(server)
@@ -4835,8 +5223,8 @@ function isAddressInfo(value) {
4835
5223
  return value !== null && typeof value === "object";
4836
5224
  }
4837
5225
  function closeServer(server) {
4838
- return new Promise((resolve) => {
4839
- server.close(() => resolve());
5226
+ return new Promise((resolve3) => {
5227
+ server.close(() => resolve3());
4840
5228
  server.closeAllConnections();
4841
5229
  });
4842
5230
  }
@@ -4941,7 +5329,7 @@ async function overview(deps) {
4941
5329
  try {
4942
5330
  manifest = await readManifest8(deps.paths);
4943
5331
  } catch (error) {
4944
- if (findErrorCode11(error, "ENOENT")) {
5332
+ if (findErrorCode12(error, "ENOENT")) {
4945
5333
  return { initialized: false, repoRoot: deps.repoRoot };
4946
5334
  }
4947
5335
  throw error;
@@ -4996,7 +5384,7 @@ async function sessionDetail(deps, sessionId) {
4996
5384
  throw error;
4997
5385
  }
4998
5386
  try {
4999
- const events = await readAllEvents2(join7(deps.paths.sessions, sessionId));
5387
+ const events = await readAllEvents2(join8(deps.paths.sessions, sessionId));
5000
5388
  return { session, events };
5001
5389
  } catch {
5002
5390
  return { session, events: [], degraded: true };
@@ -5051,11 +5439,16 @@ async function handoffView(deps) {
5051
5439
  }
5052
5440
  function readActionOptions(body) {
5053
5441
  const options = {};
5054
- if (typeof body.project === "string" && body.project.length > 0) options.project = body.project;
5442
+ const project = normalizeProject(body.project);
5443
+ if (project.length > 0) options.project = project;
5055
5444
  if (body.force === true) options.force = true;
5056
5445
  if (body.dryRun === true) options.dryRun = true;
5057
5446
  return options;
5058
5447
  }
5448
+ function normalizeProject(value) {
5449
+ const raw = Array.isArray(value) ? value : [value];
5450
+ return raw.filter((p) => typeof p === "string" && p.length > 0);
5451
+ }
5059
5452
  function hostAllowed(req, port) {
5060
5453
  const host = req.headers.host;
5061
5454
  return host === `127.0.0.1:${port}` || host === `localhost:${port}`;
@@ -5131,7 +5524,7 @@ var DEFAULT_PORT = 4319;
5131
5524
  function parsePort(value) {
5132
5525
  const port = Number.parseInt(value, 10);
5133
5526
  if (!Number.isInteger(port) || port < 1 || port > 65535) {
5134
- throw new InvalidArgumentError4("Port must be an integer between 1 and 65535.");
5527
+ throw new InvalidArgumentError5("Port must be an integer between 1 and 65535.");
5135
5528
  }
5136
5529
  return port;
5137
5530
  }
@@ -5183,7 +5576,7 @@ async function startListening(port, deps) {
5183
5576
  try {
5184
5577
  return await startViewServer({ port, deps });
5185
5578
  } catch (error) {
5186
- if (findErrorCode12(error, "EADDRINUSE")) {
5579
+ if (findErrorCode13(error, "EADDRINUSE")) {
5187
5580
  throw new Error(`Port ${port} is already in use. Pass --port <n> to choose another.`, {
5188
5581
  cause: error
5189
5582
  });
@@ -5206,7 +5599,7 @@ function openInBrowser(url, override) {
5206
5599
  }
5207
5600
  }
5208
5601
  function waitForShutdown(signal) {
5209
- return new Promise((resolve) => {
5602
+ return new Promise((resolve3) => {
5210
5603
  const cleanup = () => {
5211
5604
  process.off("SIGINT", onSignal);
5212
5605
  process.off("SIGTERM", onSignal);
@@ -5214,18 +5607,18 @@ function waitForShutdown(signal) {
5214
5607
  };
5215
5608
  const onSignal = () => {
5216
5609
  cleanup();
5217
- resolve();
5610
+ resolve3();
5218
5611
  };
5219
5612
  const onAbort = () => {
5220
5613
  cleanup();
5221
- resolve();
5614
+ resolve3();
5222
5615
  };
5223
5616
  process.on("SIGINT", onSignal);
5224
5617
  process.on("SIGTERM", onSignal);
5225
5618
  if (signal !== void 0) {
5226
5619
  if (signal.aborted) {
5227
5620
  cleanup();
5228
- resolve();
5621
+ resolve3();
5229
5622
  return;
5230
5623
  }
5231
5624
  signal.addEventListener("abort", onAbort);
@@ -5248,7 +5641,7 @@ async function assertWorkspaceInitialized10(basouRoot) {
5248
5641
  try {
5249
5642
  await assertBasouRootSafe13(basouRoot);
5250
5643
  } catch (error) {
5251
- if (findErrorCode12(error, "ENOENT")) {
5644
+ if (findErrorCode13(error, "ENOENT")) {
5252
5645
  throw new Error("Workspace not initialized. Run 'basou init' first.");
5253
5646
  }
5254
5647
  throw error;