@basou/cli 0.71.1 → 0.71.2
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 +247 -42
- package/dist/index.js.map +1 -1
- package/dist/program.js +247 -42
- package/dist/program.js.map +1 -1
- package/package.json +2 -2
package/dist/program.js
CHANGED
|
@@ -1040,6 +1040,8 @@ import {
|
|
|
1040
1040
|
CLAUDE_IMPORT_SOURCE,
|
|
1041
1041
|
CODEX_IMPORT_SOURCE,
|
|
1042
1042
|
classifyFilesBySourceRoot,
|
|
1043
|
+
claudeTranscriptOwnRecords,
|
|
1044
|
+
claudeTranscriptRecordUuid,
|
|
1043
1045
|
claudeTranscriptToImportPayload,
|
|
1044
1046
|
codexRolloutToImportPayload,
|
|
1045
1047
|
displayPath as displayPath2,
|
|
@@ -1140,24 +1142,43 @@ async function doRunImportClaudeCode(options, ctx) {
|
|
|
1140
1142
|
});
|
|
1141
1143
|
const projectsRoot = ctx.claudeProjectsDir ?? join2(homedir(), ".claude", "projects");
|
|
1142
1144
|
const files = await selectTranscriptFiles(projectsRoot, projectPaths, options);
|
|
1145
|
+
const store = await loadImportStore(paths, options, CLAUDE_IMPORT_SOURCE);
|
|
1146
|
+
const conversations = await indexConversations(
|
|
1147
|
+
await listTranscriptFiles(projectsRoot, projectPaths),
|
|
1148
|
+
ctx.transcriptCreatedAtMs ?? birthTimeMs,
|
|
1149
|
+
(externalId) => earliestStart(store.byExternalId.get(externalId) ?? [])
|
|
1150
|
+
);
|
|
1143
1151
|
const projectSet = new Set(projectPaths);
|
|
1144
1152
|
const candidates = files.map((file) => {
|
|
1145
1153
|
const externalId = basename3(file, ".jsonl");
|
|
1154
|
+
const { root, older } = conversations.place(file);
|
|
1146
1155
|
return {
|
|
1147
1156
|
externalId,
|
|
1148
1157
|
sourcePath: file,
|
|
1149
|
-
|
|
1158
|
+
// An earlier import that took the copies too began the session at the
|
|
1159
|
+
// conversation's first record or before it; one without them begins after
|
|
1160
|
+
// the last copy.
|
|
1161
|
+
holdsCopies: (prior) => older.length > 0 && root?.timestamp !== void 0 && Date.parse(prior.startedAt) <= Date.parse(root.timestamp),
|
|
1162
|
+
toPayload: async (notBefore) => {
|
|
1150
1163
|
const { records, sizeBytes } = await readJsonlRecords(file);
|
|
1151
1164
|
const cwd = firstTranscriptCwd(records);
|
|
1152
|
-
if (cwd === void 0 || !projectSet.has(cwd))
|
|
1165
|
+
if (cwd === void 0 || !projectSet.has(cwd))
|
|
1166
|
+
return { payload: null, leftOutCopies: false };
|
|
1153
1167
|
const observation2 = await readSessionObservation(paths.observations, externalId);
|
|
1154
1168
|
const observedFiles = observation2 === null ? [] : observedFilesOf(observation2);
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
sourceSizeBytes: sizeBytes,
|
|
1159
|
-
...observedFiles.length > 0 ? { observedFiles } : {}
|
|
1169
|
+
const own = claudeTranscriptOwnRecords(records, {
|
|
1170
|
+
copiedUuids: await conversations.uuidsOf(older),
|
|
1171
|
+
...notBefore !== void 0 ? { notBefore } : {}
|
|
1160
1172
|
});
|
|
1173
|
+
return {
|
|
1174
|
+
payload: claudeTranscriptToImportPayload(own, {
|
|
1175
|
+
workspaceId: manifest.workspace.id,
|
|
1176
|
+
externalId,
|
|
1177
|
+
sourceSizeBytes: sizeBytes,
|
|
1178
|
+
...observedFiles.length > 0 ? { observedFiles } : {}
|
|
1179
|
+
}),
|
|
1180
|
+
leftOutCopies: own.length < records.length
|
|
1181
|
+
};
|
|
1161
1182
|
}
|
|
1162
1183
|
};
|
|
1163
1184
|
});
|
|
@@ -1165,7 +1186,7 @@ async function doRunImportClaudeCode(options, ctx) {
|
|
|
1165
1186
|
paths,
|
|
1166
1187
|
manifest,
|
|
1167
1188
|
options,
|
|
1168
|
-
|
|
1189
|
+
store,
|
|
1169
1190
|
candidates,
|
|
1170
1191
|
projectPaths,
|
|
1171
1192
|
hasDeclaredBoundary(options, manifest)
|
|
@@ -1187,18 +1208,21 @@ async function doRunImportCodex(options, ctx) {
|
|
|
1187
1208
|
sourcePath: file,
|
|
1188
1209
|
toPayload: async () => {
|
|
1189
1210
|
const { records, sizeBytes } = await readJsonlRecords(file);
|
|
1190
|
-
return
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1211
|
+
return {
|
|
1212
|
+
payload: codexRolloutToImportPayload(records, {
|
|
1213
|
+
workspaceId: manifest.workspace.id,
|
|
1214
|
+
externalId,
|
|
1215
|
+
sourceSizeBytes: sizeBytes
|
|
1216
|
+
}),
|
|
1217
|
+
leftOutCopies: false
|
|
1218
|
+
};
|
|
1195
1219
|
}
|
|
1196
1220
|
}));
|
|
1197
1221
|
await importDerivedSessions(
|
|
1198
1222
|
paths,
|
|
1199
1223
|
manifest,
|
|
1200
1224
|
options,
|
|
1201
|
-
CODEX_IMPORT_SOURCE,
|
|
1225
|
+
await loadImportStore(paths, options, CODEX_IMPORT_SOURCE),
|
|
1202
1226
|
candidates,
|
|
1203
1227
|
projectPaths,
|
|
1204
1228
|
hasDeclaredBoundary(options, manifest)
|
|
@@ -1223,14 +1247,8 @@ async function resolveImportTarget(ctx) {
|
|
|
1223
1247
|
const manifest = await readManifest2(paths);
|
|
1224
1248
|
return { repositoryRoot, paths, manifest };
|
|
1225
1249
|
}
|
|
1226
|
-
async function importDerivedSessions(paths, manifest, options,
|
|
1227
|
-
|
|
1228
|
-
if (options.dryRun !== true) await assertLockStoreSafe2(paths);
|
|
1229
|
-
const {
|
|
1230
|
-
byExternalId: existingByExternalId,
|
|
1231
|
-
notDirectories,
|
|
1232
|
-
yamlNotFiles
|
|
1233
|
-
} = await loadExistingByExternalId(paths, sourceKind);
|
|
1250
|
+
async function importDerivedSessions(paths, manifest, options, store, candidates, projectPaths, boundaryDeclared) {
|
|
1251
|
+
const { byExternalId: existingByExternalId, notDirectories, yamlNotFiles } = store;
|
|
1234
1252
|
let notDirectoriesWarned = false;
|
|
1235
1253
|
const seenThisRun = /* @__PURE__ */ new Set();
|
|
1236
1254
|
const crossProjectCheck = boundaryDeclared;
|
|
@@ -1258,7 +1276,8 @@ async function importDerivedSessions(paths, manifest, options, sourceKind, candi
|
|
|
1258
1276
|
skippedLegacy: 0,
|
|
1259
1277
|
skippedDecreased: 0,
|
|
1260
1278
|
skippedDuplicate: 0,
|
|
1261
|
-
skippedUnverifiable: 0
|
|
1279
|
+
skippedUnverifiable: 0,
|
|
1280
|
+
droppedCopies: 0
|
|
1262
1281
|
};
|
|
1263
1282
|
let sanitizedPaths = 0;
|
|
1264
1283
|
const validate = (payload) => {
|
|
@@ -1274,30 +1293,44 @@ async function importDerivedSessions(paths, manifest, options, sourceKind, candi
|
|
|
1274
1293
|
}
|
|
1275
1294
|
return parsed.data;
|
|
1276
1295
|
};
|
|
1277
|
-
for (const { externalId, sourcePath, toPayload } of candidates) {
|
|
1296
|
+
for (const { externalId, sourcePath, toPayload, holdsCopies } of candidates) {
|
|
1278
1297
|
if (seenThisRun.has(externalId)) {
|
|
1279
1298
|
counts.skippedExisting++;
|
|
1280
1299
|
continue;
|
|
1281
1300
|
}
|
|
1282
1301
|
const priors = existingByExternalId.get(externalId) ?? [];
|
|
1283
1302
|
if (priors.length > 0 && options.force !== true) {
|
|
1284
|
-
const
|
|
1285
|
-
if (
|
|
1286
|
-
const
|
|
1303
|
+
const decision = await classifyReimport(priors, sourcePath, externalId, counts, holdsCopies);
|
|
1304
|
+
if (decision === null) continue;
|
|
1305
|
+
const { prior, reason } = decision;
|
|
1306
|
+
const derived = await toPayload(prior.startedAt);
|
|
1307
|
+
const payload2 = validate(derived.payload);
|
|
1287
1308
|
if (payload2 === null) {
|
|
1309
|
+
if (reason === "holds_copies") {
|
|
1310
|
+
console.error(
|
|
1311
|
+
`Import: ${externalId} added no action of its own after the records it copied from an older transcript; its session, imported with those records, is left as it is`
|
|
1312
|
+
);
|
|
1313
|
+
}
|
|
1288
1314
|
counts.skippedNoAction++;
|
|
1289
1315
|
continue;
|
|
1290
1316
|
}
|
|
1317
|
+
if (reason === "holds_copies" && !derived.leftOutCopies) {
|
|
1318
|
+
counts.skippedExisting++;
|
|
1319
|
+
continue;
|
|
1320
|
+
}
|
|
1291
1321
|
const readSize = payload2.session.source.source_size_bytes;
|
|
1292
|
-
if (prior.sourceSizeBytes !== void 0 && readSize !== void 0 && readSize <= prior.sourceSizeBytes) {
|
|
1322
|
+
if (prior.sourceSizeBytes !== void 0 && readSize !== void 0 && (reason === "grew" ? readSize <= prior.sourceSizeBytes : readSize < prior.sourceSizeBytes)) {
|
|
1293
1323
|
console.error(
|
|
1294
|
-
`Import: ${externalId} source changed during read (now ${readSize}
|
|
1324
|
+
`Import: ${externalId} source changed during read (now ${readSize} bytes, was ${prior.sourceSizeBytes}); re-import skipped`
|
|
1295
1325
|
);
|
|
1296
1326
|
counts.skippedDecreased++;
|
|
1297
1327
|
continue;
|
|
1298
1328
|
}
|
|
1299
1329
|
const outcome = await reimportPreservingId(paths, manifest, prior.sessionId, payload2, {
|
|
1300
|
-
dryRun: options.dryRun === true
|
|
1330
|
+
dryRun: options.dryRun === true,
|
|
1331
|
+
// What an earlier import derived from the copies predates the session's
|
|
1332
|
+
// fresh start; anything later that is not reproduced still refuses.
|
|
1333
|
+
...derived.leftOutCopies ? { dropDerivedBefore: payload2.session.started_at } : {}
|
|
1301
1334
|
});
|
|
1302
1335
|
if (outcome.status === "skipped") {
|
|
1303
1336
|
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')" : outcome.reason === "prior_yaml_invalid" ? "prior session.yaml does not load as a whole document" : "source changed in a non-append way (derived events would be dropped)";
|
|
@@ -1306,11 +1339,12 @@ async function importDerivedSessions(paths, manifest, options, sourceKind, candi
|
|
|
1306
1339
|
continue;
|
|
1307
1340
|
}
|
|
1308
1341
|
counts.reimported++;
|
|
1342
|
+
if (holdsCopies?.(prior) === true) counts.droppedCopies++;
|
|
1309
1343
|
seenThisRun.add(externalId);
|
|
1310
1344
|
await noteCrossProject(externalId, payload2);
|
|
1311
1345
|
continue;
|
|
1312
1346
|
}
|
|
1313
|
-
const payload = validate(await toPayload());
|
|
1347
|
+
const payload = validate((await toPayload(earliestStart(priors))).payload);
|
|
1314
1348
|
if (payload === null) {
|
|
1315
1349
|
counts.skippedNoAction++;
|
|
1316
1350
|
continue;
|
|
@@ -1361,7 +1395,7 @@ async function importDerivedSessions(paths, manifest, options, sourceKind, candi
|
|
|
1361
1395
|
}
|
|
1362
1396
|
printImportResult(options, results, counts);
|
|
1363
1397
|
}
|
|
1364
|
-
async function classifyReimport(priors, sourcePath, externalId, counts) {
|
|
1398
|
+
async function classifyReimport(priors, sourcePath, externalId, counts, holdsCopies) {
|
|
1365
1399
|
if (priors.length > 1) {
|
|
1366
1400
|
console.error(
|
|
1367
1401
|
`Import: ${externalId} has ${priors.length} prior sessions; re-import skipped (use --force)`
|
|
@@ -1384,6 +1418,7 @@ async function classifyReimport(priors, sourcePath, externalId, counts) {
|
|
|
1384
1418
|
return null;
|
|
1385
1419
|
}
|
|
1386
1420
|
if (currentSize === prior.sourceSizeBytes) {
|
|
1421
|
+
if (holdsCopies?.(prior) === true) return { prior, reason: "holds_copies" };
|
|
1387
1422
|
counts.skippedExisting++;
|
|
1388
1423
|
return null;
|
|
1389
1424
|
}
|
|
@@ -1394,7 +1429,15 @@ async function classifyReimport(priors, sourcePath, externalId, counts) {
|
|
|
1394
1429
|
counts.skippedDecreased++;
|
|
1395
1430
|
return null;
|
|
1396
1431
|
}
|
|
1397
|
-
return prior;
|
|
1432
|
+
return { prior, reason: "grew" };
|
|
1433
|
+
}
|
|
1434
|
+
function earliestStart(priors) {
|
|
1435
|
+
let earliest;
|
|
1436
|
+
for (const { startedAt } of priors) {
|
|
1437
|
+
if (earliest === void 0 || Date.parse(startedAt) < Date.parse(earliest))
|
|
1438
|
+
earliest = startedAt;
|
|
1439
|
+
}
|
|
1440
|
+
return earliest;
|
|
1398
1441
|
}
|
|
1399
1442
|
function encodeProjectDir(projectPath) {
|
|
1400
1443
|
return projectPath.replace(/[^a-zA-Z0-9]/g, "-");
|
|
@@ -1406,6 +1449,11 @@ function firstTranscriptCwd(records) {
|
|
|
1406
1449
|
}
|
|
1407
1450
|
return void 0;
|
|
1408
1451
|
}
|
|
1452
|
+
async function loadImportStore(paths, options, sourceKind) {
|
|
1453
|
+
await assertSessionStoreSafe(paths);
|
|
1454
|
+
if (options.dryRun !== true) await assertLockStoreSafe2(paths);
|
|
1455
|
+
return loadExistingByExternalId(paths, sourceKind);
|
|
1456
|
+
}
|
|
1409
1457
|
async function loadExistingByExternalId(paths, sourceKind) {
|
|
1410
1458
|
const byExternalId = /* @__PURE__ */ new Map();
|
|
1411
1459
|
const add = (externalId, prior) => {
|
|
@@ -1431,7 +1479,8 @@ async function loadExistingByExternalId(paths, sourceKind) {
|
|
|
1431
1479
|
}
|
|
1432
1480
|
if (session.session.source.kind !== sourceKind) continue;
|
|
1433
1481
|
const sourceSizeBytes = session.session.source.source_size_bytes;
|
|
1434
|
-
const
|
|
1482
|
+
const startedAt = session.session.started_at;
|
|
1483
|
+
const prior = sourceSizeBytes !== void 0 ? { sessionId, startedAt, sourceSizeBytes } : { sessionId, startedAt };
|
|
1435
1484
|
const ext = session.session.source.external_id;
|
|
1436
1485
|
if (typeof ext === "string" && ext.length > 0) {
|
|
1437
1486
|
add(ext, prior);
|
|
@@ -1476,6 +1525,153 @@ async function selectTranscriptFiles(projectsRoot, projectPaths, options) {
|
|
|
1476
1525
|
}
|
|
1477
1526
|
return [...new Set(files)].sort();
|
|
1478
1527
|
}
|
|
1528
|
+
async function listTranscriptFiles(projectsRoot, projectPaths) {
|
|
1529
|
+
const files = [];
|
|
1530
|
+
for (const projectPath of projectPaths) {
|
|
1531
|
+
const transcriptDir = join2(projectsRoot, encodeProjectDir(projectPath));
|
|
1532
|
+
let entries;
|
|
1533
|
+
try {
|
|
1534
|
+
entries = await readdir(transcriptDir);
|
|
1535
|
+
} catch (error) {
|
|
1536
|
+
if (findErrorCode2(error, "ENOENT")) continue;
|
|
1537
|
+
throw new Error("Failed to read Claude transcript directory", { cause: error });
|
|
1538
|
+
}
|
|
1539
|
+
for (const name of entries) {
|
|
1540
|
+
if (name.endsWith(".jsonl")) files.push(join2(transcriptDir, name));
|
|
1541
|
+
}
|
|
1542
|
+
}
|
|
1543
|
+
return [...new Set(files)].sort();
|
|
1544
|
+
}
|
|
1545
|
+
async function indexConversations(files, createdAtMs, importedStartedAt) {
|
|
1546
|
+
const uuidsByFile = /* @__PURE__ */ new Map();
|
|
1547
|
+
const uuidsIn = async (file) => {
|
|
1548
|
+
let held = uuidsByFile.get(file);
|
|
1549
|
+
if (held === void 0) {
|
|
1550
|
+
held = await readTranscriptUuids(file);
|
|
1551
|
+
uuidsByFile.set(file, held);
|
|
1552
|
+
}
|
|
1553
|
+
return held;
|
|
1554
|
+
};
|
|
1555
|
+
const rootOf = /* @__PURE__ */ new Map();
|
|
1556
|
+
const byRoot = /* @__PURE__ */ new Map();
|
|
1557
|
+
for (const file of files) {
|
|
1558
|
+
const root = await readConversationRoot(file);
|
|
1559
|
+
if (root === void 0) continue;
|
|
1560
|
+
rootOf.set(file, root);
|
|
1561
|
+
const entry = { file, externalId: basename3(file, ".jsonl"), createdAtMs: Number.NaN };
|
|
1562
|
+
const list = byRoot.get(root.uuid);
|
|
1563
|
+
if (list === void 0) byRoot.set(root.uuid, [entry]);
|
|
1564
|
+
else list.push(entry);
|
|
1565
|
+
}
|
|
1566
|
+
for (const list of byRoot.values()) {
|
|
1567
|
+
if (list.length < 2) continue;
|
|
1568
|
+
for (const entry of list) entry.createdAtMs = await createdAtMs(entry.file);
|
|
1569
|
+
list.sort((a, b) => {
|
|
1570
|
+
const knownA = Number.isFinite(a.createdAtMs) && a.createdAtMs > 0;
|
|
1571
|
+
const knownB = Number.isFinite(b.createdAtMs) && b.createdAtMs > 0;
|
|
1572
|
+
if (knownA && knownB && a.createdAtMs !== b.createdAtMs) return a.createdAtMs - b.createdAtMs;
|
|
1573
|
+
if (knownA !== knownB) return knownA ? -1 : 1;
|
|
1574
|
+
return a.externalId < b.externalId ? -1 : a.externalId > b.externalId ? 1 : 0;
|
|
1575
|
+
});
|
|
1576
|
+
const startedAtMs = /* @__PURE__ */ new Map();
|
|
1577
|
+
for (const entry of list) {
|
|
1578
|
+
const ms = Date.parse(importedStartedAt(entry.externalId) ?? "");
|
|
1579
|
+
if (!Number.isNaN(ms)) startedAtMs.set(entry, ms);
|
|
1580
|
+
}
|
|
1581
|
+
const imported = [...startedAtMs.keys()].sort(
|
|
1582
|
+
(a, b) => (startedAtMs.get(a) ?? 0) - (startedAtMs.get(b) ?? 0)
|
|
1583
|
+
);
|
|
1584
|
+
list.forEach((entry, index) => {
|
|
1585
|
+
if (startedAtMs.has(entry)) list[index] = imported.shift() ?? entry;
|
|
1586
|
+
});
|
|
1587
|
+
const rootMs = Date.parse(rootOf.get(list[0]?.file ?? "")?.timestamp ?? "");
|
|
1588
|
+
const forks = list.filter((entry) => (startedAtMs.get(entry) ?? Number.NaN) > rootMs);
|
|
1589
|
+
if (forks.length === 0) continue;
|
|
1590
|
+
const rank = new Map(list.map((entry, index) => [entry, index]));
|
|
1591
|
+
const countedBy = /* @__PURE__ */ new Map();
|
|
1592
|
+
for (const [index, entry] of list.entries()) {
|
|
1593
|
+
if (startedAtMs.has(entry)) continue;
|
|
1594
|
+
for (const fork of forks) {
|
|
1595
|
+
const forkRank = rank.get(fork) ?? index;
|
|
1596
|
+
if (forkRank < index) continue;
|
|
1597
|
+
let counted = countedBy.get(fork);
|
|
1598
|
+
if (counted === void 0) {
|
|
1599
|
+
counted = await readTranscriptUuids(fork.file, importedStartedAt(fork.externalId));
|
|
1600
|
+
countedBy.set(fork, counted);
|
|
1601
|
+
}
|
|
1602
|
+
if (counted.size === 0) continue;
|
|
1603
|
+
const held = await uuidsIn(entry.file);
|
|
1604
|
+
if ([...counted].some((uuid) => held.has(uuid))) {
|
|
1605
|
+
rank.set(entry, Math.max(rank.get(entry) ?? index, forkRank + 0.5));
|
|
1606
|
+
}
|
|
1607
|
+
}
|
|
1608
|
+
}
|
|
1609
|
+
list.sort((a, b) => (rank.get(a) ?? 0) - (rank.get(b) ?? 0));
|
|
1610
|
+
}
|
|
1611
|
+
return {
|
|
1612
|
+
place(file) {
|
|
1613
|
+
const root = rootOf.get(file);
|
|
1614
|
+
if (root === void 0) return { root: void 0, older: [] };
|
|
1615
|
+
const list = byRoot.get(root.uuid) ?? [];
|
|
1616
|
+
const at = list.findIndex((entry) => entry.file === file);
|
|
1617
|
+
return { root, older: list.slice(0, Math.max(at, 0)).map((entry) => entry.file) };
|
|
1618
|
+
},
|
|
1619
|
+
async uuidsOf(older) {
|
|
1620
|
+
const uuids = /* @__PURE__ */ new Set();
|
|
1621
|
+
for (const file of older) {
|
|
1622
|
+
for (const uuid of await uuidsIn(file)) uuids.add(uuid);
|
|
1623
|
+
}
|
|
1624
|
+
return uuids;
|
|
1625
|
+
}
|
|
1626
|
+
};
|
|
1627
|
+
}
|
|
1628
|
+
async function readTranscriptUuids(file, notBefore) {
|
|
1629
|
+
const uuids = /* @__PURE__ */ new Set();
|
|
1630
|
+
try {
|
|
1631
|
+
const { records } = await readJsonlRecords(file);
|
|
1632
|
+
const kept = notBefore === void 0 ? records : claudeTranscriptOwnRecords(records, { notBefore });
|
|
1633
|
+
for (const record of kept) {
|
|
1634
|
+
const uuid = claudeTranscriptRecordUuid(record);
|
|
1635
|
+
if (uuid !== void 0) uuids.add(uuid);
|
|
1636
|
+
}
|
|
1637
|
+
} catch {
|
|
1638
|
+
}
|
|
1639
|
+
return uuids;
|
|
1640
|
+
}
|
|
1641
|
+
async function readConversationRoot(file) {
|
|
1642
|
+
const stream = createReadStream(file, { encoding: "utf8" });
|
|
1643
|
+
const rl = createInterface({ input: stream, crlfDelay: Number.POSITIVE_INFINITY });
|
|
1644
|
+
try {
|
|
1645
|
+
for await (const line of rl) {
|
|
1646
|
+
const trimmed = line.trim();
|
|
1647
|
+
if (trimmed.length === 0) continue;
|
|
1648
|
+
let parsed;
|
|
1649
|
+
try {
|
|
1650
|
+
parsed = JSON.parse(trimmed);
|
|
1651
|
+
} catch {
|
|
1652
|
+
continue;
|
|
1653
|
+
}
|
|
1654
|
+
if (!isObject(parsed)) continue;
|
|
1655
|
+
const uuid = claudeTranscriptRecordUuid(parsed);
|
|
1656
|
+
if (uuid === void 0) continue;
|
|
1657
|
+
const timestamp = parsed.timestamp;
|
|
1658
|
+
return typeof timestamp === "string" && timestamp.length > 0 ? { uuid, timestamp } : { uuid };
|
|
1659
|
+
}
|
|
1660
|
+
return void 0;
|
|
1661
|
+
} catch {
|
|
1662
|
+
return void 0;
|
|
1663
|
+
} finally {
|
|
1664
|
+
rl.close();
|
|
1665
|
+
stream.destroy();
|
|
1666
|
+
}
|
|
1667
|
+
}
|
|
1668
|
+
async function birthTimeMs(file) {
|
|
1669
|
+
try {
|
|
1670
|
+
return (await stat2(file)).birthtimeMs;
|
|
1671
|
+
} catch {
|
|
1672
|
+
return Number.NaN;
|
|
1673
|
+
}
|
|
1674
|
+
}
|
|
1479
1675
|
async function pathExists(file) {
|
|
1480
1676
|
try {
|
|
1481
1677
|
await stat2(file);
|
|
@@ -1611,7 +1807,8 @@ function printImportResult(options, results, counts) {
|
|
|
1611
1807
|
skippedLegacy,
|
|
1612
1808
|
skippedDecreased,
|
|
1613
1809
|
skippedDuplicate,
|
|
1614
|
-
skippedUnverifiable
|
|
1810
|
+
skippedUnverifiable,
|
|
1811
|
+
droppedCopies
|
|
1615
1812
|
} = counts;
|
|
1616
1813
|
if (options.json === true) {
|
|
1617
1814
|
console.log(
|
|
@@ -1625,6 +1822,7 @@ function printImportResult(options, results, counts) {
|
|
|
1625
1822
|
imported_count: results.length,
|
|
1626
1823
|
replaced_count: replaced,
|
|
1627
1824
|
reimported_count: reimported,
|
|
1825
|
+
dropped_copies_count: droppedCopies,
|
|
1628
1826
|
skipped_no_action: skippedNoAction,
|
|
1629
1827
|
skipped_already_imported: skippedExisting,
|
|
1630
1828
|
skipped_legacy_untracked: skippedLegacy,
|
|
@@ -1647,10 +1845,11 @@ function printImportResult(options, results, counts) {
|
|
|
1647
1845
|
skipParts.push(`${skippedUnverifiable} unverifiable (run 'basou verify')`);
|
|
1648
1846
|
const skipSuffix = skipParts.length > 0 ? `; skipped ${skipParts.join(", ")}` : "";
|
|
1649
1847
|
const eventsPart = replaced > 0 ? `${eventTotal} events, ${replaced} replaced` : `${eventTotal} events`;
|
|
1848
|
+
const copiesPart = droppedCopies > 0 ? ` (${droppedCopies} without the records ${droppedCopies === 1 ? "its transcript" : "their transcripts"} copied from an older one)` : "";
|
|
1650
1849
|
if (isDry) {
|
|
1651
1850
|
const parts = [];
|
|
1652
1851
|
if (results.length > 0) parts.push(`import ${results.length} session(s) (${eventsPart})`);
|
|
1653
|
-
if (reimported > 0) parts.push(`re-import ${reimported} changed session(s)`);
|
|
1852
|
+
if (reimported > 0) parts.push(`re-import ${reimported} changed session(s)${copiesPart}`);
|
|
1654
1853
|
const head = parts.length > 0 ? `Dry run: would ${parts.join(", ")}` : "Dry run: no changes";
|
|
1655
1854
|
console.log(`${head}${skipSuffix}`);
|
|
1656
1855
|
return;
|
|
@@ -1668,7 +1867,7 @@ function printImportResult(options, results, counts) {
|
|
|
1668
1867
|
}
|
|
1669
1868
|
if (reimported > 0) {
|
|
1670
1869
|
segments.push(
|
|
1671
|
-
`${results.length > 0 ? "re-imported" : "Re-imported"} ${reimported} changed session(s)`
|
|
1870
|
+
`${results.length > 0 ? "re-imported" : "Re-imported"} ${reimported} changed session(s)${copiesPart}`
|
|
1672
1871
|
);
|
|
1673
1872
|
}
|
|
1674
1873
|
console.log(`${segments.join(", ")}${skipSuffix}`);
|
|
@@ -1754,6 +1953,7 @@ async function runImport(adapter, fn) {
|
|
|
1754
1953
|
importedCount: readCount(json.imported_count),
|
|
1755
1954
|
replacedCount: readCount(json.replaced_count),
|
|
1756
1955
|
reimportedCount: readCount(json.reimported_count),
|
|
1956
|
+
droppedCopiesCount: readCount(json.dropped_copies_count),
|
|
1757
1957
|
skippedNoAction: readCount(json.skipped_no_action),
|
|
1758
1958
|
skippedAlreadyImported: readCount(json.skipped_already_imported),
|
|
1759
1959
|
skippedLegacyUntracked: readCount(json.skipped_legacy_untracked),
|
|
@@ -2195,9 +2395,9 @@ async function checkAndRecord(options, ctx) {
|
|
|
2195
2395
|
${parsed.errors.map((e) => ` - ${displayPath3(e)}`).join("\n")}`
|
|
2196
2396
|
);
|
|
2197
2397
|
}
|
|
2198
|
-
const recordedAt = ctx.nowProvider?.() ?? /* @__PURE__ */ new Date();
|
|
2199
2398
|
const previous = await previousOf(loaded);
|
|
2200
2399
|
const measurement = await measureLoaded(loaded, ctx, parsed.input.judged_by.model, previous);
|
|
2400
|
+
const recordedAt = ctx.nowProvider?.() ?? /* @__PURE__ */ new Date();
|
|
2201
2401
|
if (measurement.digest !== parsed.input.measure_digest) {
|
|
2202
2402
|
throw new RecordRefusal(
|
|
2203
2403
|
`measure_digest is ${parsed.input.measure_digest}, but the board measures ${measurement.digest} now: something it measures or board.yaml changed since the judgement, or basou was rebuilt or upgraded since that measurement. Measure again and judge that; nothing was written.`
|
|
@@ -2751,7 +2951,8 @@ function printMeasurementText(m, hasRecords2) {
|
|
|
2751
2951
|
const width = Math.max(...measures.map(([id]) => id.length));
|
|
2752
2952
|
lines.push("", "Measures:");
|
|
2753
2953
|
for (const [id, measure] of measures) {
|
|
2754
|
-
const
|
|
2954
|
+
const counted = typeof measure.value === "number" || Array.isArray(measure.value);
|
|
2955
|
+
const unit = counted ? ` ${displayPath3(measure.unit)}` : "";
|
|
2755
2956
|
const listed = Array.isArray(measure.value) ? " (paths in --json)" : "";
|
|
2756
2957
|
const lane = measure.lane === void 0 ? "" : ` [${measure.lane.join(", ")}]`;
|
|
2757
2958
|
lines.push(` ${id.padEnd(width)} ${shownValue(measure.value)}${unit}${listed}${lane}`);
|
|
@@ -9994,7 +10195,10 @@ function describeImport(outcome) {
|
|
|
9994
10195
|
}
|
|
9995
10196
|
const verb = outcome.dryRun ? "would import" : "imported";
|
|
9996
10197
|
const parts = [`${outcome.importedCount} session(s)`, `${outcome.eventTotal} events`];
|
|
9997
|
-
if (outcome.reimportedCount > 0)
|
|
10198
|
+
if (outcome.reimportedCount > 0) {
|
|
10199
|
+
const copies = outcome.droppedCopiesCount > 0 ? ` (${outcome.droppedCopiesCount} without records copied from an older transcript)` : "";
|
|
10200
|
+
parts.push(`${outcome.reimportedCount} re-imported${copies}`);
|
|
10201
|
+
}
|
|
9998
10202
|
if (outcome.replacedCount > 0) parts.push(`${outcome.replacedCount} replaced`);
|
|
9999
10203
|
if (outcome.skippedAlreadyImported > 0)
|
|
10000
10204
|
parts.push(`${outcome.skippedAlreadyImported} already imported`);
|
|
@@ -14321,7 +14525,8 @@ var BOARD_HTML = `<!doctype html>
|
|
|
14321
14525
|
el('ul', null, m.value.map(function (p) { return el('li', { text: p }); }))
|
|
14322
14526
|
])]);
|
|
14323
14527
|
}
|
|
14324
|
-
|
|
14528
|
+
// A string (a file's name, a capture) is no amount: it takes no unit.
|
|
14529
|
+
return el('li', { text: m.id + ': ' + num(m.value) + (typeof m.value === 'number' ? ' ' + m.unit : '') });
|
|
14325
14530
|
})));
|
|
14326
14531
|
}
|
|
14327
14532
|
return el('div', { class: 'lane', 'data-lane': lane.id }, body);
|
|
@@ -16129,7 +16334,7 @@ function installExperimentalNotice(program, names = EXPERIMENTAL_COMMANDS, write
|
|
|
16129
16334
|
function readBuildStamp() {
|
|
16130
16335
|
if (false) return void 0;
|
|
16131
16336
|
try {
|
|
16132
|
-
return JSON.parse('{"version":"0.71.
|
|
16337
|
+
return JSON.parse('{"version":"0.71.2","commit":"c7fc0ed","committedAt":"2026-10-19T20:00:28+09:00"}');
|
|
16133
16338
|
} catch {
|
|
16134
16339
|
return void 0;
|
|
16135
16340
|
}
|