@basou/cli 0.7.0 → 0.9.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 +376 -67
- package/dist/index.js.map +1 -1
- package/dist/program.js +376 -67
- package/dist/program.js.map +1 -1
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -124,6 +124,7 @@ import {
|
|
|
124
124
|
linkYamlFile,
|
|
125
125
|
loadApproval,
|
|
126
126
|
prefixedUlid,
|
|
127
|
+
readSessionYaml,
|
|
127
128
|
readYamlFile,
|
|
128
129
|
replayEvents,
|
|
129
130
|
resolveRepositoryRoot
|
|
@@ -331,6 +332,15 @@ async function doRunApprovalResolve(idInput, options, ctx, decision) {
|
|
|
331
332
|
if (isLazyExpired(approval, now)) {
|
|
332
333
|
throw new Error(`Approval already expired: ${idInput}`);
|
|
333
334
|
}
|
|
335
|
+
let sessionStatus = null;
|
|
336
|
+
try {
|
|
337
|
+
sessionStatus = (await readSessionYaml(paths, approval.session_id)).session.status;
|
|
338
|
+
} catch {
|
|
339
|
+
sessionStatus = null;
|
|
340
|
+
}
|
|
341
|
+
if (sessionStatus === "imported") {
|
|
342
|
+
throw new Error(`Cannot resolve an approval for an imported session: ${idInput}`);
|
|
343
|
+
}
|
|
334
344
|
const occurredAt = now.toISOString();
|
|
335
345
|
const eventId = prefixedUlid("evt");
|
|
336
346
|
if (decision === "approve") {
|
|
@@ -1342,7 +1352,8 @@ import {
|
|
|
1342
1352
|
findErrorCode as findErrorCode5,
|
|
1343
1353
|
importSessionFromJson,
|
|
1344
1354
|
readManifest as readManifest3,
|
|
1345
|
-
readSessionYaml,
|
|
1355
|
+
readSessionYaml as readSessionYaml2,
|
|
1356
|
+
reimportPreservingId,
|
|
1346
1357
|
resolveRepositoryRoot as resolveRepositoryRoot6,
|
|
1347
1358
|
SessionImportPayloadSchema
|
|
1348
1359
|
} from "@basou/core";
|
|
@@ -1418,10 +1429,15 @@ async function doRunImportClaudeCode(options, ctx) {
|
|
|
1418
1429
|
const externalId = basename(file, ".jsonl");
|
|
1419
1430
|
return {
|
|
1420
1431
|
externalId,
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1432
|
+
sourcePath: file,
|
|
1433
|
+
toPayload: async () => {
|
|
1434
|
+
const { records, sizeBytes } = await readJsonlRecords(file);
|
|
1435
|
+
return claudeTranscriptToImportPayload(records, {
|
|
1436
|
+
workspaceId: manifest.workspace.id,
|
|
1437
|
+
externalId,
|
|
1438
|
+
sourceSizeBytes: sizeBytes
|
|
1439
|
+
});
|
|
1440
|
+
}
|
|
1425
1441
|
};
|
|
1426
1442
|
});
|
|
1427
1443
|
await importDerivedSessions(paths, manifest, options, CLAUDE_IMPORT_SOURCE, candidates);
|
|
@@ -1439,10 +1455,15 @@ async function doRunImportCodex(options, ctx) {
|
|
|
1439
1455
|
const rollouts = await discoverCodexRollouts(sessionsRoot, projectPaths, options);
|
|
1440
1456
|
const candidates = rollouts.map(({ file, externalId }) => ({
|
|
1441
1457
|
externalId,
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1458
|
+
sourcePath: file,
|
|
1459
|
+
toPayload: async () => {
|
|
1460
|
+
const { records, sizeBytes } = await readJsonlRecords(file);
|
|
1461
|
+
return codexRolloutToImportPayload(records, {
|
|
1462
|
+
workspaceId: manifest.workspace.id,
|
|
1463
|
+
externalId,
|
|
1464
|
+
sourceSizeBytes: sizeBytes
|
|
1465
|
+
});
|
|
1466
|
+
}
|
|
1446
1467
|
}));
|
|
1447
1468
|
await importDerivedSessions(paths, manifest, options, CODEX_IMPORT_SOURCE, candidates);
|
|
1448
1469
|
}
|
|
@@ -1466,41 +1487,76 @@ async function importDerivedSessions(paths, manifest, options, sourceKind, candi
|
|
|
1466
1487
|
const existingByExternalId = await loadExistingByExternalId(paths, sourceKind);
|
|
1467
1488
|
const seenThisRun = /* @__PURE__ */ new Set();
|
|
1468
1489
|
const results = [];
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1490
|
+
const counts = {
|
|
1491
|
+
skippedNoAction: 0,
|
|
1492
|
+
skippedExisting: 0,
|
|
1493
|
+
replaced: 0,
|
|
1494
|
+
reimported: 0,
|
|
1495
|
+
skippedLegacy: 0,
|
|
1496
|
+
skippedDecreased: 0,
|
|
1497
|
+
skippedDuplicate: 0
|
|
1498
|
+
};
|
|
1472
1499
|
let sanitizedPaths = 0;
|
|
1473
|
-
|
|
1500
|
+
const validate = (payload) => {
|
|
1501
|
+
if (payload === null) return null;
|
|
1502
|
+
const parsed = SessionImportPayloadSchema.safeParse(payload);
|
|
1503
|
+
if (!parsed.success) {
|
|
1504
|
+
throw new Error("Invalid import payload", { cause: parsed.error });
|
|
1505
|
+
}
|
|
1506
|
+
if (parsed.data.schema_version !== "0.1.0") {
|
|
1507
|
+
throw new Error(`Unsupported import schema_version: ${parsed.data.schema_version}`);
|
|
1508
|
+
}
|
|
1509
|
+
return parsed.data;
|
|
1510
|
+
};
|
|
1511
|
+
for (const { externalId, sourcePath, toPayload } of candidates) {
|
|
1474
1512
|
if (seenThisRun.has(externalId)) {
|
|
1475
|
-
skippedExisting++;
|
|
1513
|
+
counts.skippedExisting++;
|
|
1476
1514
|
continue;
|
|
1477
1515
|
}
|
|
1478
|
-
const
|
|
1479
|
-
if (
|
|
1480
|
-
|
|
1516
|
+
const priors = existingByExternalId.get(externalId) ?? [];
|
|
1517
|
+
if (priors.length > 0 && options.force !== true) {
|
|
1518
|
+
const prior = await classifyReimport(priors, sourcePath, externalId, counts);
|
|
1519
|
+
if (prior === null) continue;
|
|
1520
|
+
const payload2 = validate(await toPayload());
|
|
1521
|
+
if (payload2 === null) {
|
|
1522
|
+
counts.skippedNoAction++;
|
|
1523
|
+
continue;
|
|
1524
|
+
}
|
|
1525
|
+
const readSize = payload2.session.source.source_size_bytes;
|
|
1526
|
+
if (prior.sourceSizeBytes !== void 0 && readSize !== void 0 && readSize <= prior.sourceSizeBytes) {
|
|
1527
|
+
console.error(
|
|
1528
|
+
`Import: ${externalId} source changed during read (now ${readSize} <= ${prior.sourceSizeBytes} bytes); re-import skipped`
|
|
1529
|
+
);
|
|
1530
|
+
counts.skippedDecreased++;
|
|
1531
|
+
continue;
|
|
1532
|
+
}
|
|
1533
|
+
const outcome = await reimportPreservingId(paths, manifest, prior.sessionId, payload2, {
|
|
1534
|
+
dryRun: options.dryRun === true
|
|
1535
|
+
});
|
|
1536
|
+
if (outcome.status === "skipped") {
|
|
1537
|
+
const detail = outcome.reason === "prior_events_unreadable" ? "prior events.jsonl has unreadable lines" : outcome.reason === "prior_chain_broken" ? "prior events.jsonl failed hash-chain verification (run 'basou verify')" : "source changed in a non-append way (derived events would be dropped)";
|
|
1538
|
+
console.error(`Import: ${externalId} ${detail}; re-import skipped`);
|
|
1539
|
+
counts.skippedNoAction++;
|
|
1540
|
+
continue;
|
|
1541
|
+
}
|
|
1542
|
+
counts.reimported++;
|
|
1543
|
+
seenThisRun.add(externalId);
|
|
1481
1544
|
continue;
|
|
1482
1545
|
}
|
|
1483
|
-
const payload = await toPayload();
|
|
1546
|
+
const payload = validate(await toPayload());
|
|
1484
1547
|
if (payload === null) {
|
|
1485
|
-
skippedNoAction++;
|
|
1548
|
+
counts.skippedNoAction++;
|
|
1486
1549
|
continue;
|
|
1487
1550
|
}
|
|
1488
|
-
|
|
1489
|
-
if (!parsed.success) {
|
|
1490
|
-
throw new Error("Invalid import payload", { cause: parsed.error });
|
|
1491
|
-
}
|
|
1492
|
-
if (parsed.data.schema_version !== "0.1.0") {
|
|
1493
|
-
throw new Error(`Unsupported import schema_version: ${parsed.data.schema_version}`);
|
|
1494
|
-
}
|
|
1495
|
-
if (priorSessionIds.length > 0 && options.force === true) {
|
|
1551
|
+
if (priors.length > 0 && options.force === true) {
|
|
1496
1552
|
if (options.dryRun !== true) {
|
|
1497
|
-
for (const
|
|
1498
|
-
await rm(join3(paths.sessions,
|
|
1553
|
+
for (const { sessionId } of priors) {
|
|
1554
|
+
await rm(join3(paths.sessions, sessionId), { recursive: true, force: true });
|
|
1499
1555
|
}
|
|
1500
1556
|
}
|
|
1501
|
-
replaced++;
|
|
1557
|
+
counts.replaced++;
|
|
1502
1558
|
}
|
|
1503
|
-
const result = await importSessionFromJson(paths, manifest,
|
|
1559
|
+
const result = await importSessionFromJson(paths, manifest, payload, {
|
|
1504
1560
|
dryRun: options.dryRun === true
|
|
1505
1561
|
});
|
|
1506
1562
|
results.push(result);
|
|
@@ -1510,17 +1566,52 @@ async function importDerivedSessions(paths, manifest, options, sourceKind, candi
|
|
|
1510
1566
|
if (sanitizedPaths > 0) {
|
|
1511
1567
|
console.error(`Imported sessions: ${sanitizedPaths} path(s) sanitized`);
|
|
1512
1568
|
}
|
|
1513
|
-
printImportResult(options, results,
|
|
1569
|
+
printImportResult(options, results, counts);
|
|
1570
|
+
}
|
|
1571
|
+
async function classifyReimport(priors, sourcePath, externalId, counts) {
|
|
1572
|
+
if (priors.length > 1) {
|
|
1573
|
+
console.error(
|
|
1574
|
+
`Import: ${externalId} has ${priors.length} prior sessions; re-import skipped (use --force)`
|
|
1575
|
+
);
|
|
1576
|
+
counts.skippedDuplicate++;
|
|
1577
|
+
return null;
|
|
1578
|
+
}
|
|
1579
|
+
const prior = priors[0];
|
|
1580
|
+
if (prior === void 0) {
|
|
1581
|
+
counts.skippedExisting++;
|
|
1582
|
+
return null;
|
|
1583
|
+
}
|
|
1584
|
+
const currentSize = await statSize(sourcePath);
|
|
1585
|
+
if (currentSize === void 0) {
|
|
1586
|
+
counts.skippedExisting++;
|
|
1587
|
+
return null;
|
|
1588
|
+
}
|
|
1589
|
+
if (prior.sourceSizeBytes === void 0) {
|
|
1590
|
+
counts.skippedLegacy++;
|
|
1591
|
+
return null;
|
|
1592
|
+
}
|
|
1593
|
+
if (currentSize === prior.sourceSizeBytes) {
|
|
1594
|
+
counts.skippedExisting++;
|
|
1595
|
+
return null;
|
|
1596
|
+
}
|
|
1597
|
+
if (currentSize < prior.sourceSizeBytes) {
|
|
1598
|
+
console.error(
|
|
1599
|
+
`Import: ${externalId} source shrank (${currentSize} < ${prior.sourceSizeBytes} bytes); re-import skipped (use --force to replace)`
|
|
1600
|
+
);
|
|
1601
|
+
counts.skippedDecreased++;
|
|
1602
|
+
return null;
|
|
1603
|
+
}
|
|
1604
|
+
return prior;
|
|
1514
1605
|
}
|
|
1515
1606
|
function encodeProjectDir(projectPath) {
|
|
1516
1607
|
return projectPath.replaceAll("/", "-");
|
|
1517
1608
|
}
|
|
1518
1609
|
async function loadExistingByExternalId(paths, sourceKind) {
|
|
1519
1610
|
const byExternalId = /* @__PURE__ */ new Map();
|
|
1520
|
-
const add = (externalId,
|
|
1611
|
+
const add = (externalId, prior) => {
|
|
1521
1612
|
const list = byExternalId.get(externalId);
|
|
1522
|
-
if (list === void 0) byExternalId.set(externalId, [
|
|
1523
|
-
else list.push(
|
|
1613
|
+
if (list === void 0) byExternalId.set(externalId, [prior]);
|
|
1614
|
+
else list.push(prior);
|
|
1524
1615
|
};
|
|
1525
1616
|
let sessionIds;
|
|
1526
1617
|
try {
|
|
@@ -1531,19 +1622,21 @@ async function loadExistingByExternalId(paths, sourceKind) {
|
|
|
1531
1622
|
for (const sessionId of sessionIds) {
|
|
1532
1623
|
let session;
|
|
1533
1624
|
try {
|
|
1534
|
-
session = await
|
|
1625
|
+
session = await readSessionYaml2(paths, sessionId);
|
|
1535
1626
|
} catch {
|
|
1536
1627
|
continue;
|
|
1537
1628
|
}
|
|
1538
1629
|
if (session.session.source.kind !== sourceKind) continue;
|
|
1630
|
+
const sourceSizeBytes = session.session.source.source_size_bytes;
|
|
1631
|
+
const prior = sourceSizeBytes !== void 0 ? { sessionId, sourceSizeBytes } : { sessionId };
|
|
1539
1632
|
const ext = session.session.source.external_id;
|
|
1540
1633
|
if (typeof ext === "string" && ext.length > 0) {
|
|
1541
|
-
add(ext,
|
|
1634
|
+
add(ext, prior);
|
|
1542
1635
|
continue;
|
|
1543
1636
|
}
|
|
1544
1637
|
const label = session.session.label;
|
|
1545
1638
|
const match = typeof label === "string" ? label.match(/^claude-code import (\S+)$/) : null;
|
|
1546
|
-
if (match?.[1] !== void 0) add(match[1],
|
|
1639
|
+
if (match?.[1] !== void 0) add(match[1], prior);
|
|
1547
1640
|
}
|
|
1548
1641
|
return byExternalId;
|
|
1549
1642
|
}
|
|
@@ -1589,6 +1682,14 @@ async function pathExists(file) {
|
|
|
1589
1682
|
throw error;
|
|
1590
1683
|
}
|
|
1591
1684
|
}
|
|
1685
|
+
async function statSize(file) {
|
|
1686
|
+
try {
|
|
1687
|
+
return (await stat(file)).size;
|
|
1688
|
+
} catch (error) {
|
|
1689
|
+
if (findErrorCode5(error, "ENOENT")) return void 0;
|
|
1690
|
+
throw error;
|
|
1691
|
+
}
|
|
1692
|
+
}
|
|
1592
1693
|
async function discoverCodexRollouts(sessionsRoot, projectPaths, options) {
|
|
1593
1694
|
const projectSet = new Set(projectPaths);
|
|
1594
1695
|
const files = await findRolloutFiles(sessionsRoot);
|
|
@@ -1667,9 +1768,9 @@ async function readFirstLine(file) {
|
|
|
1667
1768
|
}
|
|
1668
1769
|
}
|
|
1669
1770
|
async function readJsonlRecords(file) {
|
|
1670
|
-
let
|
|
1771
|
+
let buffer;
|
|
1671
1772
|
try {
|
|
1672
|
-
|
|
1773
|
+
buffer = await readFile(file);
|
|
1673
1774
|
} catch (error) {
|
|
1674
1775
|
if (findErrorCode5(error, "ENOENT")) {
|
|
1675
1776
|
throw new Error("Source log not found", { cause: error });
|
|
@@ -1680,7 +1781,7 @@ async function readJsonlRecords(file) {
|
|
|
1680
1781
|
throw new Error("Failed to read source log", { cause: error });
|
|
1681
1782
|
}
|
|
1682
1783
|
const records = [];
|
|
1683
|
-
for (const line of
|
|
1784
|
+
for (const line of buffer.toString("utf8").split("\n")) {
|
|
1684
1785
|
const trimmed = line.trim();
|
|
1685
1786
|
if (trimmed.length === 0) continue;
|
|
1686
1787
|
try {
|
|
@@ -1691,7 +1792,7 @@ async function readJsonlRecords(file) {
|
|
|
1691
1792
|
} catch {
|
|
1692
1793
|
}
|
|
1693
1794
|
}
|
|
1694
|
-
return records;
|
|
1795
|
+
return { records, sizeBytes: buffer.length };
|
|
1695
1796
|
}
|
|
1696
1797
|
function isObject(value) {
|
|
1697
1798
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
@@ -1699,7 +1800,15 @@ function isObject(value) {
|
|
|
1699
1800
|
function printImportResult(options, results, counts) {
|
|
1700
1801
|
const isDry = options.dryRun === true;
|
|
1701
1802
|
const eventTotal = results.reduce((sum, r) => sum + r.eventCount, 0);
|
|
1702
|
-
const {
|
|
1803
|
+
const {
|
|
1804
|
+
skippedNoAction,
|
|
1805
|
+
skippedExisting,
|
|
1806
|
+
replaced,
|
|
1807
|
+
reimported,
|
|
1808
|
+
skippedLegacy,
|
|
1809
|
+
skippedDecreased,
|
|
1810
|
+
skippedDuplicate
|
|
1811
|
+
} = counts;
|
|
1703
1812
|
if (options.json === true) {
|
|
1704
1813
|
console.log(
|
|
1705
1814
|
JSON.stringify({
|
|
@@ -1711,8 +1820,12 @@ function printImportResult(options, results, counts) {
|
|
|
1711
1820
|
})),
|
|
1712
1821
|
imported_count: results.length,
|
|
1713
1822
|
replaced_count: replaced,
|
|
1823
|
+
reimported_count: reimported,
|
|
1714
1824
|
skipped_no_action: skippedNoAction,
|
|
1715
1825
|
skipped_already_imported: skippedExisting,
|
|
1826
|
+
skipped_legacy_untracked: skippedLegacy,
|
|
1827
|
+
skipped_decreased: skippedDecreased,
|
|
1828
|
+
skipped_duplicate: skippedDuplicate,
|
|
1716
1829
|
event_total: eventTotal,
|
|
1717
1830
|
dry_run: isDry
|
|
1718
1831
|
})
|
|
@@ -1722,20 +1835,36 @@ function printImportResult(options, results, counts) {
|
|
|
1722
1835
|
const skipParts = [];
|
|
1723
1836
|
if (skippedNoAction > 0) skipParts.push(`${skippedNoAction} with no actions`);
|
|
1724
1837
|
if (skippedExisting > 0) skipParts.push(`${skippedExisting} already imported`);
|
|
1838
|
+
if (skippedLegacy > 0) skipParts.push(`${skippedLegacy} legacy (untracked size)`);
|
|
1839
|
+
if (skippedDecreased > 0) skipParts.push(`${skippedDecreased} shrank`);
|
|
1840
|
+
if (skippedDuplicate > 0) skipParts.push(`${skippedDuplicate} duplicated`);
|
|
1725
1841
|
const skipSuffix = skipParts.length > 0 ? `; skipped ${skipParts.join(", ")}` : "";
|
|
1726
1842
|
const eventsPart = replaced > 0 ? `${eventTotal} events, ${replaced} replaced` : `${eventTotal} events`;
|
|
1727
|
-
if (
|
|
1843
|
+
if (isDry) {
|
|
1844
|
+
const parts = [];
|
|
1845
|
+
if (results.length > 0) parts.push(`import ${results.length} session(s) (${eventsPart})`);
|
|
1846
|
+
if (reimported > 0) parts.push(`re-import ${reimported} changed session(s)`);
|
|
1847
|
+
const head = parts.length > 0 ? `Dry run: would ${parts.join(", ")}` : "Dry run: no changes";
|
|
1848
|
+
console.log(`${head}${skipSuffix}`);
|
|
1849
|
+
return;
|
|
1850
|
+
}
|
|
1851
|
+
if (results.length === 0 && reimported === 0) {
|
|
1728
1852
|
console.log(
|
|
1729
1853
|
skipParts.length > 0 ? `No new sessions imported (skipped ${skipParts.join(", ")})` : "No transcripts found to import"
|
|
1730
1854
|
);
|
|
1731
1855
|
return;
|
|
1732
1856
|
}
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
1857
|
+
const segments = [];
|
|
1858
|
+
if (results.length > 0) {
|
|
1859
|
+
const single = results.length === 1 && results[0] !== void 0 ? ` (${shortId2(results[0].sessionId)})` : "";
|
|
1860
|
+
segments.push(`Imported ${results.length} session(s)${single} (${eventsPart})`);
|
|
1736
1861
|
}
|
|
1737
|
-
|
|
1738
|
-
|
|
1862
|
+
if (reimported > 0) {
|
|
1863
|
+
segments.push(
|
|
1864
|
+
`${results.length > 0 ? "re-imported" : "Re-imported"} ${reimported} changed session(s)`
|
|
1865
|
+
);
|
|
1866
|
+
}
|
|
1867
|
+
console.log(`${segments.join(", ")}${skipSuffix}`);
|
|
1739
1868
|
}
|
|
1740
1869
|
function shortId2(id) {
|
|
1741
1870
|
if (id.startsWith(SES_PREFIX2)) {
|
|
@@ -1908,8 +2037,10 @@ async function runImport(adapter, fn) {
|
|
|
1908
2037
|
status: "ran",
|
|
1909
2038
|
importedCount: readCount(json.imported_count),
|
|
1910
2039
|
replacedCount: readCount(json.replaced_count),
|
|
2040
|
+
reimportedCount: readCount(json.reimported_count),
|
|
1911
2041
|
skippedNoAction: readCount(json.skipped_no_action),
|
|
1912
2042
|
skippedAlreadyImported: readCount(json.skipped_already_imported),
|
|
2043
|
+
skippedLegacyUntracked: readCount(json.skipped_legacy_untracked),
|
|
1913
2044
|
eventTotal: readCount(json.event_total),
|
|
1914
2045
|
dryRun: json.dry_run === true
|
|
1915
2046
|
};
|
|
@@ -2030,11 +2161,13 @@ function scansEqual(a, b) {
|
|
|
2030
2161
|
}
|
|
2031
2162
|
return true;
|
|
2032
2163
|
}
|
|
2033
|
-
function
|
|
2034
|
-
return outcome.status === "ran" ? outcome.importedCount : 0;
|
|
2164
|
+
function changedCount(outcome) {
|
|
2165
|
+
return outcome.status === "ran" ? outcome.importedCount + outcome.reimportedCount + outcome.replacedCount : 0;
|
|
2035
2166
|
}
|
|
2036
2167
|
function describeOutcome(outcome) {
|
|
2037
|
-
|
|
2168
|
+
if (outcome.status !== "ran") return `${outcome.adapter} skipped`;
|
|
2169
|
+
const reimported = outcome.reimportedCount > 0 ? ` ~${outcome.reimportedCount}` : "";
|
|
2170
|
+
return `${outcome.adapter} +${outcome.importedCount}${reimported}`;
|
|
2038
2171
|
}
|
|
2039
2172
|
function hms(date) {
|
|
2040
2173
|
return date.toISOString().slice(11, 19);
|
|
@@ -2042,7 +2175,7 @@ function hms(date) {
|
|
|
2042
2175
|
async function runImports(deps) {
|
|
2043
2176
|
const claude = await importClaudeCode(deps.importOptions, deps.ctx);
|
|
2044
2177
|
const codex = await importCodex(deps.importOptions, deps.ctx);
|
|
2045
|
-
return { claude, codex,
|
|
2178
|
+
return { claude, codex, changed: changedCount(claude) + changedCount(codex) };
|
|
2046
2179
|
}
|
|
2047
2180
|
async function regenerate(deps) {
|
|
2048
2181
|
const nowIso = deps.now().toISOString();
|
|
@@ -2074,8 +2207,8 @@ async function runRefreshWatch(deps) {
|
|
|
2074
2207
|
try {
|
|
2075
2208
|
const current = await scanSourceLogs(roots);
|
|
2076
2209
|
if (scansEqual(current, lastScan) && !scansEqual(current, importedScan)) {
|
|
2077
|
-
const { claude, codex,
|
|
2078
|
-
if (
|
|
2210
|
+
const { claude, codex, changed } = await runImports(deps);
|
|
2211
|
+
if (changed > 0) pendingRegen = true;
|
|
2079
2212
|
if (pendingRegen) {
|
|
2080
2213
|
const sessions = await regenerate(deps);
|
|
2081
2214
|
pendingRegen = false;
|
|
@@ -2214,9 +2347,11 @@ function describeImport(outcome) {
|
|
|
2214
2347
|
}
|
|
2215
2348
|
const verb = outcome.dryRun ? "would import" : "imported";
|
|
2216
2349
|
const parts = [`${outcome.importedCount} session(s)`, `${outcome.eventTotal} events`];
|
|
2350
|
+
if (outcome.reimportedCount > 0) parts.push(`${outcome.reimportedCount} re-imported`);
|
|
2217
2351
|
if (outcome.replacedCount > 0) parts.push(`${outcome.replacedCount} replaced`);
|
|
2218
2352
|
if (outcome.skippedAlreadyImported > 0)
|
|
2219
2353
|
parts.push(`${outcome.skippedAlreadyImported} already imported`);
|
|
2354
|
+
if (outcome.skippedLegacyUntracked > 0) parts.push(`${outcome.skippedLegacyUntracked} legacy`);
|
|
2220
2355
|
return `${outcome.adapter}: ${verb} ${parts.join(", ")}`;
|
|
2221
2356
|
}
|
|
2222
2357
|
function printRefreshSummary(result) {
|
|
@@ -2660,12 +2795,14 @@ import {
|
|
|
2660
2795
|
appendEventToExistingSession as appendEventToExistingSession2,
|
|
2661
2796
|
assertBasouRootSafe as assertBasouRootSafe9,
|
|
2662
2797
|
basouPaths as basouPaths9,
|
|
2798
|
+
enumerateSessionDirs as enumerateSessionDirs2,
|
|
2663
2799
|
findErrorCode as findErrorCode8,
|
|
2664
2800
|
importSessionFromJson as importSessionFromJson2,
|
|
2665
2801
|
loadSessionEntries,
|
|
2666
2802
|
readAllEvents,
|
|
2667
2803
|
readManifest as readManifest5,
|
|
2668
2804
|
readYamlFile as readYamlFile4,
|
|
2805
|
+
rechainSessionInPlace,
|
|
2669
2806
|
resolveRepositoryRoot as resolveRepositoryRoot10,
|
|
2670
2807
|
resolveSessionId as resolveSessionId2,
|
|
2671
2808
|
resolveTaskId,
|
|
@@ -2714,6 +2851,11 @@ function registerSessionCommand(program2) {
|
|
|
2714
2851
|
session.command("note <session_id>").description("Append a note_added event to an existing session").option("--body <text>", "Note body (inline)", parseNoteBodyOption).option("--from-file <path>", "Read note body from a file").option("--json", "Output the result as JSON").option("-v, --verbose", "Show error causes").action(async (sessionIdInput, options) => {
|
|
2715
2852
|
await runSessionNote(sessionIdInput, options);
|
|
2716
2853
|
});
|
|
2854
|
+
session.command("rechain").description(
|
|
2855
|
+
"Add the tamper-evidence hash chain, in place, to imported sessions created before chaining existed"
|
|
2856
|
+
).option("--session <id>", "Rechain a single session (unique id prefix accepted)").option("--all", "Rechain every session in the workspace").option("--dry-run", "Compute the outcomes only; do not write").option("--json", "Output the outcomes as JSON").option("-v, --verbose", "Show error causes").action(async (options) => {
|
|
2857
|
+
await runSessionRechain(options);
|
|
2858
|
+
});
|
|
2717
2859
|
}
|
|
2718
2860
|
async function runSessionList(options, ctx = {}) {
|
|
2719
2861
|
try {
|
|
@@ -3255,6 +3397,74 @@ function printSessionNoteResult(options, sessionId, eventId, sessionStatus, body
|
|
|
3255
3397
|
const preview = body.length > NOTE_BODY_PREVIEW_LIMIT ? `${body.slice(0, NOTE_BODY_PREVIEW_HEAD)}...` : body;
|
|
3256
3398
|
console.log(`Added note to session ${sid} (${sessionStatus}): ${preview}`);
|
|
3257
3399
|
}
|
|
3400
|
+
async function runSessionRechain(options, ctx = {}) {
|
|
3401
|
+
try {
|
|
3402
|
+
await doRunSessionRechain(options, ctx);
|
|
3403
|
+
} catch (error) {
|
|
3404
|
+
renderCliError(error, { verbose: isVerbose(options) });
|
|
3405
|
+
process.exitCode = 1;
|
|
3406
|
+
}
|
|
3407
|
+
}
|
|
3408
|
+
async function doRunSessionRechain(options, ctx) {
|
|
3409
|
+
if (options.session !== void 0 && options.all === true) {
|
|
3410
|
+
throw new Error("Specify either --session <id> or --all, not both");
|
|
3411
|
+
}
|
|
3412
|
+
if (options.session === void 0 && options.all !== true) {
|
|
3413
|
+
throw new Error("Specify --session <id> or --all");
|
|
3414
|
+
}
|
|
3415
|
+
const cwd = ctx.cwd ?? process.cwd();
|
|
3416
|
+
const repositoryRoot = await resolveRepositoryRootForSession(cwd, "rechain");
|
|
3417
|
+
const paths = basouPaths9(repositoryRoot);
|
|
3418
|
+
await assertWorkspaceInitialized7(paths.root);
|
|
3419
|
+
const sessionIds = options.session !== void 0 ? [await resolveSessionId2(paths, options.session)] : await enumerateSessionDirs2(paths);
|
|
3420
|
+
const dryRun = options.dryRun === true;
|
|
3421
|
+
const rows = [];
|
|
3422
|
+
for (const sessionId of sessionIds) {
|
|
3423
|
+
let outcome;
|
|
3424
|
+
try {
|
|
3425
|
+
outcome = await rechainSessionInPlace(paths, sessionId, { dryRun });
|
|
3426
|
+
} catch (error) {
|
|
3427
|
+
rows.push({
|
|
3428
|
+
session_id: sessionId,
|
|
3429
|
+
status: "error",
|
|
3430
|
+
message: error instanceof Error ? error.message : "Unknown error"
|
|
3431
|
+
});
|
|
3432
|
+
continue;
|
|
3433
|
+
}
|
|
3434
|
+
if (outcome.status === "rechained") {
|
|
3435
|
+
rows.push({ session_id: sessionId, status: "rechained", event_count: outcome.eventCount });
|
|
3436
|
+
} else {
|
|
3437
|
+
rows.push({ session_id: sessionId, status: "skipped", reason: outcome.reason });
|
|
3438
|
+
}
|
|
3439
|
+
}
|
|
3440
|
+
const tamperedCount = rows.filter((r) => r.reason === "tampered").length;
|
|
3441
|
+
const errorCount = rows.filter((r) => r.status === "error").length;
|
|
3442
|
+
if (options.json === true) {
|
|
3443
|
+
console.log(JSON.stringify(rows, null, 2));
|
|
3444
|
+
} else {
|
|
3445
|
+
for (const row of rows) {
|
|
3446
|
+
console.log(`${row.session_id} ${renderRechainRow(row, dryRun)}`);
|
|
3447
|
+
}
|
|
3448
|
+
const rechained = rows.filter((r) => r.status === "rechained").length;
|
|
3449
|
+
const skipped = rows.filter((r) => r.status === "skipped").length;
|
|
3450
|
+
console.log(
|
|
3451
|
+
`Sessions: ${rows.length} total \u2014 ${rechained} ${dryRun ? "would be rechained" : "rechained"}, ${skipped} skipped, ${errorCount} errors`
|
|
3452
|
+
);
|
|
3453
|
+
}
|
|
3454
|
+
if (tamperedCount > 0 || errorCount > 0) {
|
|
3455
|
+
process.exitCode = 1;
|
|
3456
|
+
}
|
|
3457
|
+
}
|
|
3458
|
+
function renderRechainRow(row, dryRun) {
|
|
3459
|
+
switch (row.status) {
|
|
3460
|
+
case "rechained":
|
|
3461
|
+
return `${dryRun ? "would rechain" : "rechained"} (${row.event_count} events)`;
|
|
3462
|
+
case "skipped":
|
|
3463
|
+
return row.reason === "tampered" ? "skipped (TAMPERED \u2014 inspect with 'basou verify')" : `skipped (${row.reason})`;
|
|
3464
|
+
case "error":
|
|
3465
|
+
return `error (${row.message})`;
|
|
3466
|
+
}
|
|
3467
|
+
}
|
|
3258
3468
|
|
|
3259
3469
|
// src/commands/stats.ts
|
|
3260
3470
|
import {
|
|
@@ -4591,9 +4801,107 @@ function maxLen3(values, floor) {
|
|
|
4591
4801
|
return max;
|
|
4592
4802
|
}
|
|
4593
4803
|
|
|
4804
|
+
// src/commands/verify.ts
|
|
4805
|
+
import {
|
|
4806
|
+
assertBasouRootSafe as assertBasouRootSafe13,
|
|
4807
|
+
basouPaths as basouPaths13,
|
|
4808
|
+
enumerateSessionDirs as enumerateSessionDirs3,
|
|
4809
|
+
findErrorCode as findErrorCode12,
|
|
4810
|
+
resolveRepositoryRoot as resolveRepositoryRoot14,
|
|
4811
|
+
resolveSessionId as resolveSessionId4,
|
|
4812
|
+
verifyEventsChain
|
|
4813
|
+
} from "@basou/core";
|
|
4814
|
+
function registerVerifyCommand(program2) {
|
|
4815
|
+
program2.command("verify").description(
|
|
4816
|
+
"Verify the tamper-evidence hash chain of imported sessions' event logs (read-only)"
|
|
4817
|
+
).option("--session <id>", "Verify a single session (unique id prefix accepted)").option("--all", "Verify every session (the default when --session is omitted)").option("--json", "Output the verdicts as JSON").option("-v, --verbose", "Show error causes").action(async (opts) => {
|
|
4818
|
+
await runVerify(opts);
|
|
4819
|
+
});
|
|
4820
|
+
}
|
|
4821
|
+
async function runVerify(options, ctx = {}) {
|
|
4822
|
+
try {
|
|
4823
|
+
await doRunVerify(options, ctx);
|
|
4824
|
+
} catch (error) {
|
|
4825
|
+
renderCliError(error, { verbose: isVerbose(options) });
|
|
4826
|
+
process.exitCode = 1;
|
|
4827
|
+
}
|
|
4828
|
+
}
|
|
4829
|
+
async function doRunVerify(options, ctx) {
|
|
4830
|
+
if (options.session !== void 0 && options.all === true) {
|
|
4831
|
+
throw new Error("Specify either --session <id> or --all, not both");
|
|
4832
|
+
}
|
|
4833
|
+
const cwd = ctx.cwd ?? process.cwd();
|
|
4834
|
+
const repositoryRoot = await resolveRepositoryRootForVerify(cwd);
|
|
4835
|
+
const paths = basouPaths13(repositoryRoot);
|
|
4836
|
+
await assertWorkspaceInitialized10(paths.root);
|
|
4837
|
+
const sessionIds = options.session !== void 0 ? [await resolveSessionId4(paths, options.session)] : await enumerateSessionDirs3(paths);
|
|
4838
|
+
const rows = [];
|
|
4839
|
+
for (const sessionId of sessionIds) {
|
|
4840
|
+
const verdict = await verifyEventsChain(paths, sessionId);
|
|
4841
|
+
rows.push({
|
|
4842
|
+
session_id: sessionId,
|
|
4843
|
+
status: verdict.status,
|
|
4844
|
+
event_count: verdict.eventCount,
|
|
4845
|
+
...verdict.reason !== void 0 ? { reason: verdict.reason } : {},
|
|
4846
|
+
...verdict.line !== void 0 ? { line: verdict.line } : {}
|
|
4847
|
+
});
|
|
4848
|
+
}
|
|
4849
|
+
const tamperedCount = rows.filter((r) => r.status === "tampered").length;
|
|
4850
|
+
if (options.json === true) {
|
|
4851
|
+
console.log(JSON.stringify(rows, null, 2));
|
|
4852
|
+
} else {
|
|
4853
|
+
for (const row of rows) {
|
|
4854
|
+
console.log(`${row.session_id} ${renderVerdict(row)}`);
|
|
4855
|
+
}
|
|
4856
|
+
const tally = (status) => rows.filter((r) => r.status === status).length;
|
|
4857
|
+
console.log(
|
|
4858
|
+
`Sessions: ${rows.length} total \u2014 ${tally("verified")} verified, ${tally("unchained")} unchained, ${tally("empty")} empty, ${tally("incomplete")} incomplete, ${tamperedCount} tampered`
|
|
4859
|
+
);
|
|
4860
|
+
}
|
|
4861
|
+
if (tamperedCount > 0) {
|
|
4862
|
+
process.exitCode = 1;
|
|
4863
|
+
}
|
|
4864
|
+
}
|
|
4865
|
+
function renderVerdict(row) {
|
|
4866
|
+
switch (row.status) {
|
|
4867
|
+
case "verified":
|
|
4868
|
+
return `verified (${row.event_count} events)`;
|
|
4869
|
+
case "tampered":
|
|
4870
|
+
return row.line !== void 0 ? `TAMPERED (${row.reason} at line ${row.line})` : `TAMPERED (${row.reason})`;
|
|
4871
|
+
case "incomplete":
|
|
4872
|
+
return "incomplete (session.yaml missing; re-import to repair)";
|
|
4873
|
+
case "unchained":
|
|
4874
|
+
return "unchained (not an imported session, or imported before chaining)";
|
|
4875
|
+
case "empty":
|
|
4876
|
+
return "empty";
|
|
4877
|
+
}
|
|
4878
|
+
}
|
|
4879
|
+
async function resolveRepositoryRootForVerify(cwd) {
|
|
4880
|
+
try {
|
|
4881
|
+
return await resolveRepositoryRoot14(cwd);
|
|
4882
|
+
} catch (error) {
|
|
4883
|
+
if (error instanceof Error && error.message === "Not a git repository") {
|
|
4884
|
+
throw new Error("Not a git repository. Run 'git init' first, then re-run 'basou verify'.", {
|
|
4885
|
+
cause: error
|
|
4886
|
+
});
|
|
4887
|
+
}
|
|
4888
|
+
throw error;
|
|
4889
|
+
}
|
|
4890
|
+
}
|
|
4891
|
+
async function assertWorkspaceInitialized10(basouRoot) {
|
|
4892
|
+
try {
|
|
4893
|
+
await assertBasouRootSafe13(basouRoot);
|
|
4894
|
+
} catch (error) {
|
|
4895
|
+
if (findErrorCode12(error, "ENOENT")) {
|
|
4896
|
+
throw new Error("Workspace not initialized. Run 'basou init' first.");
|
|
4897
|
+
}
|
|
4898
|
+
throw error;
|
|
4899
|
+
}
|
|
4900
|
+
}
|
|
4901
|
+
|
|
4594
4902
|
// src/commands/view.ts
|
|
4595
4903
|
import { spawn } from "child_process";
|
|
4596
|
-
import { assertBasouRootSafe as
|
|
4904
|
+
import { assertBasouRootSafe as assertBasouRootSafe14, basouPaths as basouPaths14, findErrorCode as findErrorCode14, resolveRepositoryRoot as resolveRepositoryRoot15 } from "@basou/core";
|
|
4597
4905
|
import { InvalidArgumentError as InvalidArgumentError5 } from "commander";
|
|
4598
4906
|
|
|
4599
4907
|
// src/lib/view-server.ts
|
|
@@ -4602,7 +4910,7 @@ import { join as join8 } from "path";
|
|
|
4602
4910
|
import {
|
|
4603
4911
|
computeWorkStats as computeWorkStats2,
|
|
4604
4912
|
enumerateApprovals as enumerateApprovals2,
|
|
4605
|
-
findErrorCode as
|
|
4913
|
+
findErrorCode as findErrorCode13,
|
|
4606
4914
|
isLazyExpired as isLazyExpired2,
|
|
4607
4915
|
loadApproval as loadApproval2,
|
|
4608
4916
|
loadSessionEntries as loadSessionEntries3,
|
|
@@ -4610,7 +4918,7 @@ import {
|
|
|
4610
4918
|
readAllEvents as readAllEvents2,
|
|
4611
4919
|
readManifest as readManifest8,
|
|
4612
4920
|
readMarkdownFile as readMarkdownFile4,
|
|
4613
|
-
readSessionYaml as
|
|
4921
|
+
readSessionYaml as readSessionYaml3,
|
|
4614
4922
|
readTaskFile as readTaskFile2,
|
|
4615
4923
|
renderDecisions as renderDecisions3,
|
|
4616
4924
|
renderHandoff as renderHandoff3
|
|
@@ -5204,7 +5512,7 @@ async function overview(deps) {
|
|
|
5204
5512
|
try {
|
|
5205
5513
|
manifest = await readManifest8(deps.paths);
|
|
5206
5514
|
} catch (error) {
|
|
5207
|
-
if (
|
|
5515
|
+
if (findErrorCode13(error, "ENOENT")) {
|
|
5208
5516
|
return { initialized: false, repoRoot: deps.repoRoot };
|
|
5209
5517
|
}
|
|
5210
5518
|
throw error;
|
|
@@ -5251,7 +5559,7 @@ async function sessionsList(deps) {
|
|
|
5251
5559
|
async function sessionDetail(deps, sessionId) {
|
|
5252
5560
|
let session;
|
|
5253
5561
|
try {
|
|
5254
|
-
session = await
|
|
5562
|
+
session = await readSessionYaml3(deps.paths, sessionId);
|
|
5255
5563
|
} catch (error) {
|
|
5256
5564
|
if (error instanceof Error && error.message === "YAML file not found") {
|
|
5257
5565
|
throw new HttpError(404, "Session not found");
|
|
@@ -5419,8 +5727,8 @@ async function runView(options, ctx = {}) {
|
|
|
5419
5727
|
async function doRunView(options, ctx) {
|
|
5420
5728
|
const cwd = ctx.cwd ?? process.cwd();
|
|
5421
5729
|
const repositoryRoot = await resolveRepositoryRootForView(cwd);
|
|
5422
|
-
const paths =
|
|
5423
|
-
await
|
|
5730
|
+
const paths = basouPaths14(repositoryRoot);
|
|
5731
|
+
await assertWorkspaceInitialized11(paths.root);
|
|
5424
5732
|
const deps = {
|
|
5425
5733
|
paths,
|
|
5426
5734
|
repoRoot: repositoryRoot,
|
|
@@ -5451,7 +5759,7 @@ async function startListening(port, deps) {
|
|
|
5451
5759
|
try {
|
|
5452
5760
|
return await startViewServer({ port, deps });
|
|
5453
5761
|
} catch (error) {
|
|
5454
|
-
if (
|
|
5762
|
+
if (findErrorCode14(error, "EADDRINUSE")) {
|
|
5455
5763
|
throw new Error(`Port ${port} is already in use. Pass --port <n> to choose another.`, {
|
|
5456
5764
|
cause: error
|
|
5457
5765
|
});
|
|
@@ -5502,7 +5810,7 @@ function waitForShutdown(signal) {
|
|
|
5502
5810
|
}
|
|
5503
5811
|
async function resolveRepositoryRootForView(cwd) {
|
|
5504
5812
|
try {
|
|
5505
|
-
return await
|
|
5813
|
+
return await resolveRepositoryRoot15(cwd);
|
|
5506
5814
|
} catch (error) {
|
|
5507
5815
|
if (error instanceof Error && error.message === "Not a git repository") {
|
|
5508
5816
|
throw new Error("Not a git repository. Run 'git init' first, then re-run 'basou view'.", {
|
|
@@ -5512,11 +5820,11 @@ async function resolveRepositoryRootForView(cwd) {
|
|
|
5512
5820
|
throw error;
|
|
5513
5821
|
}
|
|
5514
5822
|
}
|
|
5515
|
-
async function
|
|
5823
|
+
async function assertWorkspaceInitialized11(basouRoot) {
|
|
5516
5824
|
try {
|
|
5517
|
-
await
|
|
5825
|
+
await assertBasouRootSafe14(basouRoot);
|
|
5518
5826
|
} catch (error) {
|
|
5519
|
-
if (
|
|
5827
|
+
if (findErrorCode14(error, "ENOENT")) {
|
|
5520
5828
|
throw new Error("Workspace not initialized. Run 'basou init' first.");
|
|
5521
5829
|
}
|
|
5522
5830
|
throw error;
|
|
@@ -5538,6 +5846,7 @@ function buildProgram() {
|
|
|
5538
5846
|
registerSessionCommand(program2);
|
|
5539
5847
|
registerImportCommand(program2);
|
|
5540
5848
|
registerRefreshCommand(program2);
|
|
5849
|
+
registerVerifyCommand(program2);
|
|
5541
5850
|
registerViewCommand(program2);
|
|
5542
5851
|
registerApprovalCommand(program2);
|
|
5543
5852
|
registerDecisionCommand(program2);
|