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