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