@basou/cli 0.4.0 → 0.6.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 +1892 -198
- package/dist/index.js.map +1 -1
- package/dist/program.d.ts +15 -0
- package/dist/program.js +5285 -0
- package/dist/program.js.map +1 -0
- package/package.json +8 -4
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;
|
|
@@ -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,66 +1782,282 @@ 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
|
-
getDiff,
|
|
1415
|
-
getSnapshot as getSnapshot2,
|
|
1416
|
-
overwriteYamlFile as overwriteYamlFile2,
|
|
1417
|
-
prefixedUlid as prefixedUlid4,
|
|
1418
|
-
readManifest as readManifest3,
|
|
1419
|
-
readYamlFile as readYamlFile3,
|
|
1420
|
-
resolveClaudeCodeCommand,
|
|
1421
|
-
resolveRepositoryRoot as resolveRepositoryRoot7,
|
|
1422
|
-
SessionSchema as SessionSchema2,
|
|
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
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
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);
|
|
2055
|
+
const sessionId = prefixedUlid4("ses");
|
|
2056
|
+
const sessionDir = join4(paths.sessions, sessionId);
|
|
2057
|
+
await mkdir2(sessionDir, { recursive: true });
|
|
2058
|
+
const startedAt = now().toISOString();
|
|
2059
|
+
const sessionYamlPath = join4(sessionDir, "session.yaml");
|
|
2060
|
+
const session = buildInitialSession2({
|
|
1464
2061
|
id: sessionId,
|
|
1465
2062
|
command,
|
|
1466
2063
|
args,
|
|
@@ -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
2399
|
acquireLock as acquireLock2,
|
|
1803
2400
|
appendEventToExistingSession as appendEventToExistingSession2,
|
|
1804
|
-
assertBasouRootSafe as
|
|
1805
|
-
basouPaths as
|
|
1806
|
-
findErrorCode as
|
|
1807
|
-
importSessionFromJson,
|
|
2401
|
+
assertBasouRootSafe as assertBasouRootSafe9,
|
|
2402
|
+
basouPaths as basouPaths9,
|
|
2403
|
+
findErrorCode as findErrorCode7,
|
|
2404
|
+
importSessionFromJson as importSessionFromJson2,
|
|
1808
2405
|
loadSessionEntries,
|
|
1809
2406
|
readAllEvents,
|
|
1810
|
-
readManifest as
|
|
2407
|
+
readManifest as readManifest5,
|
|
1811
2408
|
readYamlFile as readYamlFile4,
|
|
1812
|
-
resolveRepositoryRoot as
|
|
2409
|
+
resolveRepositoryRoot as resolveRepositoryRoot10,
|
|
1813
2410
|
resolveSessionId as resolveSessionId2,
|
|
1814
2411
|
resolveTaskId,
|
|
1815
|
-
SessionImportPayloadSchema,
|
|
2412
|
+
SessionImportPayloadSchema as SessionImportPayloadSchema2,
|
|
1816
2413
|
SessionSchema as SessionSchema3,
|
|
1817
|
-
SessionStatusSchema
|
|
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,22 @@ 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
|
+
if (w.availability.machineActive) {
|
|
2636
|
+
parts.push(`machine ${formatDurationMs(w.machineActiveTimeMs)}`);
|
|
2637
|
+
}
|
|
2638
|
+
parts.push(`span ${formatDurationMs(w.sessionSpanMs)}${w.open ? " (open)" : ""}`);
|
|
2639
|
+
parts.push(
|
|
2640
|
+
w.availability.commandTime ? `command ${formatDurationMs(w.commandTimeMs)}` : "command n/a (import)"
|
|
2641
|
+
);
|
|
2642
|
+
return parts.join(", ");
|
|
2643
|
+
}
|
|
2014
2644
|
function formatWorkingDir(workingDir, repositoryRoot, options) {
|
|
2015
2645
|
if (options.fullPath === true) return workingDir;
|
|
2016
2646
|
if (!isAbsolute(workingDir)) {
|
|
@@ -2091,7 +2721,7 @@ function eventVariantSummary(ev) {
|
|
|
2091
2721
|
return `${ev.stream} "${ev.summary}" raw_ref=${ev.raw_ref}`;
|
|
2092
2722
|
}
|
|
2093
2723
|
}
|
|
2094
|
-
function
|
|
2724
|
+
function shortId3(id) {
|
|
2095
2725
|
return sliceShort2(id, SHORT_ID_BASE_LEN2);
|
|
2096
2726
|
}
|
|
2097
2727
|
function shortTaskId2(id) {
|
|
@@ -2101,8 +2731,8 @@ function shortTaskId2(id) {
|
|
|
2101
2731
|
return id.slice(0, SHORT_ID_BASE_LEN2);
|
|
2102
2732
|
}
|
|
2103
2733
|
function sliceShort2(id, len) {
|
|
2104
|
-
if (id.startsWith(
|
|
2105
|
-
return id.slice(
|
|
2734
|
+
if (id.startsWith(SES_PREFIX3)) {
|
|
2735
|
+
return id.slice(SES_PREFIX3.length, SES_PREFIX3.length + len);
|
|
2106
2736
|
}
|
|
2107
2737
|
return id.slice(0, len);
|
|
2108
2738
|
}
|
|
@@ -2133,7 +2763,7 @@ function maxLen2(values, floor) {
|
|
|
2133
2763
|
}
|
|
2134
2764
|
async function resolveRepositoryRootForSession(cwd, subcmd) {
|
|
2135
2765
|
try {
|
|
2136
|
-
return await
|
|
2766
|
+
return await resolveRepositoryRoot10(cwd);
|
|
2137
2767
|
} catch (error) {
|
|
2138
2768
|
if (error instanceof Error && error.message === "Not a git repository") {
|
|
2139
2769
|
throw new Error(
|
|
@@ -2144,11 +2774,11 @@ async function resolveRepositoryRootForSession(cwd, subcmd) {
|
|
|
2144
2774
|
throw error;
|
|
2145
2775
|
}
|
|
2146
2776
|
}
|
|
2147
|
-
async function
|
|
2777
|
+
async function assertWorkspaceInitialized7(basouRoot) {
|
|
2148
2778
|
try {
|
|
2149
|
-
await
|
|
2779
|
+
await assertBasouRootSafe9(basouRoot);
|
|
2150
2780
|
} catch (error) {
|
|
2151
|
-
if (
|
|
2781
|
+
if (findErrorCode7(error, "ENOENT")) {
|
|
2152
2782
|
throw new Error("Workspace not initialized. Run 'basou init' first.");
|
|
2153
2783
|
}
|
|
2154
2784
|
throw error;
|
|
@@ -2186,24 +2816,24 @@ async function runSessionImport(options, ctx = {}) {
|
|
|
2186
2816
|
async function doRunSessionImport(options, ctx) {
|
|
2187
2817
|
const cwd = ctx.cwd ?? process.cwd();
|
|
2188
2818
|
const repositoryRoot = await resolveRepositoryRootForSession(cwd, "import");
|
|
2189
|
-
const paths =
|
|
2190
|
-
await
|
|
2191
|
-
const manifest = await
|
|
2819
|
+
const paths = basouPaths9(repositoryRoot);
|
|
2820
|
+
await assertWorkspaceInitialized7(paths.root);
|
|
2821
|
+
const manifest = await readManifest5(paths);
|
|
2192
2822
|
const rawBody = await readInputFile(options.from);
|
|
2193
2823
|
const json = parseJsonStrict(rawBody);
|
|
2194
|
-
const parsed =
|
|
2824
|
+
const parsed = SessionImportPayloadSchema2.safeParse(json);
|
|
2195
2825
|
if (!parsed.success) {
|
|
2196
2826
|
throw new Error("Invalid import payload", { cause: parsed.error });
|
|
2197
2827
|
}
|
|
2198
2828
|
if (parsed.data.schema_version !== "0.1.0") {
|
|
2199
2829
|
throw new Error(`Unsupported import schema_version: ${parsed.data.schema_version}`);
|
|
2200
2830
|
}
|
|
2201
|
-
const
|
|
2202
|
-
if (options.label !== void 0)
|
|
2831
|
+
const importOptions2 = { dryRun: options.dryRun === true };
|
|
2832
|
+
if (options.label !== void 0) importOptions2.labelOverride = options.label;
|
|
2203
2833
|
if (options.task !== void 0) {
|
|
2204
|
-
|
|
2834
|
+
importOptions2.taskIdOverride = await resolveTaskId(paths, options.task);
|
|
2205
2835
|
}
|
|
2206
|
-
const result = await
|
|
2836
|
+
const result = await importSessionFromJson2(paths, manifest, parsed.data, importOptions2);
|
|
2207
2837
|
const sanitizeReport = result.pathSanitizeReport;
|
|
2208
2838
|
if (sanitizeReport.relatedFiles > 0 || sanitizeReport.workingDirectoryRewritten) {
|
|
2209
2839
|
const wdCount = sanitizeReport.workingDirectoryRewritten ? 1 : 0;
|
|
@@ -2215,12 +2845,12 @@ async function doRunSessionImport(options, ctx) {
|
|
|
2215
2845
|
}
|
|
2216
2846
|
async function readInputFile(path) {
|
|
2217
2847
|
try {
|
|
2218
|
-
return await
|
|
2848
|
+
return await readFile2(path, "utf8");
|
|
2219
2849
|
} catch (error) {
|
|
2220
|
-
if (
|
|
2850
|
+
if (findErrorCode7(error, "ENOENT")) {
|
|
2221
2851
|
throw new Error("Import source not found", { cause: error });
|
|
2222
2852
|
}
|
|
2223
|
-
if (
|
|
2853
|
+
if (findErrorCode7(error, "EISDIR")) {
|
|
2224
2854
|
throw new Error("Import source is not a file", { cause: error });
|
|
2225
2855
|
}
|
|
2226
2856
|
throw new Error("Failed to read import source", { cause: error });
|
|
@@ -2253,7 +2883,7 @@ function parseTaskIdOverride(raw) {
|
|
|
2253
2883
|
}
|
|
2254
2884
|
function printSessionImportResult(options, result) {
|
|
2255
2885
|
const isDry = options.dryRun === true;
|
|
2256
|
-
const sid =
|
|
2886
|
+
const sid = shortId3(result.sessionId);
|
|
2257
2887
|
if (options.json === true) {
|
|
2258
2888
|
console.log(
|
|
2259
2889
|
JSON.stringify({
|
|
@@ -2273,7 +2903,7 @@ function printSessionImportResult(options, result) {
|
|
|
2273
2903
|
return;
|
|
2274
2904
|
}
|
|
2275
2905
|
console.log(
|
|
2276
|
-
`Imported session ${sid} (${result.eventCount} events) from ${
|
|
2906
|
+
`Imported session ${sid} (${result.eventCount} events) from ${basename3(options.from)}`
|
|
2277
2907
|
);
|
|
2278
2908
|
}
|
|
2279
2909
|
var NOTE_BODY_PREVIEW_LIMIT = 80;
|
|
@@ -2300,8 +2930,8 @@ async function doRunSessionNote(sessionIdInput, options, ctx) {
|
|
|
2300
2930
|
}
|
|
2301
2931
|
const cwd = ctx.cwd ?? process.cwd();
|
|
2302
2932
|
const repositoryRoot = await resolveRepositoryRootForSession(cwd, "note");
|
|
2303
|
-
const paths =
|
|
2304
|
-
await
|
|
2933
|
+
const paths = basouPaths9(repositoryRoot);
|
|
2934
|
+
await assertWorkspaceInitialized7(paths.root);
|
|
2305
2935
|
const sessionId = await resolveSessionId2(paths, sessionIdInput);
|
|
2306
2936
|
const body = hasBody ? options.body : await readNoteFile(options.fromFile);
|
|
2307
2937
|
if (body.length === 0) {
|
|
@@ -2332,12 +2962,12 @@ async function doRunSessionNote(sessionIdInput, options, ctx) {
|
|
|
2332
2962
|
}
|
|
2333
2963
|
async function readNoteFile(path) {
|
|
2334
2964
|
try {
|
|
2335
|
-
return await
|
|
2965
|
+
return await readFile2(path, "utf8");
|
|
2336
2966
|
} catch (error) {
|
|
2337
|
-
if (
|
|
2967
|
+
if (findErrorCode7(error, "ENOENT")) {
|
|
2338
2968
|
throw new Error("Note source not found", { cause: error });
|
|
2339
2969
|
}
|
|
2340
|
-
if (
|
|
2970
|
+
if (findErrorCode7(error, "EISDIR")) {
|
|
2341
2971
|
throw new Error("Note source is not a file", { cause: error });
|
|
2342
2972
|
}
|
|
2343
2973
|
throw new Error("Failed to read note source", { cause: error });
|
|
@@ -2350,7 +2980,7 @@ function parseNoteBodyOption(raw) {
|
|
|
2350
2980
|
return raw;
|
|
2351
2981
|
}
|
|
2352
2982
|
function printSessionNoteResult(options, sessionId, eventId, sessionStatus, body) {
|
|
2353
|
-
const sid =
|
|
2983
|
+
const sid = shortId3(sessionId);
|
|
2354
2984
|
if (options.json === true) {
|
|
2355
2985
|
console.log(
|
|
2356
2986
|
JSON.stringify({
|
|
@@ -2366,14 +2996,144 @@ function printSessionNoteResult(options, sessionId, eventId, sessionStatus, body
|
|
|
2366
2996
|
console.log(`Added note to session ${sid} (${sessionStatus}): ${preview}`);
|
|
2367
2997
|
}
|
|
2368
2998
|
|
|
2999
|
+
// src/commands/stats.ts
|
|
3000
|
+
import {
|
|
3001
|
+
assertBasouRootSafe as assertBasouRootSafe10,
|
|
3002
|
+
basouPaths as basouPaths10,
|
|
3003
|
+
computeWorkStats,
|
|
3004
|
+
findErrorCode as findErrorCode8,
|
|
3005
|
+
resolveRepositoryRoot as resolveRepositoryRoot11
|
|
3006
|
+
} from "@basou/core";
|
|
3007
|
+
function registerStatsCommand(program2) {
|
|
3008
|
+
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) => {
|
|
3009
|
+
await runStats(options);
|
|
3010
|
+
});
|
|
3011
|
+
}
|
|
3012
|
+
async function runStats(options, ctx = {}) {
|
|
3013
|
+
try {
|
|
3014
|
+
await doRunStats(options, ctx);
|
|
3015
|
+
} catch (error) {
|
|
3016
|
+
renderCliError(error, { verbose: isVerbose(options) });
|
|
3017
|
+
process.exitCode = 1;
|
|
3018
|
+
}
|
|
3019
|
+
}
|
|
3020
|
+
async function doRunStats(options, ctx) {
|
|
3021
|
+
const cwd = ctx.cwd ?? process.cwd();
|
|
3022
|
+
const repositoryRoot = await resolveRepositoryRootForStats(cwd);
|
|
3023
|
+
const paths = basouPaths10(repositoryRoot);
|
|
3024
|
+
await assertWorkspaceInitialized8(paths.root);
|
|
3025
|
+
const now = ctx.nowProvider?.() ?? /* @__PURE__ */ new Date();
|
|
3026
|
+
const result = await computeWorkStats({
|
|
3027
|
+
paths,
|
|
3028
|
+
now,
|
|
3029
|
+
onWarning: (w, sid) => printReplayWarning(w, sid),
|
|
3030
|
+
onSessionSkip: (sid, reason) => printSessionSkip(sid, reason)
|
|
3031
|
+
});
|
|
3032
|
+
if (options.json === true) {
|
|
3033
|
+
console.log(JSON.stringify(result, null, 2));
|
|
3034
|
+
return;
|
|
3035
|
+
}
|
|
3036
|
+
printStatsText(result, options.bySource === true, options.byDay === true);
|
|
3037
|
+
}
|
|
3038
|
+
function printStatsText(result, bySource, byDay) {
|
|
3039
|
+
const t = result.totals;
|
|
3040
|
+
const statusPart = result.byStatus.length > 0 ? ` (${result.byStatus.map((s) => `${s.status} ${s.count}`).join(", ")})` : "";
|
|
3041
|
+
console.log(`Sessions: ${t.sessionCount}${statusPart}`);
|
|
3042
|
+
console.log("");
|
|
3043
|
+
console.log("Volume (what the AI produced):");
|
|
3044
|
+
const tokenSessions = result.sessions.filter((s) => s.availability.tokens).length;
|
|
3045
|
+
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)";
|
|
3046
|
+
console.log(` Output tokens: ${formatInt(t.tokens.output)}${tokenCaveat}`);
|
|
3047
|
+
if (t.tokens.reasoning > 0) {
|
|
3048
|
+
console.log(` Reasoning tokens: ${formatInt(t.tokens.reasoning)} (Codex)`);
|
|
3049
|
+
}
|
|
3050
|
+
console.log(
|
|
3051
|
+
` Actions: ${t.commandCount} commands, ${t.fileChangedCount} files, ${t.decisionCount} decisions`
|
|
3052
|
+
);
|
|
3053
|
+
console.log("");
|
|
3054
|
+
console.log("Time (proxies for human harness labor; active = billing primary):");
|
|
3055
|
+
const turnSessions = result.sessions.filter((s) => s.activeTimeBasis === "engaged-turns").length;
|
|
3056
|
+
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`;
|
|
3057
|
+
console.log(
|
|
3058
|
+
` Billable active: ${formatDurationMs(t.billableActiveTimeMs)} (union; ${basisCaveat}; idle gaps > 5m excluded; tz ${result.timeZone})`
|
|
3059
|
+
);
|
|
3060
|
+
if (t.activeTimeMs !== t.billableActiveTimeMs) {
|
|
3061
|
+
console.log(
|
|
3062
|
+
` Summed: ${formatDurationMs(t.activeTimeMs)} (per-session sum; concurrent sessions double-counted)`
|
|
3063
|
+
);
|
|
3064
|
+
}
|
|
3065
|
+
if (t.machineActiveAvailable) {
|
|
3066
|
+
const machineSessions = result.sessions.filter((s) => s.availability.machineActive).length;
|
|
3067
|
+
console.log(
|
|
3068
|
+
` Model working: ${formatDurationMs(t.machineActiveTimeMs)} (model compute, subset of active; Codex turn duration on ${machineSessions} of ${t.sessionCount} sessions; summed, not wall-clock-deduped)`
|
|
3069
|
+
);
|
|
3070
|
+
}
|
|
3071
|
+
const openPart = t.openSessionCount > 0 ? `; ${t.openSessionCount} open counted to now` : "";
|
|
3072
|
+
console.log(
|
|
3073
|
+
` Span: ${formatDurationMs(t.sessionSpanMs)} (total elapsed${openPart})`
|
|
3074
|
+
);
|
|
3075
|
+
const cmdCaveat = t.commandTimeReliable ? "" : "; some sessions (e.g. claude-code-import) report 0 shell time";
|
|
3076
|
+
console.log(
|
|
3077
|
+
` Command: ${formatDurationMs(t.commandTimeMs)} (real shell execution${cmdCaveat})`
|
|
3078
|
+
);
|
|
3079
|
+
if (bySource && result.bySource.length > 0) {
|
|
3080
|
+
console.log("");
|
|
3081
|
+
console.log("By source:");
|
|
3082
|
+
for (const s of result.bySource) {
|
|
3083
|
+
console.log(` ${s.sourceKind}: ${describeSource(s)}`);
|
|
3084
|
+
}
|
|
3085
|
+
}
|
|
3086
|
+
if (byDay && result.byDay.length > 0) {
|
|
3087
|
+
console.log("");
|
|
3088
|
+
console.log("By day (billable time x volume):");
|
|
3089
|
+
for (const d of result.byDay) {
|
|
3090
|
+
const machine = d.machineActiveTimeMs > 0 ? ` (model ${formatDurationMs(d.machineActiveTimeMs)})` : "";
|
|
3091
|
+
console.log(
|
|
3092
|
+
` ${d.date}: ${formatDurationMs(d.billableActiveTimeMs)} active${machine}, ${formatInt(d.tokens.output)} out tok, ${d.commandCount} cmd / ${d.fileChangedCount} files / ${d.decisionCount} dec`
|
|
3093
|
+
);
|
|
3094
|
+
}
|
|
3095
|
+
}
|
|
3096
|
+
}
|
|
3097
|
+
function describeSource(s) {
|
|
3098
|
+
const cmd = s.commandTimeReliable ? formatDurationMs(s.commandTimeMs) : "n/a";
|
|
3099
|
+
const tokens = s.tokensAvailable ? `${formatInt(s.tokens.output)} out tok` : "no tokens";
|
|
3100
|
+
const machine = s.machineActiveAvailable ? `, model ${formatDurationMs(s.machineActiveTimeMs)}` : "";
|
|
3101
|
+
return `${s.sessionCount} sessions, ${tokens}, active ${formatDurationMs(s.activeTimeMs)}${machine}, command ${cmd}`;
|
|
3102
|
+
}
|
|
3103
|
+
function formatInt(n) {
|
|
3104
|
+
return n.toLocaleString("en-US");
|
|
3105
|
+
}
|
|
3106
|
+
async function resolveRepositoryRootForStats(cwd) {
|
|
3107
|
+
try {
|
|
3108
|
+
return await resolveRepositoryRoot11(cwd);
|
|
3109
|
+
} catch (error) {
|
|
3110
|
+
if (error instanceof Error && error.message === "Not a git repository") {
|
|
3111
|
+
throw new Error("Not a git repository. Run 'git init' first, then re-run 'basou stats'.", {
|
|
3112
|
+
cause: error
|
|
3113
|
+
});
|
|
3114
|
+
}
|
|
3115
|
+
throw error;
|
|
3116
|
+
}
|
|
3117
|
+
}
|
|
3118
|
+
async function assertWorkspaceInitialized8(basouRoot) {
|
|
3119
|
+
try {
|
|
3120
|
+
await assertBasouRootSafe10(basouRoot);
|
|
3121
|
+
} catch (error) {
|
|
3122
|
+
if (findErrorCode8(error, "ENOENT")) {
|
|
3123
|
+
throw new Error("Workspace not initialized. Run 'basou init' first.");
|
|
3124
|
+
}
|
|
3125
|
+
throw error;
|
|
3126
|
+
}
|
|
3127
|
+
}
|
|
3128
|
+
|
|
2369
3129
|
// src/commands/status.ts
|
|
2370
3130
|
import {
|
|
2371
|
-
assertBasouRootSafe as
|
|
2372
|
-
basouPaths as
|
|
3131
|
+
assertBasouRootSafe as assertBasouRootSafe11,
|
|
3132
|
+
basouPaths as basouPaths11,
|
|
2373
3133
|
buildStatusSnapshot,
|
|
2374
|
-
findErrorCode as
|
|
2375
|
-
readManifest as
|
|
2376
|
-
resolveRepositoryRoot as
|
|
3134
|
+
findErrorCode as findErrorCode9,
|
|
3135
|
+
readManifest as readManifest6,
|
|
3136
|
+
resolveRepositoryRoot as resolveRepositoryRoot12,
|
|
2377
3137
|
writeStatus
|
|
2378
3138
|
} from "@basou/core";
|
|
2379
3139
|
function registerStatusCommand(program2) {
|
|
@@ -2392,20 +3152,20 @@ async function runStatus(options, ctx = {}) {
|
|
|
2392
3152
|
async function doRunStatus(options, ctx) {
|
|
2393
3153
|
const cwd = ctx.cwd ?? process.cwd();
|
|
2394
3154
|
const repositoryRoot = await resolveRepositoryRootForStatus(cwd);
|
|
2395
|
-
const paths =
|
|
3155
|
+
const paths = basouPaths11(repositoryRoot);
|
|
2396
3156
|
try {
|
|
2397
|
-
await
|
|
3157
|
+
await assertBasouRootSafe11(paths.root);
|
|
2398
3158
|
} catch (error) {
|
|
2399
|
-
if (
|
|
3159
|
+
if (findErrorCode9(error, "ENOENT")) {
|
|
2400
3160
|
throw new Error("Workspace not initialized. Run 'basou init' first.");
|
|
2401
3161
|
}
|
|
2402
3162
|
throw error;
|
|
2403
3163
|
}
|
|
2404
3164
|
let manifest;
|
|
2405
3165
|
try {
|
|
2406
|
-
manifest = await
|
|
3166
|
+
manifest = await readManifest6(paths);
|
|
2407
3167
|
} catch (error) {
|
|
2408
|
-
if (
|
|
3168
|
+
if (findErrorCode9(error, "ENOENT")) {
|
|
2409
3169
|
throw new Error("Workspace not initialized. Run 'basou init' first.");
|
|
2410
3170
|
}
|
|
2411
3171
|
throw new Error("Failed to read workspace manifest", { cause: error });
|
|
@@ -2429,7 +3189,7 @@ function renderTextStatus(s) {
|
|
|
2429
3189
|
}
|
|
2430
3190
|
async function resolveRepositoryRootForStatus(cwd) {
|
|
2431
3191
|
try {
|
|
2432
|
-
return await
|
|
3192
|
+
return await resolveRepositoryRoot12(cwd);
|
|
2433
3193
|
} catch (error) {
|
|
2434
3194
|
if (error instanceof Error && error.message === "Not a git repository") {
|
|
2435
3195
|
throw new Error("Not a git repository. Run 'git init' first, then re-run 'basou status'.", {
|
|
@@ -2441,28 +3201,28 @@ async function resolveRepositoryRootForStatus(cwd) {
|
|
|
2441
3201
|
}
|
|
2442
3202
|
|
|
2443
3203
|
// src/commands/task.ts
|
|
2444
|
-
import { readFile as
|
|
2445
|
-
import { join as
|
|
3204
|
+
import { readFile as readFile3 } from "fs/promises";
|
|
3205
|
+
import { join as join6 } from "path";
|
|
2446
3206
|
import {
|
|
2447
3207
|
archiveTask,
|
|
2448
|
-
assertBasouRootSafe as
|
|
2449
|
-
basouPaths as
|
|
3208
|
+
assertBasouRootSafe as assertBasouRootSafe12,
|
|
3209
|
+
basouPaths as basouPaths12,
|
|
2450
3210
|
createTaskWithEvent,
|
|
2451
3211
|
deleteTask,
|
|
2452
3212
|
editTask,
|
|
2453
3213
|
enumerateArchivedTaskIds,
|
|
2454
|
-
findErrorCode as
|
|
3214
|
+
findErrorCode as findErrorCode10,
|
|
2455
3215
|
loadSessionEntries as loadSessionEntries2,
|
|
2456
3216
|
loadTaskEntries,
|
|
2457
3217
|
prefixedUlid as prefixedUlid5,
|
|
2458
|
-
readManifest as
|
|
3218
|
+
readManifest as readManifest7,
|
|
2459
3219
|
readTaskFile,
|
|
2460
3220
|
readTaskFileWithArchiveFallback,
|
|
2461
3221
|
reconcileAllTasks,
|
|
2462
3222
|
reconcileTask,
|
|
2463
3223
|
refreshTaskLinkedSessions,
|
|
2464
3224
|
replayEvents as replayEvents2,
|
|
2465
|
-
resolveRepositoryRoot as
|
|
3225
|
+
resolveRepositoryRoot as resolveRepositoryRoot13,
|
|
2466
3226
|
resolveSessionId as resolveSessionId3,
|
|
2467
3227
|
resolveTaskId as resolveTaskId2,
|
|
2468
3228
|
TaskStatusSchema,
|
|
@@ -2548,8 +3308,8 @@ async function doRunTaskNew(options, ctx) {
|
|
|
2548
3308
|
}
|
|
2549
3309
|
const cwd = ctx.cwd ?? process.cwd();
|
|
2550
3310
|
const repositoryRoot = await resolveRepositoryRootForTask(cwd, "new");
|
|
2551
|
-
const paths =
|
|
2552
|
-
await
|
|
3311
|
+
const paths = basouPaths12(repositoryRoot);
|
|
3312
|
+
await assertWorkspaceInitialized9(paths.root);
|
|
2553
3313
|
const description = options.description !== void 0 ? options.description : options.fromFile !== void 0 ? await readDescriptionFile(options.fromFile) : "";
|
|
2554
3314
|
const now = ctx.nowProvider !== void 0 ? ctx.nowProvider() : /* @__PURE__ */ new Date();
|
|
2555
3315
|
const occurredAt = now.toISOString();
|
|
@@ -2583,7 +3343,7 @@ async function doRunTaskNew(options, ctx) {
|
|
|
2583
3343
|
});
|
|
2584
3344
|
return;
|
|
2585
3345
|
}
|
|
2586
|
-
const manifest = await
|
|
3346
|
+
const manifest = await readManifest7(paths);
|
|
2587
3347
|
const result = await createTaskWithEvent({
|
|
2588
3348
|
mode: "ad-hoc",
|
|
2589
3349
|
paths,
|
|
@@ -2657,8 +3417,8 @@ async function runTaskList(options, ctx = {}) {
|
|
|
2657
3417
|
async function doRunTaskList(options, ctx) {
|
|
2658
3418
|
const cwd = ctx.cwd ?? process.cwd();
|
|
2659
3419
|
const repositoryRoot = await resolveRepositoryRootForTask(cwd, "list");
|
|
2660
|
-
const paths =
|
|
2661
|
-
await
|
|
3420
|
+
const paths = basouPaths12(repositoryRoot);
|
|
3421
|
+
await assertWorkspaceInitialized9(paths.root);
|
|
2662
3422
|
const entries = await loadTaskEntries(paths, {
|
|
2663
3423
|
onSkip: (id, reason) => printTaskSkip(id, reason)
|
|
2664
3424
|
});
|
|
@@ -2761,15 +3521,15 @@ async function runTaskShow(idInput, options, ctx = {}) {
|
|
|
2761
3521
|
async function doRunTaskShow(idInput, options, ctx) {
|
|
2762
3522
|
const cwd = ctx.cwd ?? process.cwd();
|
|
2763
3523
|
const repositoryRoot = await resolveRepositoryRootForTask(cwd, "show");
|
|
2764
|
-
const paths =
|
|
2765
|
-
await
|
|
3524
|
+
const paths = basouPaths12(repositoryRoot);
|
|
3525
|
+
await assertWorkspaceInitialized9(paths.root);
|
|
2766
3526
|
const taskId = await resolveTaskId2(paths, idInput, { includeArchived: true });
|
|
2767
3527
|
const { doc, archived } = await readTaskFileWithArchiveFallback(paths, taskId);
|
|
2768
3528
|
const sessions = await loadSessionEntries2(paths, { now: /* @__PURE__ */ new Date() });
|
|
2769
3529
|
const events = [];
|
|
2770
3530
|
const linkedSessionIds = new Set(doc.task.task.linked_sessions);
|
|
2771
3531
|
for (const s of sessions) {
|
|
2772
|
-
const sessionDir =
|
|
3532
|
+
const sessionDir = join6(paths.sessions, s.sessionId);
|
|
2773
3533
|
try {
|
|
2774
3534
|
for await (const ev of replayEvents2(sessionDir, {
|
|
2775
3535
|
onWarning: (w) => printReplayWarning(w, s.sessionId)
|
|
@@ -2905,8 +3665,8 @@ async function doRunTaskStatus(taskIdInput, newStatusInput, options, ctx) {
|
|
|
2905
3665
|
const newStatus = parseTaskStatusPositional(newStatusInput);
|
|
2906
3666
|
const cwd = ctx.cwd ?? process.cwd();
|
|
2907
3667
|
const repositoryRoot = await resolveRepositoryRootForTask(cwd, "status");
|
|
2908
|
-
const paths =
|
|
2909
|
-
await
|
|
3668
|
+
const paths = basouPaths12(repositoryRoot);
|
|
3669
|
+
await assertWorkspaceInitialized9(paths.root);
|
|
2910
3670
|
const taskId = await resolveTaskId2(paths, taskIdInput);
|
|
2911
3671
|
const now = ctx.nowProvider !== void 0 ? ctx.nowProvider() : /* @__PURE__ */ new Date();
|
|
2912
3672
|
const occurredAt = now.toISOString();
|
|
@@ -2931,7 +3691,7 @@ async function doRunTaskStatus(taskIdInput, newStatusInput, options, ctx) {
|
|
|
2931
3691
|
});
|
|
2932
3692
|
return;
|
|
2933
3693
|
}
|
|
2934
|
-
const manifest = await
|
|
3694
|
+
const manifest = await readManifest7(paths);
|
|
2935
3695
|
const result = await updateTaskStatusWithEvent({
|
|
2936
3696
|
mode: "ad-hoc",
|
|
2937
3697
|
paths,
|
|
@@ -2982,9 +3742,9 @@ async function runTaskReconcile(options, ctx = {}) {
|
|
|
2982
3742
|
async function doRunTaskReconcile(options, ctx) {
|
|
2983
3743
|
const cwd = ctx.cwd ?? process.cwd();
|
|
2984
3744
|
const repositoryRoot = await resolveRepositoryRootForTask(cwd, "reconcile");
|
|
2985
|
-
const paths =
|
|
2986
|
-
await
|
|
2987
|
-
const manifest = await
|
|
3745
|
+
const paths = basouPaths12(repositoryRoot);
|
|
3746
|
+
await assertWorkspaceInitialized9(paths.root);
|
|
3747
|
+
const manifest = await readManifest7(paths);
|
|
2988
3748
|
const nowProvider = ctx.nowProvider ?? (() => /* @__PURE__ */ new Date());
|
|
2989
3749
|
const write = options.write === true;
|
|
2990
3750
|
const verbose = isVerbose(options);
|
|
@@ -3162,9 +3922,9 @@ async function doRunTaskRefreshLinkage(taskIdInput, options, ctx) {
|
|
|
3162
3922
|
}
|
|
3163
3923
|
const cwd = ctx.cwd ?? process.cwd();
|
|
3164
3924
|
const repositoryRoot = await resolveRepositoryRootForTask(cwd, "refresh-linkage");
|
|
3165
|
-
const paths =
|
|
3166
|
-
await
|
|
3167
|
-
const manifest = await
|
|
3925
|
+
const paths = basouPaths12(repositoryRoot);
|
|
3926
|
+
await assertWorkspaceInitialized9(paths.root);
|
|
3927
|
+
const manifest = await readManifest7(paths);
|
|
3168
3928
|
const taskId = await resolveTaskId2(paths, taskIdInput);
|
|
3169
3929
|
const nowProvider = ctx.nowProvider ?? (() => /* @__PURE__ */ new Date());
|
|
3170
3930
|
const write = options.write === true;
|
|
@@ -3242,9 +4002,9 @@ async function doRunTaskEdit(taskIdInput, options, ctx) {
|
|
|
3242
4002
|
}
|
|
3243
4003
|
const cwd = ctx.cwd ?? process.cwd();
|
|
3244
4004
|
const repositoryRoot = await resolveRepositoryRootForTask(cwd, "edit");
|
|
3245
|
-
const paths =
|
|
3246
|
-
await
|
|
3247
|
-
const manifest = await
|
|
4005
|
+
const paths = basouPaths12(repositoryRoot);
|
|
4006
|
+
await assertWorkspaceInitialized9(paths.root);
|
|
4007
|
+
const manifest = await readManifest7(paths);
|
|
3248
4008
|
const taskId = await resolveTaskId2(paths, taskIdInput);
|
|
3249
4009
|
const now = ctx.nowProvider !== void 0 ? ctx.nowProvider() : /* @__PURE__ */ new Date();
|
|
3250
4010
|
const occurredAt = now.toISOString();
|
|
@@ -3298,9 +4058,9 @@ async function doRunTaskDelete(taskIdInput, options, ctx) {
|
|
|
3298
4058
|
}
|
|
3299
4059
|
const cwd = ctx.cwd ?? process.cwd();
|
|
3300
4060
|
const repositoryRoot = await resolveRepositoryRootForTask(cwd, "delete");
|
|
3301
|
-
const paths =
|
|
3302
|
-
await
|
|
3303
|
-
const manifest = await
|
|
4061
|
+
const paths = basouPaths12(repositoryRoot);
|
|
4062
|
+
await assertWorkspaceInitialized9(paths.root);
|
|
4063
|
+
const manifest = await readManifest7(paths);
|
|
3304
4064
|
const taskId = await resolveTaskId2(paths, taskIdInput);
|
|
3305
4065
|
if (options.yes !== true) {
|
|
3306
4066
|
await confirmDestructiveAction("delete", taskId);
|
|
@@ -3343,9 +4103,9 @@ async function doRunTaskArchive(taskIdInput, options, ctx) {
|
|
|
3343
4103
|
}
|
|
3344
4104
|
const cwd = ctx.cwd ?? process.cwd();
|
|
3345
4105
|
const repositoryRoot = await resolveRepositoryRootForTask(cwd, "archive");
|
|
3346
|
-
const paths =
|
|
3347
|
-
await
|
|
3348
|
-
const manifest = await
|
|
4106
|
+
const paths = basouPaths12(repositoryRoot);
|
|
4107
|
+
await assertWorkspaceInitialized9(paths.root);
|
|
4108
|
+
const manifest = await readManifest7(paths);
|
|
3349
4109
|
const taskId = await resolveTaskId2(paths, taskIdInput);
|
|
3350
4110
|
if (options.yes !== true) {
|
|
3351
4111
|
await confirmDestructiveAction("archive", taskId);
|
|
@@ -3387,8 +4147,8 @@ async function confirmDestructiveAction(action, taskId) {
|
|
|
3387
4147
|
}
|
|
3388
4148
|
}
|
|
3389
4149
|
async function readSingleLineFromStdin() {
|
|
3390
|
-
const { createInterface } = await import("readline/promises");
|
|
3391
|
-
const rl =
|
|
4150
|
+
const { createInterface: createInterface2 } = await import("readline/promises");
|
|
4151
|
+
const rl = createInterface2({ input: process.stdin, output: process.stdout });
|
|
3392
4152
|
try {
|
|
3393
4153
|
const line = await rl.question("");
|
|
3394
4154
|
return line;
|
|
@@ -3457,12 +4217,12 @@ function parsePositiveInt2(raw) {
|
|
|
3457
4217
|
}
|
|
3458
4218
|
async function readDescriptionFile(path) {
|
|
3459
4219
|
try {
|
|
3460
|
-
return await
|
|
4220
|
+
return await readFile3(path, "utf8");
|
|
3461
4221
|
} catch (error) {
|
|
3462
|
-
if (
|
|
4222
|
+
if (findErrorCode10(error, "ENOENT")) {
|
|
3463
4223
|
throw new Error("Description source not found", { cause: error });
|
|
3464
4224
|
}
|
|
3465
|
-
if (
|
|
4225
|
+
if (findErrorCode10(error, "EISDIR")) {
|
|
3466
4226
|
throw new Error("Description source is not a file", { cause: error });
|
|
3467
4227
|
}
|
|
3468
4228
|
throw new Error("Failed to read description source", { cause: error });
|
|
@@ -3470,7 +4230,7 @@ async function readDescriptionFile(path) {
|
|
|
3470
4230
|
}
|
|
3471
4231
|
async function resolveRepositoryRootForTask(cwd, subcmd) {
|
|
3472
4232
|
try {
|
|
3473
|
-
return await
|
|
4233
|
+
return await resolveRepositoryRoot13(cwd);
|
|
3474
4234
|
} catch (error) {
|
|
3475
4235
|
if (error instanceof Error && error.message === "Not a git repository") {
|
|
3476
4236
|
throw new Error(
|
|
@@ -3481,11 +4241,11 @@ async function resolveRepositoryRootForTask(cwd, subcmd) {
|
|
|
3481
4241
|
throw error;
|
|
3482
4242
|
}
|
|
3483
4243
|
}
|
|
3484
|
-
async function
|
|
4244
|
+
async function assertWorkspaceInitialized9(basouRoot) {
|
|
3485
4245
|
try {
|
|
3486
|
-
await
|
|
4246
|
+
await assertBasouRootSafe12(basouRoot);
|
|
3487
4247
|
} catch (error) {
|
|
3488
|
-
if (
|
|
4248
|
+
if (findErrorCode10(error, "ENOENT")) {
|
|
3489
4249
|
throw new Error("Workspace not initialized. Run 'basou init' first.");
|
|
3490
4250
|
}
|
|
3491
4251
|
throw error;
|
|
@@ -3571,22 +4331,956 @@ function maxLen3(values, floor) {
|
|
|
3571
4331
|
return max;
|
|
3572
4332
|
}
|
|
3573
4333
|
|
|
3574
|
-
// src/
|
|
4334
|
+
// src/commands/view.ts
|
|
4335
|
+
import { spawn } from "child_process";
|
|
4336
|
+
import { assertBasouRootSafe as assertBasouRootSafe13, basouPaths as basouPaths13, findErrorCode as findErrorCode12, resolveRepositoryRoot as resolveRepositoryRoot14 } from "@basou/core";
|
|
4337
|
+
import { InvalidArgumentError as InvalidArgumentError4 } from "commander";
|
|
4338
|
+
|
|
4339
|
+
// src/lib/view-server.ts
|
|
4340
|
+
import { createServer } from "http";
|
|
4341
|
+
import { join as join7 } from "path";
|
|
4342
|
+
import {
|
|
4343
|
+
computeWorkStats as computeWorkStats2,
|
|
4344
|
+
enumerateApprovals as enumerateApprovals2,
|
|
4345
|
+
findErrorCode as findErrorCode11,
|
|
4346
|
+
isLazyExpired as isLazyExpired2,
|
|
4347
|
+
loadApproval as loadApproval2,
|
|
4348
|
+
loadSessionEntries as loadSessionEntries3,
|
|
4349
|
+
loadTaskEntries as loadTaskEntries2,
|
|
4350
|
+
readAllEvents as readAllEvents2,
|
|
4351
|
+
readManifest as readManifest8,
|
|
4352
|
+
readMarkdownFile as readMarkdownFile4,
|
|
4353
|
+
readSessionYaml as readSessionYaml2,
|
|
4354
|
+
readTaskFile as readTaskFile2,
|
|
4355
|
+
renderDecisions as renderDecisions3,
|
|
4356
|
+
renderHandoff as renderHandoff3
|
|
4357
|
+
} from "@basou/core";
|
|
4358
|
+
|
|
4359
|
+
// src/lib/view-ui.ts
|
|
4360
|
+
var VIEW_HTML = `<!doctype html>
|
|
4361
|
+
<html lang="en">
|
|
4362
|
+
<head>
|
|
4363
|
+
<meta charset="utf-8" />
|
|
4364
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
4365
|
+
<title>basou view</title>
|
|
4366
|
+
<style>
|
|
4367
|
+
:root { color-scheme: light dark; }
|
|
4368
|
+
* { box-sizing: border-box; }
|
|
4369
|
+
body { margin: 0; font: 14px/1.5 system-ui, -apple-system, Segoe UI, sans-serif; }
|
|
4370
|
+
header { padding: 10px 16px; border-bottom: 1px solid #8884; display: flex; flex-wrap: wrap; gap: 8px 12px; align-items: center; }
|
|
4371
|
+
header h1 { font-size: 15px; margin: 0 12px 0 0; font-weight: 700; }
|
|
4372
|
+
header .grow { flex: 1; }
|
|
4373
|
+
input[type=text] { padding: 4px 8px; border: 1px solid #8886; border-radius: 6px; min-width: 280px; font: inherit; }
|
|
4374
|
+
button { padding: 4px 10px; border: 1px solid #8886; border-radius: 6px; background: #8881; cursor: pointer; font: inherit; }
|
|
4375
|
+
button.primary { background: #2563eb; color: #fff; border-color: #2563eb; }
|
|
4376
|
+
button:disabled { opacity: .5; cursor: default; }
|
|
4377
|
+
label.chk { font-size: 13px; opacity: .85; }
|
|
4378
|
+
#status { padding: 6px 16px; font-size: 13px; min-height: 20px; border-bottom: 1px solid #8884; white-space: pre-wrap; }
|
|
4379
|
+
#status.err { color: #dc2626; }
|
|
4380
|
+
nav { display: flex; gap: 2px; padding: 6px 12px; border-bottom: 1px solid #8884; flex-wrap: wrap; }
|
|
4381
|
+
nav button { border: none; border-radius: 6px; background: transparent; }
|
|
4382
|
+
nav button.active { background: #2563eb22; font-weight: 600; }
|
|
4383
|
+
main { display: grid; grid-template-columns: minmax(220px, 320px) 1fr; min-height: 60vh; }
|
|
4384
|
+
main.single { grid-template-columns: 1fr; }
|
|
4385
|
+
#list { border-right: 1px solid #8884; overflow: auto; max-height: 80vh; }
|
|
4386
|
+
#list .row { padding: 8px 12px; border-bottom: 1px solid #8883; cursor: pointer; }
|
|
4387
|
+
#list .row:hover { background: #8881; }
|
|
4388
|
+
#list .row.active { background: #2563eb22; }
|
|
4389
|
+
#list .row .meta { font-size: 12px; opacity: .7; }
|
|
4390
|
+
#detail { padding: 12px 16px; overflow: auto; max-height: 80vh; }
|
|
4391
|
+
.badge { display: inline-block; padding: 0 6px; border-radius: 6px; background: #8882; font-size: 12px; }
|
|
4392
|
+
.badge.warn { background: #f59e0b33; }
|
|
4393
|
+
pre { background: #8881; padding: 12px; border-radius: 8px; overflow: auto; white-space: pre-wrap; word-break: break-word; }
|
|
4394
|
+
table.kv { border-collapse: collapse; }
|
|
4395
|
+
table.kv td { padding: 2px 10px 2px 0; vertical-align: top; }
|
|
4396
|
+
table.kv td.k { opacity: .7; }
|
|
4397
|
+
.cards { display: flex; flex-wrap: wrap; gap: 10px; }
|
|
4398
|
+
.card { border: 1px solid #8884; border-radius: 8px; padding: 10px 14px; min-width: 120px; }
|
|
4399
|
+
.card .n { font-size: 22px; font-weight: 700; }
|
|
4400
|
+
.card .l { font-size: 12px; opacity: .7; }
|
|
4401
|
+
.tl { border-left: 2px solid #8885; margin-left: 6px; padding-left: 12px; }
|
|
4402
|
+
.tl .ev { margin-bottom: 8px; }
|
|
4403
|
+
.tl .ev .t { font-size: 12px; opacity: .65; }
|
|
4404
|
+
.muted { opacity: .6; }
|
|
4405
|
+
</style>
|
|
4406
|
+
</head>
|
|
4407
|
+
<body>
|
|
4408
|
+
<header>
|
|
4409
|
+
<h1>basou view</h1>
|
|
4410
|
+
<input type="text" id="project" placeholder="project path" />
|
|
4411
|
+
<button class="primary" id="btn-refresh">Refresh all</button>
|
|
4412
|
+
<button id="btn-import-claude">Import claude-code</button>
|
|
4413
|
+
<button id="btn-import-codex">Import codex</button>
|
|
4414
|
+
<button id="btn-gen-handoff">Regenerate handoff</button>
|
|
4415
|
+
<button id="btn-gen-decisions">Regenerate decisions</button>
|
|
4416
|
+
<span class="grow"></span>
|
|
4417
|
+
<label class="chk"><input type="checkbox" id="opt-force" /> force</label>
|
|
4418
|
+
<label class="chk"><input type="checkbox" id="opt-dry" /> dry-run</label>
|
|
4419
|
+
</header>
|
|
4420
|
+
<div id="status"></div>
|
|
4421
|
+
<nav id="tabs"></nav>
|
|
4422
|
+
<main id="main">
|
|
4423
|
+
<div id="list"></div>
|
|
4424
|
+
<div id="detail"></div>
|
|
4425
|
+
</main>
|
|
4426
|
+
<script>
|
|
4427
|
+
(function () {
|
|
4428
|
+
var TABS = ['overview', 'stats', 'sessions', 'tasks', 'decisions', 'approvals', 'handoff'];
|
|
4429
|
+
var state = { tab: 'overview', repoRoot: '' };
|
|
4430
|
+
|
|
4431
|
+
function $(id) { return document.getElementById(id); }
|
|
4432
|
+
function clear(node) { while (node.firstChild) node.removeChild(node.firstChild); }
|
|
4433
|
+
|
|
4434
|
+
function el(tag, attrs, children) {
|
|
4435
|
+
var node = document.createElement(tag);
|
|
4436
|
+
if (attrs) {
|
|
4437
|
+
for (var k in attrs) {
|
|
4438
|
+
if (!Object.prototype.hasOwnProperty.call(attrs, k)) continue;
|
|
4439
|
+
if (k === 'class') node.className = attrs[k];
|
|
4440
|
+
else if (k === 'text') node.textContent = attrs[k];
|
|
4441
|
+
else if (k.slice(0, 2) === 'on') node.addEventListener(k.slice(2), attrs[k]);
|
|
4442
|
+
else node.setAttribute(k, attrs[k]);
|
|
4443
|
+
}
|
|
4444
|
+
}
|
|
4445
|
+
if (children) {
|
|
4446
|
+
for (var i = 0; i < children.length; i++) {
|
|
4447
|
+
var c = children[i];
|
|
4448
|
+
if (c === null || c === undefined) continue;
|
|
4449
|
+
node.appendChild(typeof c === 'string' ? document.createTextNode(c) : c);
|
|
4450
|
+
}
|
|
4451
|
+
}
|
|
4452
|
+
return node;
|
|
4453
|
+
}
|
|
4454
|
+
|
|
4455
|
+
function setStatus(msg, isErr) {
|
|
4456
|
+
var s = $('status');
|
|
4457
|
+
s.textContent = msg || '';
|
|
4458
|
+
s.className = isErr ? 'err' : '';
|
|
4459
|
+
}
|
|
4460
|
+
|
|
4461
|
+
function fetchJson(path, opts) {
|
|
4462
|
+
return fetch(path, opts).then(function (res) {
|
|
4463
|
+
return res.text().then(function (text) {
|
|
4464
|
+
var data = null;
|
|
4465
|
+
try { data = text ? JSON.parse(text) : null; } catch (e) { data = null; }
|
|
4466
|
+
if (!res.ok) {
|
|
4467
|
+
var m = data && data.error ? data.error : ('HTTP ' + res.status);
|
|
4468
|
+
throw new Error(m);
|
|
4469
|
+
}
|
|
4470
|
+
return data;
|
|
4471
|
+
});
|
|
4472
|
+
});
|
|
4473
|
+
}
|
|
4474
|
+
|
|
4475
|
+
function single(on) { $('main').className = on ? 'single' : ''; if (on) clear($('list')); }
|
|
4476
|
+
|
|
4477
|
+
// --- action bar ---------------------------------------------------------
|
|
4478
|
+
|
|
4479
|
+
function actionBody() {
|
|
4480
|
+
var body = {};
|
|
4481
|
+
var project = $('project').value.trim();
|
|
4482
|
+
if (project) body.project = project;
|
|
4483
|
+
if ($('opt-force').checked) body.force = true;
|
|
4484
|
+
if ($('opt-dry').checked) body.dryRun = true;
|
|
4485
|
+
return body;
|
|
4486
|
+
}
|
|
4487
|
+
|
|
4488
|
+
function setBusy(busy) {
|
|
4489
|
+
var ids = ['btn-refresh', 'btn-import-claude', 'btn-import-codex', 'btn-gen-handoff', 'btn-gen-decisions'];
|
|
4490
|
+
for (var i = 0; i < ids.length; i++) $(ids[i]).disabled = busy;
|
|
4491
|
+
}
|
|
4492
|
+
|
|
4493
|
+
function post(path, label) {
|
|
4494
|
+
setBusy(true);
|
|
4495
|
+
setStatus(label + '...', false);
|
|
4496
|
+
fetchJson(path, {
|
|
4497
|
+
method: 'POST',
|
|
4498
|
+
headers: { 'Content-Type': 'application/json' },
|
|
4499
|
+
body: JSON.stringify(actionBody())
|
|
4500
|
+
}).then(function (data) {
|
|
4501
|
+
setStatus(label + ' done: ' + summarize(data), false);
|
|
4502
|
+
loadTab(state.tab);
|
|
4503
|
+
}).catch(function (err) {
|
|
4504
|
+
setStatus(label + ' failed: ' + err.message, true);
|
|
4505
|
+
}).then(function () { setBusy(false); });
|
|
4506
|
+
}
|
|
4507
|
+
|
|
4508
|
+
function summarize(data) {
|
|
4509
|
+
if (!data) return 'ok';
|
|
4510
|
+
if (data.claudeCode || data.codex) {
|
|
4511
|
+
return 'claude-code ' + imp(data.claudeCode) + ', codex ' + imp(data.codex)
|
|
4512
|
+
+ (data.handoff && data.handoff.status === 'generated' ? '; handoff+decisions regenerated' : '');
|
|
4513
|
+
}
|
|
4514
|
+
if (data.status === 'ran') return imp(data);
|
|
4515
|
+
if (data.status === 'skipped') return 'skipped (' + data.reason + ')';
|
|
4516
|
+
if (typeof data.sessionCount === 'number') return 'sessions ' + data.sessionCount + ', decisions ' + data.decisionCount;
|
|
4517
|
+
if (typeof data.decisionCount === 'number') return 'decisions ' + data.decisionCount;
|
|
4518
|
+
return 'ok';
|
|
4519
|
+
}
|
|
4520
|
+
function imp(o) {
|
|
4521
|
+
if (!o) return '-';
|
|
4522
|
+
if (o.status === 'skipped') return 'skipped';
|
|
4523
|
+
return (o.dryRun ? 'would import ' : 'imported ') + o.importedCount + ' (' + o.eventTotal + ' events)';
|
|
4524
|
+
}
|
|
4525
|
+
|
|
4526
|
+
// --- tabs ---------------------------------------------------------------
|
|
4527
|
+
|
|
4528
|
+
function buildTabs() {
|
|
4529
|
+
var nav = $('tabs');
|
|
4530
|
+
clear(nav);
|
|
4531
|
+
TABS.forEach(function (name) {
|
|
4532
|
+
nav.appendChild(el('button', {
|
|
4533
|
+
class: name === state.tab ? 'active' : '',
|
|
4534
|
+
text: name,
|
|
4535
|
+
onclick: function () { loadTab(name); }
|
|
4536
|
+
}));
|
|
4537
|
+
});
|
|
4538
|
+
}
|
|
4539
|
+
|
|
4540
|
+
function loadTab(name) {
|
|
4541
|
+
state.tab = name;
|
|
4542
|
+
buildTabs();
|
|
4543
|
+
clear($('detail'));
|
|
4544
|
+
clear($('list'));
|
|
4545
|
+
if (name === 'overview') return loadOverview();
|
|
4546
|
+
if (name === 'stats') return loadStats();
|
|
4547
|
+
if (name === 'sessions') return loadSessions();
|
|
4548
|
+
if (name === 'tasks') return loadTasks();
|
|
4549
|
+
if (name === 'decisions') return loadMarkdown('/api/decisions', 'decisions');
|
|
4550
|
+
if (name === 'approvals') return loadApprovals();
|
|
4551
|
+
if (name === 'handoff') return loadMarkdown('/api/handoff', 'handoff');
|
|
4552
|
+
}
|
|
4553
|
+
|
|
4554
|
+
function fail(err) { setStatus(err.message, true); }
|
|
4555
|
+
|
|
4556
|
+
function loadOverview() {
|
|
4557
|
+
single(true);
|
|
4558
|
+
fetchJson('/api/overview').then(function (d) {
|
|
4559
|
+
var detail = $('detail');
|
|
4560
|
+
if (!d || d.initialized === false) {
|
|
4561
|
+
detail.appendChild(el('p', { class: 'muted', text: 'Workspace not initialized.' }));
|
|
4562
|
+
return;
|
|
4563
|
+
}
|
|
4564
|
+
$('project').value = $('project').value || d.repoRoot || '';
|
|
4565
|
+
state.repoRoot = d.repoRoot || '';
|
|
4566
|
+
detail.appendChild(el('p', {}, [
|
|
4567
|
+
el('strong', { text: d.workspace.name }), ' ',
|
|
4568
|
+
el('span', { class: 'muted', text: d.workspace.id })
|
|
4569
|
+
]));
|
|
4570
|
+
var c = d.counts;
|
|
4571
|
+
var cards = el('div', { class: 'cards' }, [
|
|
4572
|
+
card(c.sessions, 'sessions'),
|
|
4573
|
+
card(c.suspectSessions, 'suspect'),
|
|
4574
|
+
card(c.tasks, 'tasks'),
|
|
4575
|
+
card(c.pendingTasks, 'pending tasks'),
|
|
4576
|
+
card(c.decisions, 'decisions'),
|
|
4577
|
+
card(c.approvalsPending, 'approvals pending')
|
|
4578
|
+
]);
|
|
4579
|
+
detail.appendChild(cards);
|
|
4580
|
+
detail.appendChild(el('p', { class: 'muted', text: 'repo: ' + d.repoRoot }));
|
|
4581
|
+
}).catch(fail);
|
|
4582
|
+
}
|
|
4583
|
+
function card(n, label) {
|
|
4584
|
+
return el('div', { class: 'card' }, [
|
|
4585
|
+
el('div', { class: 'n', text: String(n) }),
|
|
4586
|
+
el('div', { class: 'l', text: label })
|
|
4587
|
+
]);
|
|
4588
|
+
}
|
|
4589
|
+
|
|
4590
|
+
function numfmt(n) { return (n || 0).toLocaleString('en-US'); }
|
|
4591
|
+
function fmtDur(ms) {
|
|
4592
|
+
var s = Math.round((ms || 0) / 1000);
|
|
4593
|
+
var h = Math.floor(s / 3600), m = Math.floor((s % 3600) / 60), sec = s % 60;
|
|
4594
|
+
if (h > 0) return h + 'h ' + (m < 10 ? '0' : '') + m + 'm';
|
|
4595
|
+
if (m > 0) return m + 'm ' + (sec < 10 ? '0' : '') + sec + 's';
|
|
4596
|
+
return sec + 's';
|
|
4597
|
+
}
|
|
4598
|
+
function kvrow(k, v) {
|
|
4599
|
+
return el('tr', {}, [el('td', { class: 'k', text: k }), el('td', { text: v })]);
|
|
4600
|
+
}
|
|
4601
|
+
|
|
4602
|
+
function loadStats() {
|
|
4603
|
+
single(true);
|
|
4604
|
+
fetchJson('/api/stats').then(function (d) {
|
|
4605
|
+
var detail = $('detail');
|
|
4606
|
+
var t = d.totals;
|
|
4607
|
+
detail.appendChild(el('p', { text: 'Sessions: ' + t.sessionCount }));
|
|
4608
|
+
detail.appendChild(el('h3', { text: 'Volume (what the AI produced)' }));
|
|
4609
|
+
detail.appendChild(el('div', { class: 'cards' }, [
|
|
4610
|
+
card(numfmt(t.tokens.output), 'output tokens'),
|
|
4611
|
+
(t.tokens.reasoning > 0 ? card(numfmt(t.tokens.reasoning), 'reasoning tokens') : null),
|
|
4612
|
+
card(t.commandCount, 'commands'),
|
|
4613
|
+
card(t.fileChangedCount, 'files'),
|
|
4614
|
+
card(t.decisionCount, 'decisions')
|
|
4615
|
+
]));
|
|
4616
|
+
var sessions = d.sessions || [];
|
|
4617
|
+
var tokenSessions = sessions.filter(function (s) { return s.availability && s.availability.tokens; }).length;
|
|
4618
|
+
if (!t.tokensAvailable) {
|
|
4619
|
+
detail.appendChild(el('p', { class: 'muted', text: 'No token data captured; re-import to backfill.' }));
|
|
4620
|
+
} else if (tokenSessions < t.sessionCount) {
|
|
4621
|
+
detail.appendChild(el('p', { class: 'muted', text: 'Token data on ' + tokenSessions + ' of ' + t.sessionCount + ' sessions; re-import to backfill the rest.' }));
|
|
4622
|
+
}
|
|
4623
|
+
var degraded = sessions.filter(function (s) { return s.eventsUnreadable; }).length;
|
|
4624
|
+
if (degraded > 0) {
|
|
4625
|
+
detail.appendChild(el('p', { class: 'muted', text: degraded + ' session(s) had unreadable event logs; their counts are incomplete.' }));
|
|
4626
|
+
}
|
|
4627
|
+
detail.appendChild(el('h3', { text: 'Time (human harness labor; active = billing primary)' }));
|
|
4628
|
+
var turnSessions = sessions.filter(function (s) { return s.activeTimeBasis === 'engaged-turns'; }).length;
|
|
4629
|
+
var basisNote = turnSessions === t.sessionCount ? 'engaged turns' : (turnSessions === 0 ? 'event stream; re-import to capture conversation' : 'engaged turns on ' + turnSessions + ' of ' + t.sessionCount + ' sessions');
|
|
4630
|
+
var timeRows = [kvrow('billable active', fmtDur(t.billableActiveTimeMs) + ' (union; ' + basisNote + '; idle gaps > 5m excluded; tz ' + d.timeZone + ')')];
|
|
4631
|
+
if (t.activeTimeMs !== t.billableActiveTimeMs) {
|
|
4632
|
+
timeRows.push(kvrow('summed', fmtDur(t.activeTimeMs) + ' (concurrent sessions double-counted)'));
|
|
4633
|
+
}
|
|
4634
|
+
if (t.machineActiveAvailable) {
|
|
4635
|
+
var machineSessions = sessions.filter(function (s) { return s.availability && s.availability.machineActive; }).length;
|
|
4636
|
+
timeRows.push(kvrow('model working', fmtDur(t.machineActiveTimeMs) + ' (model compute, subset of active; Codex turn duration on ' + machineSessions + ' of ' + t.sessionCount + ' sessions; not wall-clock-deduped)'));
|
|
4637
|
+
}
|
|
4638
|
+
timeRows.push(kvrow('span', fmtDur(t.sessionSpanMs) + (t.openSessionCount > 0 ? ' (' + t.openSessionCount + ' open)' : '')));
|
|
4639
|
+
timeRows.push(kvrow('command', fmtDur(t.commandTimeMs) + (t.commandTimeReliable ? '' : ' (some sessions report 0)')));
|
|
4640
|
+
detail.appendChild(el('table', { class: 'kv' }, [el('tbody', {}, timeRows)]));
|
|
4641
|
+
if (d.bySource && d.bySource.length) {
|
|
4642
|
+
detail.appendChild(el('h3', { text: 'By source' }));
|
|
4643
|
+
d.bySource.forEach(function (s) {
|
|
4644
|
+
var cmd = s.commandTimeReliable ? fmtDur(s.commandTimeMs) : 'n/a';
|
|
4645
|
+
var machine = s.machineActiveAvailable ? ', model ' + fmtDur(s.machineActiveTimeMs) : '';
|
|
4646
|
+
detail.appendChild(el('div', { class: 'row' }, [
|
|
4647
|
+
el('span', { text: s.sourceKind + ': ' + s.sessionCount + ' sessions, ' + numfmt(s.tokens.output) + ' out tok, active ' + fmtDur(s.activeTimeMs) + machine + ', command ' + cmd })
|
|
4648
|
+
]));
|
|
4649
|
+
});
|
|
4650
|
+
}
|
|
4651
|
+
if (d.byDay && d.byDay.length) {
|
|
4652
|
+
detail.appendChild(el('h3', { text: 'By day (billable time x volume)' }));
|
|
4653
|
+
d.byDay.forEach(function (day) {
|
|
4654
|
+
var dayMachine = day.machineActiveTimeMs > 0 ? ' (model ' + fmtDur(day.machineActiveTimeMs) + ')' : '';
|
|
4655
|
+
detail.appendChild(el('div', { class: 'row' }, [
|
|
4656
|
+
el('span', { text: day.date + ': ' + fmtDur(day.billableActiveTimeMs) + ' active' + dayMachine + ', ' + numfmt(day.tokens.output) + ' out tok, ' + day.commandCount + ' cmd / ' + day.fileChangedCount + ' files / ' + day.decisionCount + ' dec' })
|
|
4657
|
+
]));
|
|
4658
|
+
});
|
|
4659
|
+
}
|
|
4660
|
+
}).catch(fail);
|
|
4661
|
+
}
|
|
4662
|
+
|
|
4663
|
+
function loadSessions() {
|
|
4664
|
+
single(false);
|
|
4665
|
+
fetchJson('/api/sessions').then(function (d) {
|
|
4666
|
+
var list = $('list');
|
|
4667
|
+
var rows = (d && d.sessions) || [];
|
|
4668
|
+
if (rows.length === 0) { list.appendChild(el('div', { class: 'row muted', text: 'no sessions' })); return; }
|
|
4669
|
+
rows.forEach(function (s) {
|
|
4670
|
+
var row = el('div', { class: 'row', onclick: function () { selectSession(row, s.sessionId); } }, [
|
|
4671
|
+
el('div', { text: s.label || s.sessionId }),
|
|
4672
|
+
el('div', { class: 'meta', text: s.sourceKind + ' ' + s.status + (s.suspect ? ' suspect' : '') })
|
|
4673
|
+
]);
|
|
4674
|
+
list.appendChild(row);
|
|
4675
|
+
});
|
|
4676
|
+
}).catch(fail);
|
|
4677
|
+
}
|
|
4678
|
+
function selectSession(row, id) {
|
|
4679
|
+
var rows = $('list').querySelectorAll('.row');
|
|
4680
|
+
for (var i = 0; i < rows.length; i++) rows[i].classList.remove('active');
|
|
4681
|
+
row.classList.add('active');
|
|
4682
|
+
var detail = $('detail');
|
|
4683
|
+
clear(detail);
|
|
4684
|
+
fetchJson('/api/sessions/' + encodeURIComponent(id)).then(function (d) {
|
|
4685
|
+
var s = d.session.session;
|
|
4686
|
+
detail.appendChild(el('h3', { text: s.label || id }));
|
|
4687
|
+
detail.appendChild(kv([
|
|
4688
|
+
['status', s.status], ['source', s.source.kind], ['started', s.started_at],
|
|
4689
|
+
['ended', s.ended_at || '-'], ['workdir', s.working_directory]
|
|
4690
|
+
]));
|
|
4691
|
+
if (d.degraded) detail.appendChild(el('p', { class: 'badge warn', text: 'events unreadable' }));
|
|
4692
|
+
var events = d.events || [];
|
|
4693
|
+
detail.appendChild(el('p', { class: 'muted', text: events.length + ' events' }));
|
|
4694
|
+
var tl = el('div', { class: 'tl' }, []);
|
|
4695
|
+
events.forEach(function (ev) {
|
|
4696
|
+
tl.appendChild(el('div', { class: 'ev' }, [
|
|
4697
|
+
el('div', { class: 't', text: ev.occurred_at + ' ' + ev.type }),
|
|
4698
|
+
el('div', { text: eventSummary(ev) })
|
|
4699
|
+
]));
|
|
4700
|
+
});
|
|
4701
|
+
detail.appendChild(tl);
|
|
4702
|
+
}).catch(fail);
|
|
4703
|
+
}
|
|
4704
|
+
function eventSummary(ev) {
|
|
4705
|
+
if (ev.type === 'command_executed') {
|
|
4706
|
+
var cmd = (ev.args && ev.args.length) ? ev.args.join(' ') : ev.command;
|
|
4707
|
+
var ex = (ev.exit_code === null || ev.exit_code === undefined) ? '' : ' (exit ' + ev.exit_code + ')';
|
|
4708
|
+
return cmd + ex;
|
|
4709
|
+
}
|
|
4710
|
+
if (ev.type === 'file_changed') return ev.path + ' [' + ev.change_type + ']';
|
|
4711
|
+
if (ev.type === 'decision_recorded') return ev.title || '';
|
|
4712
|
+
return '';
|
|
4713
|
+
}
|
|
4714
|
+
|
|
4715
|
+
function loadTasks() {
|
|
4716
|
+
single(false);
|
|
4717
|
+
fetchJson('/api/tasks').then(function (d) {
|
|
4718
|
+
var list = $('list');
|
|
4719
|
+
var rows = (d && d.tasks) || [];
|
|
4720
|
+
if (rows.length === 0) { list.appendChild(el('div', { class: 'row muted', text: 'no tasks' })); return; }
|
|
4721
|
+
rows.forEach(function (t) {
|
|
4722
|
+
var row = el('div', { class: 'row', onclick: function () { selectTask(row, t.id); } }, [
|
|
4723
|
+
el('div', { text: t.title || t.label || t.id }),
|
|
4724
|
+
el('div', { class: 'meta', text: String(t.status || '') })
|
|
4725
|
+
]);
|
|
4726
|
+
list.appendChild(row);
|
|
4727
|
+
});
|
|
4728
|
+
}).catch(fail);
|
|
4729
|
+
}
|
|
4730
|
+
function selectTask(row, id) {
|
|
4731
|
+
var rows = $('list').querySelectorAll('.row');
|
|
4732
|
+
for (var i = 0; i < rows.length; i++) rows[i].classList.remove('active');
|
|
4733
|
+
row.classList.add('active');
|
|
4734
|
+
var detail = $('detail');
|
|
4735
|
+
clear(detail);
|
|
4736
|
+
fetchJson('/api/tasks/' + encodeURIComponent(id)).then(function (d) {
|
|
4737
|
+
detail.appendChild(el('h3', { text: (d.task && (d.task.title || d.task.label)) || id }));
|
|
4738
|
+
detail.appendChild(el('pre', { text: JSON.stringify(d.task, null, 2) }));
|
|
4739
|
+
if (d.body) detail.appendChild(el('pre', { text: d.body }));
|
|
4740
|
+
}).catch(fail);
|
|
4741
|
+
}
|
|
4742
|
+
|
|
4743
|
+
function loadMarkdown(path, label) {
|
|
4744
|
+
single(true);
|
|
4745
|
+
fetchJson(path).then(function (d) {
|
|
4746
|
+
var detail = $('detail');
|
|
4747
|
+
var count = (typeof d.decisionCount === 'number') ? (' (' + d.decisionCount + ' decisions)') : '';
|
|
4748
|
+
detail.appendChild(el('p', { class: 'muted', text: label + count }));
|
|
4749
|
+
detail.appendChild(el('pre', { text: (d && d.body) || '(empty)' }));
|
|
4750
|
+
}).catch(fail);
|
|
4751
|
+
}
|
|
4752
|
+
|
|
4753
|
+
function loadApprovals() {
|
|
4754
|
+
single(true);
|
|
4755
|
+
fetchJson('/api/approvals').then(function (d) {
|
|
4756
|
+
var detail = $('detail');
|
|
4757
|
+
var groups = [['pending', d.pending || []], ['resolved', d.resolved || []]];
|
|
4758
|
+
groups.forEach(function (g) {
|
|
4759
|
+
detail.appendChild(el('h3', { text: g[0] + ' (' + g[1].length + ')' }));
|
|
4760
|
+
if (g[1].length === 0) { detail.appendChild(el('p', { class: 'muted', text: 'none' })); return; }
|
|
4761
|
+
g[1].forEach(function (a) {
|
|
4762
|
+
detail.appendChild(el('div', { class: 'row' }, [
|
|
4763
|
+
el('span', { text: a.id + ' ' }),
|
|
4764
|
+
el('span', { class: a.expired ? 'badge warn' : 'badge', text: a.expired ? 'expired' : (a.approval && a.approval.status) || '' })
|
|
4765
|
+
]));
|
|
4766
|
+
});
|
|
4767
|
+
});
|
|
4768
|
+
}).catch(fail);
|
|
4769
|
+
}
|
|
4770
|
+
|
|
4771
|
+
function kv(pairs) {
|
|
4772
|
+
var tbody = el('tbody', {}, pairs.map(function (p) {
|
|
4773
|
+
return el('tr', {}, [el('td', { class: 'k', text: p[0] }), el('td', { text: String(p[1]) })]);
|
|
4774
|
+
}));
|
|
4775
|
+
return el('table', { class: 'kv' }, [tbody]);
|
|
4776
|
+
}
|
|
4777
|
+
|
|
4778
|
+
// --- wire up ------------------------------------------------------------
|
|
4779
|
+
|
|
4780
|
+
$('btn-refresh').addEventListener('click', function () { post('/api/refresh', 'Refresh all'); });
|
|
4781
|
+
$('btn-import-claude').addEventListener('click', function () { post('/api/import/claude-code', 'Import claude-code'); });
|
|
4782
|
+
$('btn-import-codex').addEventListener('click', function () { post('/api/import/codex', 'Import codex'); });
|
|
4783
|
+
$('btn-gen-handoff').addEventListener('click', function () { post('/api/handoff/generate', 'Regenerate handoff'); });
|
|
4784
|
+
$('btn-gen-decisions').addEventListener('click', function () { post('/api/decisions/generate', 'Regenerate decisions'); });
|
|
4785
|
+
|
|
4786
|
+
buildTabs();
|
|
4787
|
+
loadTab('overview');
|
|
4788
|
+
})();
|
|
4789
|
+
</script>
|
|
4790
|
+
</body>
|
|
4791
|
+
</html>`;
|
|
4792
|
+
|
|
4793
|
+
// src/lib/view-server.ts
|
|
4794
|
+
var HttpError = class extends Error {
|
|
4795
|
+
constructor(status, message) {
|
|
4796
|
+
super(message);
|
|
4797
|
+
this.status = status;
|
|
4798
|
+
}
|
|
4799
|
+
status;
|
|
4800
|
+
};
|
|
4801
|
+
var MAX_BODY_BYTES = 64 * 1024;
|
|
4802
|
+
function startViewServer(opts) {
|
|
4803
|
+
const { port, host = "127.0.0.1", deps } = opts;
|
|
4804
|
+
let actionQueue = Promise.resolve();
|
|
4805
|
+
const runExclusive = (fn) => {
|
|
4806
|
+
const result = actionQueue.then(fn, fn);
|
|
4807
|
+
actionQueue = result.then(
|
|
4808
|
+
() => void 0,
|
|
4809
|
+
() => void 0
|
|
4810
|
+
);
|
|
4811
|
+
return result;
|
|
4812
|
+
};
|
|
4813
|
+
let boundPort = port;
|
|
4814
|
+
const getPort = () => boundPort;
|
|
4815
|
+
return new Promise((resolve, reject) => {
|
|
4816
|
+
const server = createServer((req, res) => {
|
|
4817
|
+
handleRequest(req, res, deps, getPort, runExclusive).catch((error) => {
|
|
4818
|
+
sendError(res, error instanceof HttpError ? error.status : 500, pathlessMessage(error));
|
|
4819
|
+
});
|
|
4820
|
+
});
|
|
4821
|
+
server.on("error", reject);
|
|
4822
|
+
server.listen(port, host, () => {
|
|
4823
|
+
const address = server.address();
|
|
4824
|
+
boundPort = isAddressInfo(address) ? address.port : port;
|
|
4825
|
+
server.off("error", reject);
|
|
4826
|
+
resolve({
|
|
4827
|
+
url: `http://${host}:${boundPort}`,
|
|
4828
|
+
port: boundPort,
|
|
4829
|
+
close: () => closeServer(server)
|
|
4830
|
+
});
|
|
4831
|
+
});
|
|
4832
|
+
});
|
|
4833
|
+
}
|
|
4834
|
+
function isAddressInfo(value) {
|
|
4835
|
+
return value !== null && typeof value === "object";
|
|
4836
|
+
}
|
|
4837
|
+
function closeServer(server) {
|
|
4838
|
+
return new Promise((resolve) => {
|
|
4839
|
+
server.close(() => resolve());
|
|
4840
|
+
server.closeAllConnections();
|
|
4841
|
+
});
|
|
4842
|
+
}
|
|
4843
|
+
async function handleRequest(req, res, deps, getPort, runExclusive) {
|
|
4844
|
+
const method = req.method ?? "GET";
|
|
4845
|
+
const url = new URL(req.url ?? "/", "http://localhost");
|
|
4846
|
+
const pathname = url.pathname;
|
|
4847
|
+
if (!hostAllowed(req, getPort())) {
|
|
4848
|
+
sendError(res, 403, "Forbidden: host not allowed");
|
|
4849
|
+
return;
|
|
4850
|
+
}
|
|
4851
|
+
if (method === "GET") {
|
|
4852
|
+
await handleGet(res, pathname, deps);
|
|
4853
|
+
return;
|
|
4854
|
+
}
|
|
4855
|
+
if (method === "POST") {
|
|
4856
|
+
if (!originAllowed(req, getPort())) {
|
|
4857
|
+
sendError(res, 403, "Forbidden: cross-origin request");
|
|
4858
|
+
return;
|
|
4859
|
+
}
|
|
4860
|
+
const body = await readBody(req);
|
|
4861
|
+
await handlePost(res, pathname, body, deps, runExclusive);
|
|
4862
|
+
return;
|
|
4863
|
+
}
|
|
4864
|
+
sendError(res, 405, "Method not allowed");
|
|
4865
|
+
}
|
|
4866
|
+
async function handleGet(res, pathname, deps) {
|
|
4867
|
+
if (pathname === "/") {
|
|
4868
|
+
sendHtml(res, VIEW_HTML);
|
|
4869
|
+
return;
|
|
4870
|
+
}
|
|
4871
|
+
if (pathname === "/api/overview") {
|
|
4872
|
+
sendJson(res, 200, await overview(deps));
|
|
4873
|
+
return;
|
|
4874
|
+
}
|
|
4875
|
+
if (pathname === "/api/sessions") {
|
|
4876
|
+
sendJson(res, 200, await sessionsList(deps));
|
|
4877
|
+
return;
|
|
4878
|
+
}
|
|
4879
|
+
const sessionId = matchId(pathname, "/api/sessions/");
|
|
4880
|
+
if (sessionId !== null) {
|
|
4881
|
+
sendJson(res, 200, await sessionDetail(deps, sessionId));
|
|
4882
|
+
return;
|
|
4883
|
+
}
|
|
4884
|
+
if (pathname === "/api/tasks") {
|
|
4885
|
+
sendJson(res, 200, await tasksList(deps));
|
|
4886
|
+
return;
|
|
4887
|
+
}
|
|
4888
|
+
const taskId = matchId(pathname, "/api/tasks/");
|
|
4889
|
+
if (taskId !== null) {
|
|
4890
|
+
sendJson(res, 200, await taskDetail(deps, taskId));
|
|
4891
|
+
return;
|
|
4892
|
+
}
|
|
4893
|
+
if (pathname === "/api/decisions") {
|
|
4894
|
+
sendJson(res, 200, await decisionsView(deps));
|
|
4895
|
+
return;
|
|
4896
|
+
}
|
|
4897
|
+
if (pathname === "/api/approvals") {
|
|
4898
|
+
sendJson(res, 200, await approvalsView(deps));
|
|
4899
|
+
return;
|
|
4900
|
+
}
|
|
4901
|
+
if (pathname === "/api/handoff") {
|
|
4902
|
+
sendJson(res, 200, await handoffView(deps));
|
|
4903
|
+
return;
|
|
4904
|
+
}
|
|
4905
|
+
if (pathname === "/api/stats") {
|
|
4906
|
+
sendJson(res, 200, await computeWorkStats2({ paths: deps.paths, now: deps.nowProvider() }));
|
|
4907
|
+
return;
|
|
4908
|
+
}
|
|
4909
|
+
sendError(res, 404, "Not found");
|
|
4910
|
+
}
|
|
4911
|
+
async function handlePost(res, pathname, body, deps, runExclusive) {
|
|
4912
|
+
const nowIso = deps.nowProvider().toISOString();
|
|
4913
|
+
const actionOptions = readActionOptions(body);
|
|
4914
|
+
if (pathname === "/api/refresh") {
|
|
4915
|
+
const result = await runExclusive(
|
|
4916
|
+
() => refreshAll({ options: actionOptions, ctx: deps.importCtx, paths: deps.paths, nowIso })
|
|
4917
|
+
);
|
|
4918
|
+
sendJson(res, 200, result);
|
|
4919
|
+
return;
|
|
4920
|
+
}
|
|
4921
|
+
if (pathname === "/api/import/claude-code") {
|
|
4922
|
+
sendJson(res, 200, await runExclusive(() => importClaudeCode(actionOptions, deps.importCtx)));
|
|
4923
|
+
return;
|
|
4924
|
+
}
|
|
4925
|
+
if (pathname === "/api/import/codex") {
|
|
4926
|
+
sendJson(res, 200, await runExclusive(() => importCodex(actionOptions, deps.importCtx)));
|
|
4927
|
+
return;
|
|
4928
|
+
}
|
|
4929
|
+
if (pathname === "/api/handoff/generate") {
|
|
4930
|
+
sendJson(res, 200, await runExclusive(() => regenerateHandoff(deps.paths, nowIso)));
|
|
4931
|
+
return;
|
|
4932
|
+
}
|
|
4933
|
+
if (pathname === "/api/decisions/generate") {
|
|
4934
|
+
sendJson(res, 200, await runExclusive(() => regenerateDecisions(deps.paths, nowIso)));
|
|
4935
|
+
return;
|
|
4936
|
+
}
|
|
4937
|
+
sendError(res, 404, "Not found");
|
|
4938
|
+
}
|
|
4939
|
+
async function overview(deps) {
|
|
4940
|
+
let manifest;
|
|
4941
|
+
try {
|
|
4942
|
+
manifest = await readManifest8(deps.paths);
|
|
4943
|
+
} catch (error) {
|
|
4944
|
+
if (findErrorCode11(error, "ENOENT")) {
|
|
4945
|
+
return { initialized: false, repoRoot: deps.repoRoot };
|
|
4946
|
+
}
|
|
4947
|
+
throw error;
|
|
4948
|
+
}
|
|
4949
|
+
const nowIso = deps.nowProvider().toISOString();
|
|
4950
|
+
const handoff = await renderHandoff3({ paths: deps.paths, nowIso });
|
|
4951
|
+
const approvals = await enumerateApprovals2(deps.paths);
|
|
4952
|
+
return {
|
|
4953
|
+
initialized: true,
|
|
4954
|
+
repoRoot: deps.repoRoot,
|
|
4955
|
+
workspace: {
|
|
4956
|
+
id: manifest.workspace.id,
|
|
4957
|
+
name: manifest.workspace.name,
|
|
4958
|
+
basouVersion: manifest.basou_version
|
|
4959
|
+
},
|
|
4960
|
+
counts: {
|
|
4961
|
+
sessions: handoff.sessionCount,
|
|
4962
|
+
suspectSessions: handoff.suspectCount,
|
|
4963
|
+
tasks: handoff.taskCount,
|
|
4964
|
+
pendingTasks: handoff.pendingTaskCount,
|
|
4965
|
+
decisions: handoff.decisionCount,
|
|
4966
|
+
approvalsPending: approvals.pending.length,
|
|
4967
|
+
approvalsResolved: approvals.resolved.length
|
|
4968
|
+
},
|
|
4969
|
+
generatedAt: nowIso
|
|
4970
|
+
};
|
|
4971
|
+
}
|
|
4972
|
+
async function sessionsList(deps) {
|
|
4973
|
+
const entries = await loadSessionEntries3(deps.paths, { now: deps.nowProvider() });
|
|
4974
|
+
const sessions = entries.map((entry) => ({
|
|
4975
|
+
sessionId: entry.sessionId,
|
|
4976
|
+
label: entry.session.session.label ?? null,
|
|
4977
|
+
status: entry.session.session.status,
|
|
4978
|
+
sourceKind: entry.session.session.source.kind,
|
|
4979
|
+
startedAt: entry.session.session.started_at,
|
|
4980
|
+
endedAt: entry.session.session.ended_at ?? null,
|
|
4981
|
+
suspect: entry.suspect,
|
|
4982
|
+
suspectReason: entry.suspectReason,
|
|
4983
|
+
taskId: entry.session.session.task_id ?? null,
|
|
4984
|
+
relatedFilesCount: entry.session.session.related_files.length
|
|
4985
|
+
})).reverse();
|
|
4986
|
+
return { sessions };
|
|
4987
|
+
}
|
|
4988
|
+
async function sessionDetail(deps, sessionId) {
|
|
4989
|
+
let session;
|
|
4990
|
+
try {
|
|
4991
|
+
session = await readSessionYaml2(deps.paths, sessionId);
|
|
4992
|
+
} catch (error) {
|
|
4993
|
+
if (error instanceof Error && error.message === "YAML file not found") {
|
|
4994
|
+
throw new HttpError(404, "Session not found");
|
|
4995
|
+
}
|
|
4996
|
+
throw error;
|
|
4997
|
+
}
|
|
4998
|
+
try {
|
|
4999
|
+
const events = await readAllEvents2(join7(deps.paths.sessions, sessionId));
|
|
5000
|
+
return { session, events };
|
|
5001
|
+
} catch {
|
|
5002
|
+
return { session, events: [], degraded: true };
|
|
5003
|
+
}
|
|
5004
|
+
}
|
|
5005
|
+
async function tasksList(deps) {
|
|
5006
|
+
const entries = await loadTaskEntries2(deps.paths);
|
|
5007
|
+
return { tasks: entries.map((entry) => entry.task).reverse() };
|
|
5008
|
+
}
|
|
5009
|
+
async function taskDetail(deps, taskId) {
|
|
5010
|
+
try {
|
|
5011
|
+
const doc = await readTaskFile2(deps.paths, taskId);
|
|
5012
|
+
return { task: doc.task, body: doc.body };
|
|
5013
|
+
} catch (error) {
|
|
5014
|
+
if (error instanceof Error && error.message === "Task file not found") {
|
|
5015
|
+
throw new HttpError(404, "Task not found");
|
|
5016
|
+
}
|
|
5017
|
+
throw error;
|
|
5018
|
+
}
|
|
5019
|
+
}
|
|
5020
|
+
async function decisionsView(deps) {
|
|
5021
|
+
const fromDisk = await readMarkdownFile4(deps.paths.files.decisions);
|
|
5022
|
+
if (fromDisk !== null) {
|
|
5023
|
+
return { body: fromDisk, fromDisk: true };
|
|
5024
|
+
}
|
|
5025
|
+
const nowIso = deps.nowProvider().toISOString();
|
|
5026
|
+
const result = await renderDecisions3({ paths: deps.paths, nowIso });
|
|
5027
|
+
return { body: result.body, decisionCount: result.decisionCount, fromDisk: false };
|
|
5028
|
+
}
|
|
5029
|
+
async function approvalsView(deps) {
|
|
5030
|
+
const now = deps.nowProvider();
|
|
5031
|
+
const ids = await enumerateApprovals2(deps.paths);
|
|
5032
|
+
const toViews = async (list) => {
|
|
5033
|
+
const views = [];
|
|
5034
|
+
for (const id of list) {
|
|
5035
|
+
const loaded = await loadApproval2(deps.paths, id);
|
|
5036
|
+
if (loaded === null) continue;
|
|
5037
|
+
views.push({ id, expired: isLazyExpired2(loaded.approval, now), approval: loaded.approval });
|
|
5038
|
+
}
|
|
5039
|
+
return views;
|
|
5040
|
+
};
|
|
5041
|
+
return { pending: await toViews(ids.pending), resolved: await toViews(ids.resolved) };
|
|
5042
|
+
}
|
|
5043
|
+
async function handoffView(deps) {
|
|
5044
|
+
const fromDisk = await readMarkdownFile4(deps.paths.files.handoff);
|
|
5045
|
+
if (fromDisk !== null) {
|
|
5046
|
+
return { body: fromDisk, fromDisk: true };
|
|
5047
|
+
}
|
|
5048
|
+
const nowIso = deps.nowProvider().toISOString();
|
|
5049
|
+
const result = await renderHandoff3({ paths: deps.paths, nowIso });
|
|
5050
|
+
return { body: result.body, fromDisk: false };
|
|
5051
|
+
}
|
|
5052
|
+
function readActionOptions(body) {
|
|
5053
|
+
const options = {};
|
|
5054
|
+
if (typeof body.project === "string" && body.project.length > 0) options.project = body.project;
|
|
5055
|
+
if (body.force === true) options.force = true;
|
|
5056
|
+
if (body.dryRun === true) options.dryRun = true;
|
|
5057
|
+
return options;
|
|
5058
|
+
}
|
|
5059
|
+
function hostAllowed(req, port) {
|
|
5060
|
+
const host = req.headers.host;
|
|
5061
|
+
return host === `127.0.0.1:${port}` || host === `localhost:${port}`;
|
|
5062
|
+
}
|
|
5063
|
+
function originAllowed(req, port) {
|
|
5064
|
+
const origin = req.headers.origin;
|
|
5065
|
+
if (origin === void 0) return true;
|
|
5066
|
+
return origin === `http://127.0.0.1:${port}` || origin === `http://localhost:${port}`;
|
|
5067
|
+
}
|
|
5068
|
+
async function readBody(req) {
|
|
5069
|
+
const chunks = [];
|
|
5070
|
+
let size = 0;
|
|
5071
|
+
for await (const chunk of req) {
|
|
5072
|
+
const buf = Buffer.isBuffer(chunk) ? chunk : Buffer.from(String(chunk));
|
|
5073
|
+
size += buf.length;
|
|
5074
|
+
if (size > MAX_BODY_BYTES) throw new HttpError(413, "Request body too large");
|
|
5075
|
+
chunks.push(buf);
|
|
5076
|
+
}
|
|
5077
|
+
const raw = Buffer.concat(chunks).toString("utf8").trim();
|
|
5078
|
+
if (raw.length === 0) return {};
|
|
5079
|
+
try {
|
|
5080
|
+
const parsed = JSON.parse(raw);
|
|
5081
|
+
if (parsed !== null && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
5082
|
+
return parsed;
|
|
5083
|
+
}
|
|
5084
|
+
} catch {
|
|
5085
|
+
}
|
|
5086
|
+
throw new HttpError(400, "Invalid JSON body");
|
|
5087
|
+
}
|
|
5088
|
+
function matchId(pathname, prefix) {
|
|
5089
|
+
if (!pathname.startsWith(prefix)) return null;
|
|
5090
|
+
const encoded = pathname.slice(prefix.length);
|
|
5091
|
+
if (encoded.length === 0 || encoded.includes("/")) return null;
|
|
5092
|
+
let id;
|
|
5093
|
+
try {
|
|
5094
|
+
id = decodeURIComponent(encoded);
|
|
5095
|
+
} catch {
|
|
5096
|
+
return null;
|
|
5097
|
+
}
|
|
5098
|
+
if (id.length === 0 || id.includes("/") || id.includes("\\") || id.includes("\0") || id === "." || id === "..") {
|
|
5099
|
+
return null;
|
|
5100
|
+
}
|
|
5101
|
+
return id;
|
|
5102
|
+
}
|
|
5103
|
+
function sendJson(res, status, data) {
|
|
5104
|
+
const body = JSON.stringify(data);
|
|
5105
|
+
res.writeHead(status, {
|
|
5106
|
+
"Content-Type": "application/json; charset=utf-8",
|
|
5107
|
+
"Cache-Control": "no-store"
|
|
5108
|
+
});
|
|
5109
|
+
res.end(body);
|
|
5110
|
+
}
|
|
5111
|
+
function sendHtml(res, html) {
|
|
5112
|
+
res.writeHead(200, {
|
|
5113
|
+
"Content-Type": "text/html; charset=utf-8",
|
|
5114
|
+
"Cache-Control": "no-store"
|
|
5115
|
+
});
|
|
5116
|
+
res.end(html);
|
|
5117
|
+
}
|
|
5118
|
+
function sendError(res, status, message) {
|
|
5119
|
+
if (res.headersSent) {
|
|
5120
|
+
res.end();
|
|
5121
|
+
return;
|
|
5122
|
+
}
|
|
5123
|
+
sendJson(res, status, { error: message });
|
|
5124
|
+
}
|
|
5125
|
+
function pathlessMessage(error) {
|
|
5126
|
+
return error instanceof Error ? error.message : "Internal error";
|
|
5127
|
+
}
|
|
5128
|
+
|
|
5129
|
+
// src/commands/view.ts
|
|
5130
|
+
var DEFAULT_PORT = 4319;
|
|
5131
|
+
function parsePort(value) {
|
|
5132
|
+
const port = Number.parseInt(value, 10);
|
|
5133
|
+
if (!Number.isInteger(port) || port < 1 || port > 65535) {
|
|
5134
|
+
throw new InvalidArgumentError4("Port must be an integer between 1 and 65535.");
|
|
5135
|
+
}
|
|
5136
|
+
return port;
|
|
5137
|
+
}
|
|
5138
|
+
function registerViewCommand(program2) {
|
|
5139
|
+
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) => {
|
|
5140
|
+
await runView(options);
|
|
5141
|
+
});
|
|
5142
|
+
}
|
|
5143
|
+
async function runView(options, ctx = {}) {
|
|
5144
|
+
try {
|
|
5145
|
+
await doRunView(options, ctx);
|
|
5146
|
+
} catch (error) {
|
|
5147
|
+
renderCliError(error, { verbose: isVerbose(options) });
|
|
5148
|
+
process.exitCode = 1;
|
|
5149
|
+
}
|
|
5150
|
+
}
|
|
5151
|
+
async function doRunView(options, ctx) {
|
|
5152
|
+
const cwd = ctx.cwd ?? process.cwd();
|
|
5153
|
+
const repositoryRoot = await resolveRepositoryRootForView(cwd);
|
|
5154
|
+
const paths = basouPaths13(repositoryRoot);
|
|
5155
|
+
await assertWorkspaceInitialized10(paths.root);
|
|
5156
|
+
const deps = {
|
|
5157
|
+
paths,
|
|
5158
|
+
repoRoot: repositoryRoot,
|
|
5159
|
+
importCtx: {
|
|
5160
|
+
cwd: repositoryRoot,
|
|
5161
|
+
...ctx.claudeProjectsDir !== void 0 ? { claudeProjectsDir: ctx.claudeProjectsDir } : {},
|
|
5162
|
+
...ctx.codexSessionsDir !== void 0 ? { codexSessionsDir: ctx.codexSessionsDir } : {}
|
|
5163
|
+
},
|
|
5164
|
+
nowProvider: ctx.nowProvider ?? (() => /* @__PURE__ */ new Date())
|
|
5165
|
+
};
|
|
5166
|
+
const port = options.port ?? DEFAULT_PORT;
|
|
5167
|
+
const handle = await startListening(port, deps);
|
|
5168
|
+
try {
|
|
5169
|
+
console.log(`basou view running at ${handle.url}`);
|
|
5170
|
+
console.log(
|
|
5171
|
+
"Localhost only, no authentication. Do not expose this port beyond your machine. Press Ctrl+C to stop."
|
|
5172
|
+
);
|
|
5173
|
+
if (options.open !== false) {
|
|
5174
|
+
openInBrowser(handle.url, ctx.openBrowser);
|
|
5175
|
+
}
|
|
5176
|
+
ctx.onListening?.(handle);
|
|
5177
|
+
await waitForShutdown(ctx.signal);
|
|
5178
|
+
} finally {
|
|
5179
|
+
await handle.close();
|
|
5180
|
+
}
|
|
5181
|
+
}
|
|
5182
|
+
async function startListening(port, deps) {
|
|
5183
|
+
try {
|
|
5184
|
+
return await startViewServer({ port, deps });
|
|
5185
|
+
} catch (error) {
|
|
5186
|
+
if (findErrorCode12(error, "EADDRINUSE")) {
|
|
5187
|
+
throw new Error(`Port ${port} is already in use. Pass --port <n> to choose another.`, {
|
|
5188
|
+
cause: error
|
|
5189
|
+
});
|
|
5190
|
+
}
|
|
5191
|
+
throw error;
|
|
5192
|
+
}
|
|
5193
|
+
}
|
|
5194
|
+
function openInBrowser(url, override) {
|
|
5195
|
+
if (override !== void 0) {
|
|
5196
|
+
override(url);
|
|
5197
|
+
return;
|
|
5198
|
+
}
|
|
5199
|
+
if (process.platform !== "darwin") return;
|
|
5200
|
+
try {
|
|
5201
|
+
const child = spawn("open", [url], { stdio: "ignore", detached: true });
|
|
5202
|
+
child.on("error", () => {
|
|
5203
|
+
});
|
|
5204
|
+
child.unref();
|
|
5205
|
+
} catch {
|
|
5206
|
+
}
|
|
5207
|
+
}
|
|
5208
|
+
function waitForShutdown(signal) {
|
|
5209
|
+
return new Promise((resolve) => {
|
|
5210
|
+
const cleanup = () => {
|
|
5211
|
+
process.off("SIGINT", onSignal);
|
|
5212
|
+
process.off("SIGTERM", onSignal);
|
|
5213
|
+
signal?.removeEventListener("abort", onAbort);
|
|
5214
|
+
};
|
|
5215
|
+
const onSignal = () => {
|
|
5216
|
+
cleanup();
|
|
5217
|
+
resolve();
|
|
5218
|
+
};
|
|
5219
|
+
const onAbort = () => {
|
|
5220
|
+
cleanup();
|
|
5221
|
+
resolve();
|
|
5222
|
+
};
|
|
5223
|
+
process.on("SIGINT", onSignal);
|
|
5224
|
+
process.on("SIGTERM", onSignal);
|
|
5225
|
+
if (signal !== void 0) {
|
|
5226
|
+
if (signal.aborted) {
|
|
5227
|
+
cleanup();
|
|
5228
|
+
resolve();
|
|
5229
|
+
return;
|
|
5230
|
+
}
|
|
5231
|
+
signal.addEventListener("abort", onAbort);
|
|
5232
|
+
}
|
|
5233
|
+
});
|
|
5234
|
+
}
|
|
5235
|
+
async function resolveRepositoryRootForView(cwd) {
|
|
5236
|
+
try {
|
|
5237
|
+
return await resolveRepositoryRoot14(cwd);
|
|
5238
|
+
} catch (error) {
|
|
5239
|
+
if (error instanceof Error && error.message === "Not a git repository") {
|
|
5240
|
+
throw new Error("Not a git repository. Run 'git init' first, then re-run 'basou view'.", {
|
|
5241
|
+
cause: error
|
|
5242
|
+
});
|
|
5243
|
+
}
|
|
5244
|
+
throw error;
|
|
5245
|
+
}
|
|
5246
|
+
}
|
|
5247
|
+
async function assertWorkspaceInitialized10(basouRoot) {
|
|
5248
|
+
try {
|
|
5249
|
+
await assertBasouRootSafe13(basouRoot);
|
|
5250
|
+
} catch (error) {
|
|
5251
|
+
if (findErrorCode12(error, "ENOENT")) {
|
|
5252
|
+
throw new Error("Workspace not initialized. Run 'basou init' first.");
|
|
5253
|
+
}
|
|
5254
|
+
throw error;
|
|
5255
|
+
}
|
|
5256
|
+
}
|
|
5257
|
+
|
|
5258
|
+
// src/program.ts
|
|
3575
5259
|
var require2 = createRequire(import.meta.url);
|
|
3576
5260
|
var pkg = require2("../package.json");
|
|
3577
5261
|
var BASOU_CLI_VERSION = pkg.version;
|
|
3578
|
-
|
|
3579
|
-
|
|
3580
|
-
|
|
3581
|
-
|
|
3582
|
-
|
|
3583
|
-
|
|
3584
|
-
|
|
3585
|
-
|
|
3586
|
-
|
|
3587
|
-
|
|
3588
|
-
|
|
3589
|
-
|
|
5262
|
+
function buildProgram() {
|
|
5263
|
+
const program2 = new Command();
|
|
5264
|
+
program2.name("basou").description("Provenance layer for AI development").version(BASOU_CLI_VERSION).enablePositionalOptions();
|
|
5265
|
+
registerInitCommand(program2);
|
|
5266
|
+
registerStatusCommand(program2);
|
|
5267
|
+
registerStatsCommand(program2);
|
|
5268
|
+
registerExecCommand(program2);
|
|
5269
|
+
registerRunCommand(program2);
|
|
5270
|
+
registerSessionCommand(program2);
|
|
5271
|
+
registerImportCommand(program2);
|
|
5272
|
+
registerRefreshCommand(program2);
|
|
5273
|
+
registerViewCommand(program2);
|
|
5274
|
+
registerApprovalCommand(program2);
|
|
5275
|
+
registerDecisionCommand(program2);
|
|
5276
|
+
registerTaskCommand(program2);
|
|
5277
|
+
registerHandoffCommand(program2);
|
|
5278
|
+
registerDecisionsCommand(program2);
|
|
5279
|
+
return program2;
|
|
5280
|
+
}
|
|
5281
|
+
|
|
5282
|
+
// src/index.ts
|
|
5283
|
+
var program = buildProgram();
|
|
3590
5284
|
program.parseAsync(process.argv).catch((err) => {
|
|
3591
5285
|
renderCliError(err, { verbose: isVerbose(void 0) });
|
|
3592
5286
|
process.exit(1);
|