@basou/cli 0.5.0 → 0.7.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 +378 -93
- package/dist/index.js.map +1 -1
- package/dist/program.js +378 -93
- package/dist/program.js.map +1 -1
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -1327,9 +1327,9 @@ async function assertWorkspaceInitialized4(basouRoot) {
|
|
|
1327
1327
|
|
|
1328
1328
|
// src/commands/import.ts
|
|
1329
1329
|
import { createReadStream } from "fs";
|
|
1330
|
-
import { readdir, readFile, rm } from "fs/promises";
|
|
1330
|
+
import { readdir, readFile, rm, stat } from "fs/promises";
|
|
1331
1331
|
import { homedir as homedir2 } from "os";
|
|
1332
|
-
import { basename, join as join3 } from "path";
|
|
1332
|
+
import { basename, join as join3, resolve } from "path";
|
|
1333
1333
|
import { createInterface } from "readline";
|
|
1334
1334
|
import {
|
|
1335
1335
|
assertBasouRootSafe as assertBasouRootSafe6,
|
|
@@ -1348,11 +1348,16 @@ import {
|
|
|
1348
1348
|
} from "@basou/core";
|
|
1349
1349
|
var SES_PREFIX2 = "ses_";
|
|
1350
1350
|
var SHORT_ID_LEN2 = 6;
|
|
1351
|
+
function collectPath(value, previous) {
|
|
1352
|
+
return [...previous, value];
|
|
1353
|
+
}
|
|
1351
1354
|
function registerImportCommand(program2) {
|
|
1352
1355
|
const importCmd = program2.command("import").description("Import provenance from an external AI tool's native logs");
|
|
1353
1356
|
importCmd.command("claude-code").description("Derive Basou sessions from Claude Code native transcripts (~/.claude/projects)").option(
|
|
1354
1357
|
"--project <path>",
|
|
1355
|
-
"Source project path whose transcripts to import (defaults to the
|
|
1358
|
+
"Source project path whose transcripts to import (repeatable; defaults to the manifest source roots, then the repository root)",
|
|
1359
|
+
collectPath,
|
|
1360
|
+
[]
|
|
1356
1361
|
).option("--session <id>", "Import a single transcript by its Claude session id").option("--all", "Import every transcript found for the project").option(
|
|
1357
1362
|
"--force",
|
|
1358
1363
|
"Re-import sessions already imported: delete and replace them instead of skipping"
|
|
@@ -1361,7 +1366,9 @@ function registerImportCommand(program2) {
|
|
|
1361
1366
|
});
|
|
1362
1367
|
importCmd.command("codex").description("Derive Basou sessions from OpenAI Codex native rollout logs (~/.codex/sessions)").option(
|
|
1363
1368
|
"--project <path>",
|
|
1364
|
-
"Source project path whose rollouts to import (defaults to the
|
|
1369
|
+
"Source project path whose rollouts to import (repeatable; defaults to the manifest source roots, then the repository root)",
|
|
1370
|
+
collectPath,
|
|
1371
|
+
[]
|
|
1365
1372
|
).option("--session <id>", "Import a single rollout by its Codex session id").option("--all", "Import every rollout found for the project").option(
|
|
1366
1373
|
"--force",
|
|
1367
1374
|
"Re-import sessions already imported: delete and replace them instead of skipping"
|
|
@@ -1385,13 +1392,28 @@ async function runImportCodex(options, ctx = {}) {
|
|
|
1385
1392
|
process.exitCode = 1;
|
|
1386
1393
|
}
|
|
1387
1394
|
}
|
|
1395
|
+
function resolveSourceRoots(args) {
|
|
1396
|
+
const { projectFlags, manifest, repoRoot, cwd } = args;
|
|
1397
|
+
let resolved;
|
|
1398
|
+
if (projectFlags.length > 0) {
|
|
1399
|
+
resolved = projectFlags.map((p) => resolve(cwd, p));
|
|
1400
|
+
} else {
|
|
1401
|
+
const roots = manifest.import?.source_roots;
|
|
1402
|
+
resolved = roots !== void 0 && roots.length > 0 ? roots.map((r) => resolve(repoRoot, r)) : [repoRoot];
|
|
1403
|
+
}
|
|
1404
|
+
return [...new Set(resolved)];
|
|
1405
|
+
}
|
|
1388
1406
|
async function doRunImportClaudeCode(options, ctx) {
|
|
1389
1407
|
assertSelector(options);
|
|
1390
1408
|
const { repositoryRoot, paths, manifest } = await resolveImportTarget(ctx);
|
|
1391
|
-
const
|
|
1409
|
+
const projectPaths = resolveSourceRoots({
|
|
1410
|
+
projectFlags: options.project ?? [],
|
|
1411
|
+
manifest,
|
|
1412
|
+
repoRoot: repositoryRoot,
|
|
1413
|
+
cwd: ctx.cwd ?? process.cwd()
|
|
1414
|
+
});
|
|
1392
1415
|
const projectsRoot = ctx.claudeProjectsDir ?? join3(homedir2(), ".claude", "projects");
|
|
1393
|
-
const
|
|
1394
|
-
const files = await selectTranscriptFiles(transcriptDir, options);
|
|
1416
|
+
const files = await selectTranscriptFiles(projectsRoot, projectPaths, options);
|
|
1395
1417
|
const candidates = files.map((file) => {
|
|
1396
1418
|
const externalId = basename(file, ".jsonl");
|
|
1397
1419
|
return {
|
|
@@ -1407,9 +1429,14 @@ async function doRunImportClaudeCode(options, ctx) {
|
|
|
1407
1429
|
async function doRunImportCodex(options, ctx) {
|
|
1408
1430
|
assertSelector(options);
|
|
1409
1431
|
const { repositoryRoot, paths, manifest } = await resolveImportTarget(ctx);
|
|
1410
|
-
const
|
|
1432
|
+
const projectPaths = resolveSourceRoots({
|
|
1433
|
+
projectFlags: options.project ?? [],
|
|
1434
|
+
manifest,
|
|
1435
|
+
repoRoot: repositoryRoot,
|
|
1436
|
+
cwd: ctx.cwd ?? process.cwd()
|
|
1437
|
+
});
|
|
1411
1438
|
const sessionsRoot = ctx.codexSessionsDir ?? join3(homedir2(), ".codex", "sessions");
|
|
1412
|
-
const rollouts = await discoverCodexRollouts(sessionsRoot,
|
|
1439
|
+
const rollouts = await discoverCodexRollouts(sessionsRoot, projectPaths, options);
|
|
1413
1440
|
const candidates = rollouts.map(({ file, externalId }) => ({
|
|
1414
1441
|
externalId,
|
|
1415
1442
|
toPayload: async () => codexRolloutToImportPayload(await readJsonlRecords(file), {
|
|
@@ -1420,6 +1447,9 @@ async function doRunImportCodex(options, ctx) {
|
|
|
1420
1447
|
await importDerivedSessions(paths, manifest, options, CODEX_IMPORT_SOURCE, candidates);
|
|
1421
1448
|
}
|
|
1422
1449
|
function assertSelector(options) {
|
|
1450
|
+
if (options.session !== void 0 && options.all === true) {
|
|
1451
|
+
throw new Error("Specify either --session <id> or --all, not both");
|
|
1452
|
+
}
|
|
1423
1453
|
if (options.session === void 0 && options.all !== true) {
|
|
1424
1454
|
throw new Error("Specify --session <id> or --all");
|
|
1425
1455
|
}
|
|
@@ -1517,28 +1547,56 @@ async function loadExistingByExternalId(paths, sourceKind) {
|
|
|
1517
1547
|
}
|
|
1518
1548
|
return byExternalId;
|
|
1519
1549
|
}
|
|
1520
|
-
async function selectTranscriptFiles(
|
|
1550
|
+
async function selectTranscriptFiles(projectsRoot, projectPaths, options) {
|
|
1521
1551
|
if (options.session !== void 0) {
|
|
1522
|
-
|
|
1552
|
+
const matches = [];
|
|
1553
|
+
for (const projectPath of projectPaths) {
|
|
1554
|
+
const file = join3(projectsRoot, encodeProjectDir(projectPath), `${options.session}.jsonl`);
|
|
1555
|
+
if (await pathExists(file)) matches.push(file);
|
|
1556
|
+
}
|
|
1557
|
+
if (matches.length === 0) {
|
|
1558
|
+
throw new Error("Claude transcript not found for session id in project");
|
|
1559
|
+
}
|
|
1560
|
+
return [...new Set(matches)];
|
|
1561
|
+
}
|
|
1562
|
+
const files = [];
|
|
1563
|
+
let anyDirFound = false;
|
|
1564
|
+
for (const projectPath of projectPaths) {
|
|
1565
|
+
const transcriptDir = join3(projectsRoot, encodeProjectDir(projectPath));
|
|
1566
|
+
let entries;
|
|
1567
|
+
try {
|
|
1568
|
+
entries = await readdir(transcriptDir);
|
|
1569
|
+
} catch (error) {
|
|
1570
|
+
if (findErrorCode5(error, "ENOENT")) continue;
|
|
1571
|
+
throw new Error("Failed to read Claude transcript directory", { cause: error });
|
|
1572
|
+
}
|
|
1573
|
+
anyDirFound = true;
|
|
1574
|
+
for (const name of entries) {
|
|
1575
|
+
if (name.endsWith(".jsonl")) files.push(join3(transcriptDir, name));
|
|
1576
|
+
}
|
|
1523
1577
|
}
|
|
1524
|
-
|
|
1578
|
+
if (!anyDirFound) {
|
|
1579
|
+
throw new Error("Claude transcript directory not found for project");
|
|
1580
|
+
}
|
|
1581
|
+
return [...new Set(files)].sort();
|
|
1582
|
+
}
|
|
1583
|
+
async function pathExists(file) {
|
|
1525
1584
|
try {
|
|
1526
|
-
|
|
1585
|
+
await stat(file);
|
|
1586
|
+
return true;
|
|
1527
1587
|
} catch (error) {
|
|
1528
|
-
if (findErrorCode5(error, "ENOENT"))
|
|
1529
|
-
|
|
1530
|
-
}
|
|
1531
|
-
throw new Error("Failed to read Claude transcript directory", { cause: error });
|
|
1588
|
+
if (findErrorCode5(error, "ENOENT")) return false;
|
|
1589
|
+
throw error;
|
|
1532
1590
|
}
|
|
1533
|
-
return entries.filter((name) => name.endsWith(".jsonl")).sort().map((name) => join3(transcriptDir, name));
|
|
1534
1591
|
}
|
|
1535
|
-
async function discoverCodexRollouts(sessionsRoot,
|
|
1592
|
+
async function discoverCodexRollouts(sessionsRoot, projectPaths, options) {
|
|
1593
|
+
const projectSet = new Set(projectPaths);
|
|
1536
1594
|
const files = await findRolloutFiles(sessionsRoot);
|
|
1537
1595
|
const matched = [];
|
|
1538
1596
|
for (const file of files) {
|
|
1539
1597
|
const meta = await readRolloutMeta(file);
|
|
1540
1598
|
if (meta === void 0) continue;
|
|
1541
|
-
if (meta.cwd
|
|
1599
|
+
if (!projectSet.has(meta.cwd)) continue;
|
|
1542
1600
|
if (options.session !== void 0 && meta.id !== options.session) continue;
|
|
1543
1601
|
matched.push({ file, externalId: meta.id });
|
|
1544
1602
|
}
|
|
@@ -1709,7 +1767,7 @@ async function assertWorkspaceInitialized5(basouRoot) {
|
|
|
1709
1767
|
}
|
|
1710
1768
|
|
|
1711
1769
|
// src/commands/init.ts
|
|
1712
|
-
import { basename as basename2 } from "path";
|
|
1770
|
+
import { basename as basename2, relative, resolve as resolve2 } from "path";
|
|
1713
1771
|
import {
|
|
1714
1772
|
appendBasouGitignore,
|
|
1715
1773
|
createManifest,
|
|
@@ -1718,10 +1776,18 @@ import {
|
|
|
1718
1776
|
tryRemoteUrl,
|
|
1719
1777
|
writeManifest
|
|
1720
1778
|
} from "@basou/core";
|
|
1779
|
+
function collectValue(value, previous) {
|
|
1780
|
+
return [...previous, value];
|
|
1781
|
+
}
|
|
1721
1782
|
function registerInitCommand(program2) {
|
|
1722
1783
|
program2.command("init").description("Initialize a Basou workspace at the current Git repository root").option("--name <name>", "Workspace name (defaults to the repository directory name)").option("--project-name <name>", "Project display name").option("--project-description <description>", "Project description").option(
|
|
1723
1784
|
"--repo-url <url>",
|
|
1724
1785
|
"Repository URL (defaults to git remote.origin.url; pass empty string for null)"
|
|
1786
|
+
).option(
|
|
1787
|
+
"--source-root <path>",
|
|
1788
|
+
"Extra import source root, relative to the repo root (repeatable; aggregates sibling repos into this workspace)",
|
|
1789
|
+
collectValue,
|
|
1790
|
+
[]
|
|
1725
1791
|
).option("-f, --force", "Overwrite an existing manifest").option("-v, --verbose", "Show error causes").action(async (options) => {
|
|
1726
1792
|
await runInit(options);
|
|
1727
1793
|
});
|
|
@@ -1744,12 +1810,17 @@ async function doRunInit(options, ctx) {
|
|
|
1744
1810
|
} else {
|
|
1745
1811
|
repositoryUrl = await tryRemoteUrl(repositoryRoot);
|
|
1746
1812
|
}
|
|
1813
|
+
const sourceRoots = (options.sourceRoot ?? []).map((p) => {
|
|
1814
|
+
const rel = relative(repositoryRoot, resolve2(cwd, p));
|
|
1815
|
+
return rel === "" ? "." : rel;
|
|
1816
|
+
});
|
|
1747
1817
|
const paths = await ensureBasouDirectory(repositoryRoot);
|
|
1748
1818
|
const manifest = createManifest({
|
|
1749
1819
|
workspaceName,
|
|
1750
1820
|
...options.projectName !== void 0 ? { projectName: options.projectName } : {},
|
|
1751
1821
|
...options.projectDescription !== void 0 ? { projectDescription: options.projectDescription } : {},
|
|
1752
|
-
...repositoryUrl !== void 0 ? { repositoryUrl } : {}
|
|
1822
|
+
...repositoryUrl !== void 0 ? { repositoryUrl } : {},
|
|
1823
|
+
...sourceRoots.length > 0 ? { sourceRoots } : {}
|
|
1753
1824
|
});
|
|
1754
1825
|
await writeManifest(paths, manifest, { force: options.force === true });
|
|
1755
1826
|
try {
|
|
@@ -1783,7 +1854,8 @@ async function resolveRepositoryRootForInit(cwd) {
|
|
|
1783
1854
|
}
|
|
1784
1855
|
|
|
1785
1856
|
// src/commands/refresh.ts
|
|
1786
|
-
import { assertBasouRootSafe as assertBasouRootSafe7, basouPaths as basouPaths7, findErrorCode as
|
|
1857
|
+
import { assertBasouRootSafe as assertBasouRootSafe7, basouPaths as basouPaths7, findErrorCode as findErrorCode7, resolveRepositoryRoot as resolveRepositoryRoot8 } from "@basou/core";
|
|
1858
|
+
import { InvalidArgumentError as InvalidArgumentError2 } from "commander";
|
|
1787
1859
|
|
|
1788
1860
|
// src/lib/provenance-actions.ts
|
|
1789
1861
|
import {
|
|
@@ -1906,25 +1978,213 @@ async function refreshAll(args) {
|
|
|
1906
1978
|
};
|
|
1907
1979
|
}
|
|
1908
1980
|
|
|
1981
|
+
// src/commands/refresh-watch.ts
|
|
1982
|
+
import { readdir as readdir2, stat as stat2 } from "fs/promises";
|
|
1983
|
+
import { homedir as homedir3 } from "os";
|
|
1984
|
+
import { join as join4 } from "path";
|
|
1985
|
+
import { findErrorCode as findErrorCode6 } from "@basou/core";
|
|
1986
|
+
var DEFAULT_WATCH_INTERVAL_SEC = 30;
|
|
1987
|
+
var MIN_WATCH_INTERVAL_SEC = 5;
|
|
1988
|
+
var MAX_WATCH_INTERVAL_SEC = 86400;
|
|
1989
|
+
function watchedRoots(ctx) {
|
|
1990
|
+
return [
|
|
1991
|
+
ctx.codexSessionsDir ?? join4(homedir3(), ".codex", "sessions"),
|
|
1992
|
+
ctx.claudeProjectsDir ?? join4(homedir3(), ".claude", "projects")
|
|
1993
|
+
];
|
|
1994
|
+
}
|
|
1995
|
+
async function scanSourceLogs(roots) {
|
|
1996
|
+
const out = /* @__PURE__ */ new Map();
|
|
1997
|
+
const walk = async (dir) => {
|
|
1998
|
+
let entries;
|
|
1999
|
+
try {
|
|
2000
|
+
entries = await readdir2(dir, { withFileTypes: true });
|
|
2001
|
+
} catch (error) {
|
|
2002
|
+
if (findErrorCode6(error, "ENOENT") || findErrorCode6(error, "ENOTDIR")) return;
|
|
2003
|
+
throw new Error("Failed to read a source log directory", { cause: error });
|
|
2004
|
+
}
|
|
2005
|
+
for (const entry of entries) {
|
|
2006
|
+
const full = join4(dir, entry.name);
|
|
2007
|
+
if (entry.isDirectory()) {
|
|
2008
|
+
await walk(full);
|
|
2009
|
+
} else if (entry.isFile() && entry.name.endsWith(".jsonl")) {
|
|
2010
|
+
try {
|
|
2011
|
+
const info = await stat2(full);
|
|
2012
|
+
out.set(full, { mtimeMs: info.mtimeMs, size: info.size });
|
|
2013
|
+
} catch (error) {
|
|
2014
|
+
if (findErrorCode6(error, "ENOENT")) continue;
|
|
2015
|
+
throw new Error("Failed to stat a source log file", { cause: error });
|
|
2016
|
+
}
|
|
2017
|
+
}
|
|
2018
|
+
}
|
|
2019
|
+
};
|
|
2020
|
+
for (const root of roots) await walk(root);
|
|
2021
|
+
return out;
|
|
2022
|
+
}
|
|
2023
|
+
function scansEqual(a, b) {
|
|
2024
|
+
if (a.size !== b.size) return false;
|
|
2025
|
+
for (const [path, sig] of a) {
|
|
2026
|
+
const other = b.get(path);
|
|
2027
|
+
if (other === void 0 || other.mtimeMs !== sig.mtimeMs || other.size !== sig.size) {
|
|
2028
|
+
return false;
|
|
2029
|
+
}
|
|
2030
|
+
}
|
|
2031
|
+
return true;
|
|
2032
|
+
}
|
|
2033
|
+
function importedCount(outcome) {
|
|
2034
|
+
return outcome.status === "ran" ? outcome.importedCount : 0;
|
|
2035
|
+
}
|
|
2036
|
+
function describeOutcome(outcome) {
|
|
2037
|
+
return outcome.status === "ran" ? `${outcome.adapter} +${outcome.importedCount}` : `${outcome.adapter} skipped`;
|
|
2038
|
+
}
|
|
2039
|
+
function hms(date) {
|
|
2040
|
+
return date.toISOString().slice(11, 19);
|
|
2041
|
+
}
|
|
2042
|
+
async function runImports(deps) {
|
|
2043
|
+
const claude = await importClaudeCode(deps.importOptions, deps.ctx);
|
|
2044
|
+
const codex = await importCodex(deps.importOptions, deps.ctx);
|
|
2045
|
+
return { claude, codex, imported: importedCount(claude) + importedCount(codex) };
|
|
2046
|
+
}
|
|
2047
|
+
async function regenerate(deps) {
|
|
2048
|
+
const nowIso = deps.now().toISOString();
|
|
2049
|
+
const handoff = await regenerateHandoff(deps.paths, nowIso);
|
|
2050
|
+
await regenerateDecisions(deps.paths, nowIso);
|
|
2051
|
+
return handoff.sessionCount;
|
|
2052
|
+
}
|
|
2053
|
+
async function runRefreshWatch(deps) {
|
|
2054
|
+
const { intervalMs, ctx, signal, sleep, log } = deps;
|
|
2055
|
+
const roots = watchedRoots(ctx);
|
|
2056
|
+
log(
|
|
2057
|
+
`watching ${roots.join(", ")} every ${Math.round(intervalMs / 1e3)}s (imports on change; Ctrl-C to stop)`
|
|
2058
|
+
);
|
|
2059
|
+
let lastScan = await scanSourceLogs(roots);
|
|
2060
|
+
let importedScan = lastScan;
|
|
2061
|
+
const initial = await runImports(deps);
|
|
2062
|
+
const initialSessions = await regenerate(deps);
|
|
2063
|
+
log(
|
|
2064
|
+
`[${hms(deps.now())}] refreshed: ${describeOutcome(initial.codex)}, ${describeOutcome(initial.claude)} (sessions: ${initialSessions})`
|
|
2065
|
+
);
|
|
2066
|
+
if (signal.aborted) {
|
|
2067
|
+
log("watch stopped");
|
|
2068
|
+
return;
|
|
2069
|
+
}
|
|
2070
|
+
let pendingRegen = false;
|
|
2071
|
+
while (!signal.aborted) {
|
|
2072
|
+
await sleep(intervalMs, signal);
|
|
2073
|
+
if (signal.aborted) break;
|
|
2074
|
+
try {
|
|
2075
|
+
const current = await scanSourceLogs(roots);
|
|
2076
|
+
if (scansEqual(current, lastScan) && !scansEqual(current, importedScan)) {
|
|
2077
|
+
const { claude, codex, imported } = await runImports(deps);
|
|
2078
|
+
if (imported > 0) pendingRegen = true;
|
|
2079
|
+
if (pendingRegen) {
|
|
2080
|
+
const sessions = await regenerate(deps);
|
|
2081
|
+
pendingRegen = false;
|
|
2082
|
+
log(
|
|
2083
|
+
`[${hms(deps.now())}] refreshed: ${describeOutcome(codex)}, ${describeOutcome(claude)} (sessions: ${sessions})`
|
|
2084
|
+
);
|
|
2085
|
+
}
|
|
2086
|
+
importedScan = current;
|
|
2087
|
+
}
|
|
2088
|
+
lastScan = current;
|
|
2089
|
+
} catch (error) {
|
|
2090
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
2091
|
+
log(`[${hms(deps.now())}] refresh cycle skipped: ${message}`);
|
|
2092
|
+
}
|
|
2093
|
+
}
|
|
2094
|
+
log("watch stopped");
|
|
2095
|
+
}
|
|
2096
|
+
|
|
1909
2097
|
// src/commands/refresh.ts
|
|
2098
|
+
function collectPath2(value, previous) {
|
|
2099
|
+
return [...previous, value];
|
|
2100
|
+
}
|
|
2101
|
+
function parseInterval(value) {
|
|
2102
|
+
const seconds = Number(value);
|
|
2103
|
+
if (!Number.isInteger(seconds) || seconds < MIN_WATCH_INTERVAL_SEC || seconds > MAX_WATCH_INTERVAL_SEC) {
|
|
2104
|
+
throw new InvalidArgumentError2(
|
|
2105
|
+
`--interval must be an integer between ${MIN_WATCH_INTERVAL_SEC} and ${MAX_WATCH_INTERVAL_SEC} (seconds).`
|
|
2106
|
+
);
|
|
2107
|
+
}
|
|
2108
|
+
return seconds;
|
|
2109
|
+
}
|
|
2110
|
+
function abortableSleep(ms, signal) {
|
|
2111
|
+
return new Promise((resolve3) => {
|
|
2112
|
+
if (signal.aborted) {
|
|
2113
|
+
resolve3();
|
|
2114
|
+
return;
|
|
2115
|
+
}
|
|
2116
|
+
let timer;
|
|
2117
|
+
const onAbort = () => {
|
|
2118
|
+
clearTimeout(timer);
|
|
2119
|
+
resolve3();
|
|
2120
|
+
};
|
|
2121
|
+
timer = setTimeout(() => {
|
|
2122
|
+
signal.removeEventListener("abort", onAbort);
|
|
2123
|
+
resolve3();
|
|
2124
|
+
}, ms);
|
|
2125
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
2126
|
+
});
|
|
2127
|
+
}
|
|
1910
2128
|
function registerRefreshCommand(program2) {
|
|
1911
2129
|
program2.command("refresh").description(
|
|
1912
2130
|
"Import all adapters for the project and regenerate handoff + decisions in one step"
|
|
1913
2131
|
).option(
|
|
1914
2132
|
"--project <path>",
|
|
1915
|
-
"Source project path to import (defaults to the
|
|
1916
|
-
|
|
2133
|
+
"Source project path to import (repeatable; defaults to the manifest source roots, then the repository root)",
|
|
2134
|
+
collectPath2,
|
|
2135
|
+
[]
|
|
2136
|
+
).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(
|
|
2137
|
+
"--watch",
|
|
2138
|
+
"Keep running: re-import + regenerate when the native logs change (Ctrl-C to stop)"
|
|
2139
|
+
).option(
|
|
2140
|
+
"--interval <seconds>",
|
|
2141
|
+
`Poll interval for --watch, in seconds (default ${DEFAULT_WATCH_INTERVAL_SEC}, min ${MIN_WATCH_INTERVAL_SEC})`,
|
|
2142
|
+
parseInterval
|
|
2143
|
+
).option("-v, --verbose", "Show error causes").action(async (options) => {
|
|
1917
2144
|
await runRefresh(options);
|
|
1918
2145
|
});
|
|
1919
2146
|
}
|
|
1920
2147
|
async function runRefresh(options, ctx = {}) {
|
|
1921
2148
|
try {
|
|
1922
|
-
|
|
2149
|
+
if (options.watch === true) {
|
|
2150
|
+
await doRunRefreshWatch(options, ctx);
|
|
2151
|
+
} else {
|
|
2152
|
+
await doRunRefresh(options, ctx);
|
|
2153
|
+
}
|
|
1923
2154
|
} catch (error) {
|
|
1924
2155
|
renderCliError(error, { verbose: isVerbose(options) });
|
|
1925
2156
|
process.exitCode = 1;
|
|
1926
2157
|
}
|
|
1927
2158
|
}
|
|
2159
|
+
async function doRunRefreshWatch(options, ctx) {
|
|
2160
|
+
if (options.dryRun === true) throw new Error("--watch cannot be combined with --dry-run.");
|
|
2161
|
+
if (options.json === true) throw new Error("--watch cannot be combined with --json.");
|
|
2162
|
+
if (options.force === true) throw new Error("--watch cannot be combined with --force.");
|
|
2163
|
+
const cwd = ctx.cwd ?? process.cwd();
|
|
2164
|
+
const repositoryRoot = await resolveRepositoryRootForRefresh(cwd);
|
|
2165
|
+
const paths = basouPaths7(repositoryRoot);
|
|
2166
|
+
await assertWorkspaceInitialized6(paths.root);
|
|
2167
|
+
const intervalMs = (options.interval ?? DEFAULT_WATCH_INTERVAL_SEC) * 1e3;
|
|
2168
|
+
const controller = new AbortController();
|
|
2169
|
+
const onSignal = () => controller.abort();
|
|
2170
|
+
process.on("SIGINT", onSignal);
|
|
2171
|
+
process.on("SIGTERM", onSignal);
|
|
2172
|
+
try {
|
|
2173
|
+
await runRefreshWatch({
|
|
2174
|
+
ctx,
|
|
2175
|
+
paths,
|
|
2176
|
+
intervalMs,
|
|
2177
|
+
importOptions: options.project !== void 0 && options.project.length > 0 ? { project: options.project } : {},
|
|
2178
|
+
now: () => ctx.nowProvider?.() ?? /* @__PURE__ */ new Date(),
|
|
2179
|
+
signal: controller.signal,
|
|
2180
|
+
sleep: abortableSleep,
|
|
2181
|
+
log: (line) => console.log(line)
|
|
2182
|
+
});
|
|
2183
|
+
} finally {
|
|
2184
|
+
process.off("SIGINT", onSignal);
|
|
2185
|
+
process.off("SIGTERM", onSignal);
|
|
2186
|
+
}
|
|
2187
|
+
}
|
|
1928
2188
|
async function doRunRefresh(options, ctx) {
|
|
1929
2189
|
const cwd = ctx.cwd ?? process.cwd();
|
|
1930
2190
|
const repositoryRoot = await resolveRepositoryRootForRefresh(cwd);
|
|
@@ -1933,7 +2193,7 @@ async function doRunRefresh(options, ctx) {
|
|
|
1933
2193
|
const nowIso = (ctx.nowProvider?.() ?? /* @__PURE__ */ new Date()).toISOString();
|
|
1934
2194
|
const result = await refreshAll({
|
|
1935
2195
|
options: {
|
|
1936
|
-
...options.project !== void 0 ? { project: options.project } : {},
|
|
2196
|
+
...options.project !== void 0 && options.project.length > 0 ? { project: options.project } : {},
|
|
1937
2197
|
...options.force === true ? { force: true } : {},
|
|
1938
2198
|
...options.dryRun === true ? { dryRun: true } : {}
|
|
1939
2199
|
},
|
|
@@ -1991,7 +2251,7 @@ async function assertWorkspaceInitialized6(basouRoot) {
|
|
|
1991
2251
|
try {
|
|
1992
2252
|
await assertBasouRootSafe7(basouRoot);
|
|
1993
2253
|
} catch (error) {
|
|
1994
|
-
if (
|
|
2254
|
+
if (findErrorCode7(error, "ENOENT")) {
|
|
1995
2255
|
throw new Error("Workspace not initialized. Run 'basou init' first.");
|
|
1996
2256
|
}
|
|
1997
2257
|
throw error;
|
|
@@ -2000,8 +2260,8 @@ async function assertWorkspaceInitialized6(basouRoot) {
|
|
|
2000
2260
|
|
|
2001
2261
|
// src/commands/run.ts
|
|
2002
2262
|
import { mkdir as mkdir2 } from "fs/promises";
|
|
2003
|
-
import { homedir as
|
|
2004
|
-
import { join as
|
|
2263
|
+
import { homedir as homedir4 } from "os";
|
|
2264
|
+
import { join as join5 } from "path";
|
|
2005
2265
|
import {
|
|
2006
2266
|
assertBasouRootSafe as assertBasouRootSafe8,
|
|
2007
2267
|
basouPaths as basouPaths8,
|
|
@@ -2053,10 +2313,10 @@ async function runClaudeCode(args, options, ctx = {}) {
|
|
|
2053
2313
|
await assertBasouRootSafe8(paths.root);
|
|
2054
2314
|
const manifest = await readManifest4(paths);
|
|
2055
2315
|
const sessionId = prefixedUlid4("ses");
|
|
2056
|
-
const sessionDir =
|
|
2316
|
+
const sessionDir = join5(paths.sessions, sessionId);
|
|
2057
2317
|
await mkdir2(sessionDir, { recursive: true });
|
|
2058
2318
|
const startedAt = now().toISOString();
|
|
2059
|
-
const sessionYamlPath =
|
|
2319
|
+
const sessionYamlPath = join5(sessionDir, "session.yaml");
|
|
2060
2320
|
const session = buildInitialSession2({
|
|
2061
2321
|
id: sessionId,
|
|
2062
2322
|
command,
|
|
@@ -2177,7 +2437,7 @@ async function runClaudeCode(args, options, ctx = {}) {
|
|
|
2177
2437
|
const rawRelated = computeRelatedFiles(preSnapshot, postSnapshot, diff);
|
|
2178
2438
|
const relatedFiles = sanitizeRelatedFiles(rawRelated, {
|
|
2179
2439
|
workingDirectory: repoRoot,
|
|
2180
|
-
homedir:
|
|
2440
|
+
homedir: homedir4()
|
|
2181
2441
|
}).sanitized;
|
|
2182
2442
|
const finalStatus = decideFinalStatus2(result, signalReceived);
|
|
2183
2443
|
await appendEvent2(sessionDir, {
|
|
@@ -2321,7 +2581,7 @@ function buildInitialSession2(input) {
|
|
|
2321
2581
|
source: { ...claudeCodeAdapterMetadata },
|
|
2322
2582
|
started_at: input.startedAt,
|
|
2323
2583
|
status: "initialized",
|
|
2324
|
-
working_directory: sanitizeWorkingDirectory2(input.cwd, { homedir:
|
|
2584
|
+
working_directory: sanitizeWorkingDirectory2(input.cwd, { homedir: homedir4() }),
|
|
2325
2585
|
invocation: {
|
|
2326
2586
|
command: input.command,
|
|
2327
2587
|
args: [...input.args],
|
|
@@ -2394,13 +2654,13 @@ async function resolveRepositoryRootForRun(cwd) {
|
|
|
2394
2654
|
|
|
2395
2655
|
// src/commands/session.ts
|
|
2396
2656
|
import { readFile as readFile2 } from "fs/promises";
|
|
2397
|
-
import { basename as basename3, isAbsolute, join as
|
|
2657
|
+
import { basename as basename3, isAbsolute, join as join6, relative as relative2 } from "path";
|
|
2398
2658
|
import {
|
|
2399
2659
|
acquireLock as acquireLock2,
|
|
2400
2660
|
appendEventToExistingSession as appendEventToExistingSession2,
|
|
2401
2661
|
assertBasouRootSafe as assertBasouRootSafe9,
|
|
2402
2662
|
basouPaths as basouPaths9,
|
|
2403
|
-
findErrorCode as
|
|
2663
|
+
findErrorCode as findErrorCode8,
|
|
2404
2664
|
importSessionFromJson as importSessionFromJson2,
|
|
2405
2665
|
loadSessionEntries,
|
|
2406
2666
|
readAllEvents,
|
|
@@ -2414,7 +2674,7 @@ import {
|
|
|
2414
2674
|
SessionStatusSchema,
|
|
2415
2675
|
sessionWorkStatsFromEvents
|
|
2416
2676
|
} from "@basou/core";
|
|
2417
|
-
import { InvalidArgumentError as
|
|
2677
|
+
import { InvalidArgumentError as InvalidArgumentError3 } from "commander";
|
|
2418
2678
|
|
|
2419
2679
|
// src/lib/format-duration.ts
|
|
2420
2680
|
function formatDurationMs(ms) {
|
|
@@ -2521,14 +2781,14 @@ async function doRunSessionShow(idInput, options, ctx) {
|
|
|
2521
2781
|
const paths = basouPaths9(repositoryRoot);
|
|
2522
2782
|
await assertWorkspaceInitialized7(paths.root);
|
|
2523
2783
|
const sessionId = await resolveSessionId2(paths, idInput);
|
|
2524
|
-
const sessionDir =
|
|
2525
|
-
const sessionYamlPath =
|
|
2784
|
+
const sessionDir = join6(paths.sessions, sessionId);
|
|
2785
|
+
const sessionYamlPath = join6(sessionDir, "session.yaml");
|
|
2526
2786
|
let session;
|
|
2527
2787
|
try {
|
|
2528
2788
|
const raw = await readYamlFile4(sessionYamlPath);
|
|
2529
2789
|
session = SessionSchema3.parse(raw);
|
|
2530
2790
|
} catch (error) {
|
|
2531
|
-
if (
|
|
2791
|
+
if (findErrorCode8(error, "ENOENT")) {
|
|
2532
2792
|
throw new Error(`Session not found: ${idInput}`);
|
|
2533
2793
|
}
|
|
2534
2794
|
throw new Error("Failed to read session", { cause: error });
|
|
@@ -2632,6 +2892,9 @@ function formatSessionWork(session, events, now) {
|
|
|
2632
2892
|
parts.push(`${w.commandCount} cmd / ${w.fileChangedCount} files / ${w.decisionCount} dec`);
|
|
2633
2893
|
const activeBasis = w.activeTimeBasis === "engaged-turns" ? "turns" : "events";
|
|
2634
2894
|
parts.push(`active ${formatDurationMs(w.activeTimeMs)} (${activeBasis})`);
|
|
2895
|
+
if (w.availability.machineActive) {
|
|
2896
|
+
parts.push(`machine ${formatDurationMs(w.machineActiveTimeMs)}`);
|
|
2897
|
+
}
|
|
2635
2898
|
parts.push(`span ${formatDurationMs(w.sessionSpanMs)}${w.open ? " (open)" : ""}`);
|
|
2636
2899
|
parts.push(
|
|
2637
2900
|
w.availability.commandTime ? `command ${formatDurationMs(w.commandTimeMs)}` : "command n/a (import)"
|
|
@@ -2645,7 +2908,7 @@ function formatWorkingDir(workingDir, repositoryRoot, options) {
|
|
|
2645
2908
|
return workingDir;
|
|
2646
2909
|
}
|
|
2647
2910
|
if (workingDir === repositoryRoot) return "<repository_root>";
|
|
2648
|
-
const rel =
|
|
2911
|
+
const rel = relative2(repositoryRoot, workingDir);
|
|
2649
2912
|
if (rel.length === 0 || rel === ".") return "<repository_root>";
|
|
2650
2913
|
if (rel.startsWith("..")) return rel;
|
|
2651
2914
|
return `./${rel}`;
|
|
@@ -2775,7 +3038,7 @@ async function assertWorkspaceInitialized7(basouRoot) {
|
|
|
2775
3038
|
try {
|
|
2776
3039
|
await assertBasouRootSafe9(basouRoot);
|
|
2777
3040
|
} catch (error) {
|
|
2778
|
-
if (
|
|
3041
|
+
if (findErrorCode8(error, "ENOENT")) {
|
|
2779
3042
|
throw new Error("Workspace not initialized. Run 'basou init' first.");
|
|
2780
3043
|
}
|
|
2781
3044
|
throw error;
|
|
@@ -2844,10 +3107,10 @@ async function readInputFile(path) {
|
|
|
2844
3107
|
try {
|
|
2845
3108
|
return await readFile2(path, "utf8");
|
|
2846
3109
|
} catch (error) {
|
|
2847
|
-
if (
|
|
3110
|
+
if (findErrorCode8(error, "ENOENT")) {
|
|
2848
3111
|
throw new Error("Import source not found", { cause: error });
|
|
2849
3112
|
}
|
|
2850
|
-
if (
|
|
3113
|
+
if (findErrorCode8(error, "EISDIR")) {
|
|
2851
3114
|
throw new Error("Import source is not a file", { cause: error });
|
|
2852
3115
|
}
|
|
2853
3116
|
throw new Error("Failed to read import source", { cause: error });
|
|
@@ -2862,19 +3125,19 @@ function parseJsonStrict(body) {
|
|
|
2862
3125
|
}
|
|
2863
3126
|
function parseImportFormat(raw) {
|
|
2864
3127
|
if (raw !== "json") {
|
|
2865
|
-
throw new
|
|
3128
|
+
throw new InvalidArgumentError3(`Unsupported format: ${raw}. Valid values: json`);
|
|
2866
3129
|
}
|
|
2867
3130
|
return "json";
|
|
2868
3131
|
}
|
|
2869
3132
|
function parseLabelOverride(raw) {
|
|
2870
3133
|
if (raw.length === 0) {
|
|
2871
|
-
throw new
|
|
3134
|
+
throw new InvalidArgumentError3("Label must not be empty");
|
|
2872
3135
|
}
|
|
2873
3136
|
return raw;
|
|
2874
3137
|
}
|
|
2875
3138
|
function parseTaskIdOverride(raw) {
|
|
2876
3139
|
if (raw.length === 0) {
|
|
2877
|
-
throw new
|
|
3140
|
+
throw new InvalidArgumentError3("Task id is empty");
|
|
2878
3141
|
}
|
|
2879
3142
|
return raw;
|
|
2880
3143
|
}
|
|
@@ -2961,10 +3224,10 @@ async function readNoteFile(path) {
|
|
|
2961
3224
|
try {
|
|
2962
3225
|
return await readFile2(path, "utf8");
|
|
2963
3226
|
} catch (error) {
|
|
2964
|
-
if (
|
|
3227
|
+
if (findErrorCode8(error, "ENOENT")) {
|
|
2965
3228
|
throw new Error("Note source not found", { cause: error });
|
|
2966
3229
|
}
|
|
2967
|
-
if (
|
|
3230
|
+
if (findErrorCode8(error, "EISDIR")) {
|
|
2968
3231
|
throw new Error("Note source is not a file", { cause: error });
|
|
2969
3232
|
}
|
|
2970
3233
|
throw new Error("Failed to read note source", { cause: error });
|
|
@@ -2972,7 +3235,7 @@ async function readNoteFile(path) {
|
|
|
2972
3235
|
}
|
|
2973
3236
|
function parseNoteBodyOption(raw) {
|
|
2974
3237
|
if (raw.length === 0) {
|
|
2975
|
-
throw new
|
|
3238
|
+
throw new InvalidArgumentError3("--body must not be empty");
|
|
2976
3239
|
}
|
|
2977
3240
|
return raw;
|
|
2978
3241
|
}
|
|
@@ -2998,7 +3261,7 @@ import {
|
|
|
2998
3261
|
assertBasouRootSafe as assertBasouRootSafe10,
|
|
2999
3262
|
basouPaths as basouPaths10,
|
|
3000
3263
|
computeWorkStats,
|
|
3001
|
-
findErrorCode as
|
|
3264
|
+
findErrorCode as findErrorCode9,
|
|
3002
3265
|
resolveRepositoryRoot as resolveRepositoryRoot11
|
|
3003
3266
|
} from "@basou/core";
|
|
3004
3267
|
function registerStatsCommand(program2) {
|
|
@@ -3059,6 +3322,12 @@ function printStatsText(result, bySource, byDay) {
|
|
|
3059
3322
|
` Summed: ${formatDurationMs(t.activeTimeMs)} (per-session sum; concurrent sessions double-counted)`
|
|
3060
3323
|
);
|
|
3061
3324
|
}
|
|
3325
|
+
if (t.machineActiveAvailable) {
|
|
3326
|
+
const machineSessions = result.sessions.filter((s) => s.availability.machineActive).length;
|
|
3327
|
+
console.log(
|
|
3328
|
+
` Model working: ${formatDurationMs(t.machineActiveTimeMs)} (model compute, subset of active; Codex turn duration on ${machineSessions} of ${t.sessionCount} sessions; summed, not wall-clock-deduped)`
|
|
3329
|
+
);
|
|
3330
|
+
}
|
|
3062
3331
|
const openPart = t.openSessionCount > 0 ? `; ${t.openSessionCount} open counted to now` : "";
|
|
3063
3332
|
console.log(
|
|
3064
3333
|
` Span: ${formatDurationMs(t.sessionSpanMs)} (total elapsed${openPart})`
|
|
@@ -3078,8 +3347,9 @@ function printStatsText(result, bySource, byDay) {
|
|
|
3078
3347
|
console.log("");
|
|
3079
3348
|
console.log("By day (billable time x volume):");
|
|
3080
3349
|
for (const d of result.byDay) {
|
|
3350
|
+
const machine = d.machineActiveTimeMs > 0 ? ` (model ${formatDurationMs(d.machineActiveTimeMs)})` : "";
|
|
3081
3351
|
console.log(
|
|
3082
|
-
` ${d.date}: ${formatDurationMs(d.billableActiveTimeMs)} active, ${formatInt(d.tokens.output)} out tok, ${d.commandCount} cmd / ${d.fileChangedCount} files / ${d.decisionCount} dec`
|
|
3352
|
+
` ${d.date}: ${formatDurationMs(d.billableActiveTimeMs)} active${machine}, ${formatInt(d.tokens.output)} out tok, ${d.commandCount} cmd / ${d.fileChangedCount} files / ${d.decisionCount} dec`
|
|
3083
3353
|
);
|
|
3084
3354
|
}
|
|
3085
3355
|
}
|
|
@@ -3087,7 +3357,8 @@ function printStatsText(result, bySource, byDay) {
|
|
|
3087
3357
|
function describeSource(s) {
|
|
3088
3358
|
const cmd = s.commandTimeReliable ? formatDurationMs(s.commandTimeMs) : "n/a";
|
|
3089
3359
|
const tokens = s.tokensAvailable ? `${formatInt(s.tokens.output)} out tok` : "no tokens";
|
|
3090
|
-
|
|
3360
|
+
const machine = s.machineActiveAvailable ? `, model ${formatDurationMs(s.machineActiveTimeMs)}` : "";
|
|
3361
|
+
return `${s.sessionCount} sessions, ${tokens}, active ${formatDurationMs(s.activeTimeMs)}${machine}, command ${cmd}`;
|
|
3091
3362
|
}
|
|
3092
3363
|
function formatInt(n) {
|
|
3093
3364
|
return n.toLocaleString("en-US");
|
|
@@ -3108,7 +3379,7 @@ async function assertWorkspaceInitialized8(basouRoot) {
|
|
|
3108
3379
|
try {
|
|
3109
3380
|
await assertBasouRootSafe10(basouRoot);
|
|
3110
3381
|
} catch (error) {
|
|
3111
|
-
if (
|
|
3382
|
+
if (findErrorCode9(error, "ENOENT")) {
|
|
3112
3383
|
throw new Error("Workspace not initialized. Run 'basou init' first.");
|
|
3113
3384
|
}
|
|
3114
3385
|
throw error;
|
|
@@ -3120,7 +3391,7 @@ import {
|
|
|
3120
3391
|
assertBasouRootSafe as assertBasouRootSafe11,
|
|
3121
3392
|
basouPaths as basouPaths11,
|
|
3122
3393
|
buildStatusSnapshot,
|
|
3123
|
-
findErrorCode as
|
|
3394
|
+
findErrorCode as findErrorCode10,
|
|
3124
3395
|
readManifest as readManifest6,
|
|
3125
3396
|
resolveRepositoryRoot as resolveRepositoryRoot12,
|
|
3126
3397
|
writeStatus
|
|
@@ -3145,7 +3416,7 @@ async function doRunStatus(options, ctx) {
|
|
|
3145
3416
|
try {
|
|
3146
3417
|
await assertBasouRootSafe11(paths.root);
|
|
3147
3418
|
} catch (error) {
|
|
3148
|
-
if (
|
|
3419
|
+
if (findErrorCode10(error, "ENOENT")) {
|
|
3149
3420
|
throw new Error("Workspace not initialized. Run 'basou init' first.");
|
|
3150
3421
|
}
|
|
3151
3422
|
throw error;
|
|
@@ -3154,7 +3425,7 @@ async function doRunStatus(options, ctx) {
|
|
|
3154
3425
|
try {
|
|
3155
3426
|
manifest = await readManifest6(paths);
|
|
3156
3427
|
} catch (error) {
|
|
3157
|
-
if (
|
|
3428
|
+
if (findErrorCode10(error, "ENOENT")) {
|
|
3158
3429
|
throw new Error("Workspace not initialized. Run 'basou init' first.");
|
|
3159
3430
|
}
|
|
3160
3431
|
throw new Error("Failed to read workspace manifest", { cause: error });
|
|
@@ -3191,7 +3462,7 @@ async function resolveRepositoryRootForStatus(cwd) {
|
|
|
3191
3462
|
|
|
3192
3463
|
// src/commands/task.ts
|
|
3193
3464
|
import { readFile as readFile3 } from "fs/promises";
|
|
3194
|
-
import { join as
|
|
3465
|
+
import { join as join7 } from "path";
|
|
3195
3466
|
import {
|
|
3196
3467
|
archiveTask,
|
|
3197
3468
|
assertBasouRootSafe as assertBasouRootSafe12,
|
|
@@ -3200,7 +3471,7 @@ import {
|
|
|
3200
3471
|
deleteTask,
|
|
3201
3472
|
editTask,
|
|
3202
3473
|
enumerateArchivedTaskIds,
|
|
3203
|
-
findErrorCode as
|
|
3474
|
+
findErrorCode as findErrorCode11,
|
|
3204
3475
|
loadSessionEntries as loadSessionEntries2,
|
|
3205
3476
|
loadTaskEntries,
|
|
3206
3477
|
prefixedUlid as prefixedUlid5,
|
|
@@ -3218,7 +3489,7 @@ import {
|
|
|
3218
3489
|
TaskWriteAfterEventError,
|
|
3219
3490
|
updateTaskStatusWithEvent
|
|
3220
3491
|
} from "@basou/core";
|
|
3221
|
-
import { InvalidArgumentError as
|
|
3492
|
+
import { InvalidArgumentError as InvalidArgumentError4 } from "commander";
|
|
3222
3493
|
var STATUS_VALUES3 = TaskStatusSchema.options;
|
|
3223
3494
|
function registerTaskCommand(program2) {
|
|
3224
3495
|
const task = program2.command("task").description("Manage Basou tasks (purpose units that span sessions)");
|
|
@@ -3518,7 +3789,7 @@ async function doRunTaskShow(idInput, options, ctx) {
|
|
|
3518
3789
|
const events = [];
|
|
3519
3790
|
const linkedSessionIds = new Set(doc.task.task.linked_sessions);
|
|
3520
3791
|
for (const s of sessions) {
|
|
3521
|
-
const sessionDir =
|
|
3792
|
+
const sessionDir = join7(paths.sessions, s.sessionId);
|
|
3522
3793
|
try {
|
|
3523
3794
|
for await (const ev of replayEvents2(sessionDir, {
|
|
3524
3795
|
onWarning: (w) => printReplayWarning(w, s.sessionId)
|
|
@@ -4147,20 +4418,20 @@ async function readSingleLineFromStdin() {
|
|
|
4147
4418
|
}
|
|
4148
4419
|
function parseTitle2(raw) {
|
|
4149
4420
|
if (raw.length === 0) {
|
|
4150
|
-
throw new
|
|
4421
|
+
throw new InvalidArgumentError4("Title must not be empty");
|
|
4151
4422
|
}
|
|
4152
4423
|
return raw;
|
|
4153
4424
|
}
|
|
4154
4425
|
function parseLabel(raw) {
|
|
4155
4426
|
if (raw.length === 0) {
|
|
4156
|
-
throw new
|
|
4427
|
+
throw new InvalidArgumentError4("Label must not be empty");
|
|
4157
4428
|
}
|
|
4158
4429
|
return raw;
|
|
4159
4430
|
}
|
|
4160
4431
|
function parseInitialTaskStatus(raw) {
|
|
4161
4432
|
const result = TaskStatusSchema.safeParse(raw);
|
|
4162
4433
|
if (!result.success) {
|
|
4163
|
-
throw new
|
|
4434
|
+
throw new InvalidArgumentError4(
|
|
4164
4435
|
`Initial task status must be one of: ${STATUS_VALUES3.join(", ")}`
|
|
4165
4436
|
);
|
|
4166
4437
|
}
|
|
@@ -4169,7 +4440,7 @@ function parseInitialTaskStatus(raw) {
|
|
|
4169
4440
|
var ISO_DATE_RE = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:\d{2})$/;
|
|
4170
4441
|
function parseIsoTimestampOption(raw) {
|
|
4171
4442
|
if (!ISO_DATE_RE.test(raw) || Number.isNaN(Date.parse(raw))) {
|
|
4172
|
-
throw new
|
|
4443
|
+
throw new InvalidArgumentError4(
|
|
4173
4444
|
"Invalid --completed-at value; expected ISO-8601 timestamp like 2026-05-10T12:34:56+09:00"
|
|
4174
4445
|
);
|
|
4175
4446
|
}
|
|
@@ -4178,7 +4449,7 @@ function parseIsoTimestampOption(raw) {
|
|
|
4178
4449
|
function parseTaskStatusFilter(raw) {
|
|
4179
4450
|
const result = TaskStatusSchema.safeParse(raw);
|
|
4180
4451
|
if (!result.success) {
|
|
4181
|
-
throw new
|
|
4452
|
+
throw new InvalidArgumentError4(
|
|
4182
4453
|
`Invalid task status: ${raw}. Valid values: ${STATUS_VALUES3.join(", ")}`
|
|
4183
4454
|
);
|
|
4184
4455
|
}
|
|
@@ -4193,14 +4464,14 @@ function parseTaskStatusPositional(raw) {
|
|
|
4193
4464
|
}
|
|
4194
4465
|
function parseDescriptionOption(raw) {
|
|
4195
4466
|
if (raw.length === 0) {
|
|
4196
|
-
throw new
|
|
4467
|
+
throw new InvalidArgumentError4("Description must not be empty");
|
|
4197
4468
|
}
|
|
4198
4469
|
return raw;
|
|
4199
4470
|
}
|
|
4200
4471
|
function parsePositiveInt2(raw) {
|
|
4201
4472
|
const n = Number.parseInt(raw, 10);
|
|
4202
4473
|
if (!Number.isInteger(n) || n < 1 || raw.trim() !== String(n)) {
|
|
4203
|
-
throw new
|
|
4474
|
+
throw new InvalidArgumentError4(`Invalid number: ${raw}`);
|
|
4204
4475
|
}
|
|
4205
4476
|
return n;
|
|
4206
4477
|
}
|
|
@@ -4208,10 +4479,10 @@ async function readDescriptionFile(path) {
|
|
|
4208
4479
|
try {
|
|
4209
4480
|
return await readFile3(path, "utf8");
|
|
4210
4481
|
} catch (error) {
|
|
4211
|
-
if (
|
|
4482
|
+
if (findErrorCode11(error, "ENOENT")) {
|
|
4212
4483
|
throw new Error("Description source not found", { cause: error });
|
|
4213
4484
|
}
|
|
4214
|
-
if (
|
|
4485
|
+
if (findErrorCode11(error, "EISDIR")) {
|
|
4215
4486
|
throw new Error("Description source is not a file", { cause: error });
|
|
4216
4487
|
}
|
|
4217
4488
|
throw new Error("Failed to read description source", { cause: error });
|
|
@@ -4234,7 +4505,7 @@ async function assertWorkspaceInitialized9(basouRoot) {
|
|
|
4234
4505
|
try {
|
|
4235
4506
|
await assertBasouRootSafe12(basouRoot);
|
|
4236
4507
|
} catch (error) {
|
|
4237
|
-
if (
|
|
4508
|
+
if (findErrorCode11(error, "ENOENT")) {
|
|
4238
4509
|
throw new Error("Workspace not initialized. Run 'basou init' first.");
|
|
4239
4510
|
}
|
|
4240
4511
|
throw error;
|
|
@@ -4322,16 +4593,16 @@ function maxLen3(values, floor) {
|
|
|
4322
4593
|
|
|
4323
4594
|
// src/commands/view.ts
|
|
4324
4595
|
import { spawn } from "child_process";
|
|
4325
|
-
import { assertBasouRootSafe as assertBasouRootSafe13, basouPaths as basouPaths13, findErrorCode as
|
|
4326
|
-
import { InvalidArgumentError as
|
|
4596
|
+
import { assertBasouRootSafe as assertBasouRootSafe13, basouPaths as basouPaths13, findErrorCode as findErrorCode13, resolveRepositoryRoot as resolveRepositoryRoot14 } from "@basou/core";
|
|
4597
|
+
import { InvalidArgumentError as InvalidArgumentError5 } from "commander";
|
|
4327
4598
|
|
|
4328
4599
|
// src/lib/view-server.ts
|
|
4329
4600
|
import { createServer } from "http";
|
|
4330
|
-
import { join as
|
|
4601
|
+
import { join as join8 } from "path";
|
|
4331
4602
|
import {
|
|
4332
4603
|
computeWorkStats as computeWorkStats2,
|
|
4333
4604
|
enumerateApprovals as enumerateApprovals2,
|
|
4334
|
-
findErrorCode as
|
|
4605
|
+
findErrorCode as findErrorCode12,
|
|
4335
4606
|
isLazyExpired as isLazyExpired2,
|
|
4336
4607
|
loadApproval as loadApproval2,
|
|
4337
4608
|
loadSessionEntries as loadSessionEntries3,
|
|
@@ -4396,7 +4667,7 @@ var VIEW_HTML = `<!doctype html>
|
|
|
4396
4667
|
<body>
|
|
4397
4668
|
<header>
|
|
4398
4669
|
<h1>basou view</h1>
|
|
4399
|
-
<input type="text" id="project" placeholder="
|
|
4670
|
+
<input type="text" id="project" placeholder="source root (optional override)" />
|
|
4400
4671
|
<button class="primary" id="btn-refresh">Refresh all</button>
|
|
4401
4672
|
<button id="btn-import-claude">Import claude-code</button>
|
|
4402
4673
|
<button id="btn-import-codex">Import codex</button>
|
|
@@ -4550,7 +4821,10 @@ var VIEW_HTML = `<!doctype html>
|
|
|
4550
4821
|
detail.appendChild(el('p', { class: 'muted', text: 'Workspace not initialized.' }));
|
|
4551
4822
|
return;
|
|
4552
4823
|
}
|
|
4553
|
-
|
|
4824
|
+
// Leave the project field empty by default so refresh / import use the
|
|
4825
|
+
// manifest's import.source_roots (then the repo root) -- pre-filling the
|
|
4826
|
+
// repo root here would send it as an explicit --project and silently
|
|
4827
|
+
// override multi-root source roots. The field is an optional override.
|
|
4554
4828
|
state.repoRoot = d.repoRoot || '';
|
|
4555
4829
|
detail.appendChild(el('p', {}, [
|
|
4556
4830
|
el('strong', { text: d.workspace.name }), ' ',
|
|
@@ -4620,6 +4894,10 @@ var VIEW_HTML = `<!doctype html>
|
|
|
4620
4894
|
if (t.activeTimeMs !== t.billableActiveTimeMs) {
|
|
4621
4895
|
timeRows.push(kvrow('summed', fmtDur(t.activeTimeMs) + ' (concurrent sessions double-counted)'));
|
|
4622
4896
|
}
|
|
4897
|
+
if (t.machineActiveAvailable) {
|
|
4898
|
+
var machineSessions = sessions.filter(function (s) { return s.availability && s.availability.machineActive; }).length;
|
|
4899
|
+
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)'));
|
|
4900
|
+
}
|
|
4623
4901
|
timeRows.push(kvrow('span', fmtDur(t.sessionSpanMs) + (t.openSessionCount > 0 ? ' (' + t.openSessionCount + ' open)' : '')));
|
|
4624
4902
|
timeRows.push(kvrow('command', fmtDur(t.commandTimeMs) + (t.commandTimeReliable ? '' : ' (some sessions report 0)')));
|
|
4625
4903
|
detail.appendChild(el('table', { class: 'kv' }, [el('tbody', {}, timeRows)]));
|
|
@@ -4627,16 +4905,18 @@ var VIEW_HTML = `<!doctype html>
|
|
|
4627
4905
|
detail.appendChild(el('h3', { text: 'By source' }));
|
|
4628
4906
|
d.bySource.forEach(function (s) {
|
|
4629
4907
|
var cmd = s.commandTimeReliable ? fmtDur(s.commandTimeMs) : 'n/a';
|
|
4908
|
+
var machine = s.machineActiveAvailable ? ', model ' + fmtDur(s.machineActiveTimeMs) : '';
|
|
4630
4909
|
detail.appendChild(el('div', { class: 'row' }, [
|
|
4631
|
-
el('span', { text: s.sourceKind + ': ' + s.sessionCount + ' sessions, ' + numfmt(s.tokens.output) + ' out tok, active ' + fmtDur(s.activeTimeMs) + ', command ' + cmd })
|
|
4910
|
+
el('span', { text: s.sourceKind + ': ' + s.sessionCount + ' sessions, ' + numfmt(s.tokens.output) + ' out tok, active ' + fmtDur(s.activeTimeMs) + machine + ', command ' + cmd })
|
|
4632
4911
|
]));
|
|
4633
4912
|
});
|
|
4634
4913
|
}
|
|
4635
4914
|
if (d.byDay && d.byDay.length) {
|
|
4636
4915
|
detail.appendChild(el('h3', { text: 'By day (billable time x volume)' }));
|
|
4637
4916
|
d.byDay.forEach(function (day) {
|
|
4917
|
+
var dayMachine = day.machineActiveTimeMs > 0 ? ' (model ' + fmtDur(day.machineActiveTimeMs) + ')' : '';
|
|
4638
4918
|
detail.appendChild(el('div', { class: 'row' }, [
|
|
4639
|
-
el('span', { text: day.date + ': ' + fmtDur(day.billableActiveTimeMs) + ' active, ' + numfmt(day.tokens.output) + ' out tok, ' + day.commandCount + ' cmd / ' + day.fileChangedCount + ' files / ' + day.decisionCount + ' dec' })
|
|
4919
|
+
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' })
|
|
4640
4920
|
]));
|
|
4641
4921
|
});
|
|
4642
4922
|
}
|
|
@@ -4795,7 +5075,7 @@ function startViewServer(opts) {
|
|
|
4795
5075
|
};
|
|
4796
5076
|
let boundPort = port;
|
|
4797
5077
|
const getPort = () => boundPort;
|
|
4798
|
-
return new Promise((
|
|
5078
|
+
return new Promise((resolve3, reject) => {
|
|
4799
5079
|
const server = createServer((req, res) => {
|
|
4800
5080
|
handleRequest(req, res, deps, getPort, runExclusive).catch((error) => {
|
|
4801
5081
|
sendError(res, error instanceof HttpError ? error.status : 500, pathlessMessage(error));
|
|
@@ -4806,7 +5086,7 @@ function startViewServer(opts) {
|
|
|
4806
5086
|
const address = server.address();
|
|
4807
5087
|
boundPort = isAddressInfo(address) ? address.port : port;
|
|
4808
5088
|
server.off("error", reject);
|
|
4809
|
-
|
|
5089
|
+
resolve3({
|
|
4810
5090
|
url: `http://${host}:${boundPort}`,
|
|
4811
5091
|
port: boundPort,
|
|
4812
5092
|
close: () => closeServer(server)
|
|
@@ -4818,8 +5098,8 @@ function isAddressInfo(value) {
|
|
|
4818
5098
|
return value !== null && typeof value === "object";
|
|
4819
5099
|
}
|
|
4820
5100
|
function closeServer(server) {
|
|
4821
|
-
return new Promise((
|
|
4822
|
-
server.close(() =>
|
|
5101
|
+
return new Promise((resolve3) => {
|
|
5102
|
+
server.close(() => resolve3());
|
|
4823
5103
|
server.closeAllConnections();
|
|
4824
5104
|
});
|
|
4825
5105
|
}
|
|
@@ -4924,7 +5204,7 @@ async function overview(deps) {
|
|
|
4924
5204
|
try {
|
|
4925
5205
|
manifest = await readManifest8(deps.paths);
|
|
4926
5206
|
} catch (error) {
|
|
4927
|
-
if (
|
|
5207
|
+
if (findErrorCode12(error, "ENOENT")) {
|
|
4928
5208
|
return { initialized: false, repoRoot: deps.repoRoot };
|
|
4929
5209
|
}
|
|
4930
5210
|
throw error;
|
|
@@ -4979,7 +5259,7 @@ async function sessionDetail(deps, sessionId) {
|
|
|
4979
5259
|
throw error;
|
|
4980
5260
|
}
|
|
4981
5261
|
try {
|
|
4982
|
-
const events = await readAllEvents2(
|
|
5262
|
+
const events = await readAllEvents2(join8(deps.paths.sessions, sessionId));
|
|
4983
5263
|
return { session, events };
|
|
4984
5264
|
} catch {
|
|
4985
5265
|
return { session, events: [], degraded: true };
|
|
@@ -5034,11 +5314,16 @@ async function handoffView(deps) {
|
|
|
5034
5314
|
}
|
|
5035
5315
|
function readActionOptions(body) {
|
|
5036
5316
|
const options = {};
|
|
5037
|
-
|
|
5317
|
+
const project = normalizeProject(body.project);
|
|
5318
|
+
if (project.length > 0) options.project = project;
|
|
5038
5319
|
if (body.force === true) options.force = true;
|
|
5039
5320
|
if (body.dryRun === true) options.dryRun = true;
|
|
5040
5321
|
return options;
|
|
5041
5322
|
}
|
|
5323
|
+
function normalizeProject(value) {
|
|
5324
|
+
const raw = Array.isArray(value) ? value : [value];
|
|
5325
|
+
return raw.filter((p) => typeof p === "string" && p.length > 0);
|
|
5326
|
+
}
|
|
5042
5327
|
function hostAllowed(req, port) {
|
|
5043
5328
|
const host = req.headers.host;
|
|
5044
5329
|
return host === `127.0.0.1:${port}` || host === `localhost:${port}`;
|
|
@@ -5114,7 +5399,7 @@ var DEFAULT_PORT = 4319;
|
|
|
5114
5399
|
function parsePort(value) {
|
|
5115
5400
|
const port = Number.parseInt(value, 10);
|
|
5116
5401
|
if (!Number.isInteger(port) || port < 1 || port > 65535) {
|
|
5117
|
-
throw new
|
|
5402
|
+
throw new InvalidArgumentError5("Port must be an integer between 1 and 65535.");
|
|
5118
5403
|
}
|
|
5119
5404
|
return port;
|
|
5120
5405
|
}
|
|
@@ -5166,7 +5451,7 @@ async function startListening(port, deps) {
|
|
|
5166
5451
|
try {
|
|
5167
5452
|
return await startViewServer({ port, deps });
|
|
5168
5453
|
} catch (error) {
|
|
5169
|
-
if (
|
|
5454
|
+
if (findErrorCode13(error, "EADDRINUSE")) {
|
|
5170
5455
|
throw new Error(`Port ${port} is already in use. Pass --port <n> to choose another.`, {
|
|
5171
5456
|
cause: error
|
|
5172
5457
|
});
|
|
@@ -5189,7 +5474,7 @@ function openInBrowser(url, override) {
|
|
|
5189
5474
|
}
|
|
5190
5475
|
}
|
|
5191
5476
|
function waitForShutdown(signal) {
|
|
5192
|
-
return new Promise((
|
|
5477
|
+
return new Promise((resolve3) => {
|
|
5193
5478
|
const cleanup = () => {
|
|
5194
5479
|
process.off("SIGINT", onSignal);
|
|
5195
5480
|
process.off("SIGTERM", onSignal);
|
|
@@ -5197,18 +5482,18 @@ function waitForShutdown(signal) {
|
|
|
5197
5482
|
};
|
|
5198
5483
|
const onSignal = () => {
|
|
5199
5484
|
cleanup();
|
|
5200
|
-
|
|
5485
|
+
resolve3();
|
|
5201
5486
|
};
|
|
5202
5487
|
const onAbort = () => {
|
|
5203
5488
|
cleanup();
|
|
5204
|
-
|
|
5489
|
+
resolve3();
|
|
5205
5490
|
};
|
|
5206
5491
|
process.on("SIGINT", onSignal);
|
|
5207
5492
|
process.on("SIGTERM", onSignal);
|
|
5208
5493
|
if (signal !== void 0) {
|
|
5209
5494
|
if (signal.aborted) {
|
|
5210
5495
|
cleanup();
|
|
5211
|
-
|
|
5496
|
+
resolve3();
|
|
5212
5497
|
return;
|
|
5213
5498
|
}
|
|
5214
5499
|
signal.addEventListener("abort", onAbort);
|
|
@@ -5231,7 +5516,7 @@ async function assertWorkspaceInitialized10(basouRoot) {
|
|
|
5231
5516
|
try {
|
|
5232
5517
|
await assertBasouRootSafe13(basouRoot);
|
|
5233
5518
|
} catch (error) {
|
|
5234
|
-
if (
|
|
5519
|
+
if (findErrorCode13(error, "ENOENT")) {
|
|
5235
5520
|
throw new Error("Workspace not initialized. Run 'basou init' first.");
|
|
5236
5521
|
}
|
|
5237
5522
|
throw error;
|