@basou/cli 0.19.0 → 0.21.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 +168 -33
- package/dist/index.js.map +1 -1
- package/dist/program.js +168 -33
- package/dist/program.js.map +1 -1
- package/package.json +2 -2
package/dist/program.js
CHANGED
|
@@ -2033,9 +2033,135 @@ async function assertWorkspaceInitialized4(basouRoot) {
|
|
|
2033
2033
|
}
|
|
2034
2034
|
}
|
|
2035
2035
|
|
|
2036
|
+
// src/commands/hook.ts
|
|
2037
|
+
import { open, readFile as readFile2, stat as stat2 } from "fs/promises";
|
|
2038
|
+
import {
|
|
2039
|
+
DEFAULT_STOP_HOOK_MIN_ACTIONS,
|
|
2040
|
+
evaluateStopHook
|
|
2041
|
+
} from "@basou/core";
|
|
2042
|
+
var MAX_TRANSCRIPT_BYTES = 8 * 1024 * 1024;
|
|
2043
|
+
function registerHookCommand(program) {
|
|
2044
|
+
const hook = program.command("hook").description(
|
|
2045
|
+
"Claude Code hook handlers (read a hook payload on stdin, emit hook JSON on stdout)"
|
|
2046
|
+
);
|
|
2047
|
+
hook.command("stop").description(
|
|
2048
|
+
"Stop-hook: when a substantive session recorded no decisions or next step, emit a non-blocking nudge to capture them. Reads the Stop hook JSON payload on stdin; never blocks and never fails the session."
|
|
2049
|
+
).option(
|
|
2050
|
+
"--min-actions <n>",
|
|
2051
|
+
`Minimum commands+edits before nudging (default ${DEFAULT_STOP_HOOK_MIN_ACTIONS})`
|
|
2052
|
+
).addHelpText("after", HOOK_STOP_HELP).action(async (options) => {
|
|
2053
|
+
const minActions = parseMinActions(options.minActions);
|
|
2054
|
+
await runHookStop(minActions !== void 0 ? { minActions } : {});
|
|
2055
|
+
});
|
|
2056
|
+
}
|
|
2057
|
+
var HOOK_STOP_HELP = `
|
|
2058
|
+
Install as a Claude Code Stop hook in ~/.claude/settings.json:
|
|
2059
|
+
{
|
|
2060
|
+
"hooks": {
|
|
2061
|
+
"Stop": [
|
|
2062
|
+
{ "hooks": [ { "type": "command", "command": "basou hook stop" } ] }
|
|
2063
|
+
]
|
|
2064
|
+
}
|
|
2065
|
+
}
|
|
2066
|
+
|
|
2067
|
+
On every turn end basou inspects the session transcript. If the session did
|
|
2068
|
+
substantive work (>= ${DEFAULT_STOP_HOOK_MIN_ACTIONS} commands + file edits by default) but ran no capture
|
|
2069
|
+
verb ('basou decision capture' / 'decision record' / 'note'), it emits a
|
|
2070
|
+
non-blocking reminder so the agent can record the why / next step. The reminder
|
|
2071
|
+
continues the conversation (Claude may act on it or stop); it never forces.
|
|
2072
|
+
The 'stop_hook_active' flag is honored so the nudge cannot loop.
|
|
2073
|
+
`;
|
|
2074
|
+
async function runHookStop(options, ctx = {}) {
|
|
2075
|
+
try {
|
|
2076
|
+
await doRunHookStop(options, ctx);
|
|
2077
|
+
} catch {
|
|
2078
|
+
}
|
|
2079
|
+
}
|
|
2080
|
+
async function doRunHookStop(options, ctx) {
|
|
2081
|
+
const readStdin = ctx.readStdin ?? defaultReadStdin;
|
|
2082
|
+
const readTranscript = ctx.readTranscript ?? readTranscriptBounded;
|
|
2083
|
+
const write = ctx.write ?? ((text) => void process.stdout.write(text));
|
|
2084
|
+
const raw = await readStdin();
|
|
2085
|
+
if (raw.trim().length === 0) return;
|
|
2086
|
+
let payload;
|
|
2087
|
+
try {
|
|
2088
|
+
payload = JSON.parse(raw);
|
|
2089
|
+
} catch {
|
|
2090
|
+
return;
|
|
2091
|
+
}
|
|
2092
|
+
if (typeof payload !== "object" || payload === null) return;
|
|
2093
|
+
const fields = payload;
|
|
2094
|
+
if (fields.stop_hook_active === true) return;
|
|
2095
|
+
const transcriptPath = typeof fields.transcript_path === "string" ? fields.transcript_path : "";
|
|
2096
|
+
if (transcriptPath.length === 0) return;
|
|
2097
|
+
let transcript;
|
|
2098
|
+
try {
|
|
2099
|
+
transcript = await readTranscript(transcriptPath);
|
|
2100
|
+
} catch {
|
|
2101
|
+
return;
|
|
2102
|
+
}
|
|
2103
|
+
const records = parseTranscript(transcript);
|
|
2104
|
+
const evaluation = evaluateStopHook({
|
|
2105
|
+
records,
|
|
2106
|
+
// stop_hook_active was already handled by the early return above.
|
|
2107
|
+
stopHookActive: false,
|
|
2108
|
+
...options.minActions !== void 0 ? { minActions: options.minActions } : {}
|
|
2109
|
+
});
|
|
2110
|
+
if (evaluation.kind !== "nudge") return;
|
|
2111
|
+
write(
|
|
2112
|
+
`${JSON.stringify({
|
|
2113
|
+
hookSpecificOutput: {
|
|
2114
|
+
hookEventName: "Stop",
|
|
2115
|
+
additionalContext: evaluation.additionalContext
|
|
2116
|
+
}
|
|
2117
|
+
})}
|
|
2118
|
+
`
|
|
2119
|
+
);
|
|
2120
|
+
}
|
|
2121
|
+
function parseTranscript(transcript) {
|
|
2122
|
+
const records = [];
|
|
2123
|
+
for (const line of transcript.split(/\r?\n/)) {
|
|
2124
|
+
if (line.trim().length === 0) continue;
|
|
2125
|
+
try {
|
|
2126
|
+
const parsed = JSON.parse(line);
|
|
2127
|
+
if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)) {
|
|
2128
|
+
records.push(parsed);
|
|
2129
|
+
}
|
|
2130
|
+
} catch {
|
|
2131
|
+
}
|
|
2132
|
+
}
|
|
2133
|
+
return records;
|
|
2134
|
+
}
|
|
2135
|
+
async function defaultReadStdin() {
|
|
2136
|
+
if (process.stdin.isTTY === true) return "";
|
|
2137
|
+
const chunks = [];
|
|
2138
|
+
for await (const chunk of process.stdin) {
|
|
2139
|
+
chunks.push(chunk);
|
|
2140
|
+
}
|
|
2141
|
+
return Buffer.concat(chunks).toString("utf8");
|
|
2142
|
+
}
|
|
2143
|
+
async function readTranscriptBounded(path, maxBytes = MAX_TRANSCRIPT_BYTES) {
|
|
2144
|
+
const { size } = await stat2(path);
|
|
2145
|
+
if (size <= maxBytes) return readFile2(path, "utf8");
|
|
2146
|
+
const handle = await open(path, "r");
|
|
2147
|
+
try {
|
|
2148
|
+
const buffer = Buffer.alloc(maxBytes);
|
|
2149
|
+
const { bytesRead } = await handle.read(buffer, 0, maxBytes, size - maxBytes);
|
|
2150
|
+
const text = buffer.subarray(0, bytesRead).toString("utf8");
|
|
2151
|
+
const firstNewline = text.indexOf("\n");
|
|
2152
|
+
return firstNewline >= 0 ? text.slice(firstNewline + 1) : text;
|
|
2153
|
+
} finally {
|
|
2154
|
+
await handle.close();
|
|
2155
|
+
}
|
|
2156
|
+
}
|
|
2157
|
+
function parseMinActions(raw) {
|
|
2158
|
+
if (raw === void 0 || !/^\d+$/.test(raw)) return void 0;
|
|
2159
|
+
return Number(raw);
|
|
2160
|
+
}
|
|
2161
|
+
|
|
2036
2162
|
// src/commands/import.ts
|
|
2037
2163
|
import { createReadStream } from "fs";
|
|
2038
|
-
import { readdir, readFile as
|
|
2164
|
+
import { readdir, readFile as readFile3, rm, stat as stat3 } from "fs/promises";
|
|
2039
2165
|
import { homedir as homedir4 } from "os";
|
|
2040
2166
|
import { basename as basename2, dirname, join as join5, resolve as resolve4 } from "path";
|
|
2041
2167
|
import { createInterface } from "readline";
|
|
@@ -2433,7 +2559,7 @@ async function selectTranscriptFiles(projectsRoot, projectPaths, options) {
|
|
|
2433
2559
|
}
|
|
2434
2560
|
async function pathExists(file) {
|
|
2435
2561
|
try {
|
|
2436
|
-
await
|
|
2562
|
+
await stat3(file);
|
|
2437
2563
|
return true;
|
|
2438
2564
|
} catch (error) {
|
|
2439
2565
|
if (findErrorCode5(error, "ENOENT")) return false;
|
|
@@ -2442,7 +2568,7 @@ async function pathExists(file) {
|
|
|
2442
2568
|
}
|
|
2443
2569
|
async function statSize(file) {
|
|
2444
2570
|
try {
|
|
2445
|
-
return (await
|
|
2571
|
+
return (await stat3(file)).size;
|
|
2446
2572
|
} catch (error) {
|
|
2447
2573
|
if (findErrorCode5(error, "ENOENT")) return void 0;
|
|
2448
2574
|
throw error;
|
|
@@ -2528,7 +2654,7 @@ async function readFirstLine(file) {
|
|
|
2528
2654
|
async function readJsonlRecords(file) {
|
|
2529
2655
|
let buffer;
|
|
2530
2656
|
try {
|
|
2531
|
-
buffer = await
|
|
2657
|
+
buffer = await readFile3(file);
|
|
2532
2658
|
} catch (error) {
|
|
2533
2659
|
if (findErrorCode5(error, "ENOENT")) {
|
|
2534
2660
|
throw new Error("Source log not found", { cause: error });
|
|
@@ -2759,6 +2885,19 @@ import {
|
|
|
2759
2885
|
resolveSessionId as resolveSessionId2
|
|
2760
2886
|
} from "@basou/core";
|
|
2761
2887
|
import { InvalidArgumentError as InvalidArgumentError2 } from "commander";
|
|
2888
|
+
var NOTE_SUBCOMMAND_LOOKALIKES = /* @__PURE__ */ new Set([
|
|
2889
|
+
"list",
|
|
2890
|
+
"ls",
|
|
2891
|
+
"show",
|
|
2892
|
+
"get",
|
|
2893
|
+
"add",
|
|
2894
|
+
"new",
|
|
2895
|
+
"edit",
|
|
2896
|
+
"rm",
|
|
2897
|
+
"remove",
|
|
2898
|
+
"delete",
|
|
2899
|
+
"help"
|
|
2900
|
+
]);
|
|
2762
2901
|
var LABEL_BODY_MAX = 80;
|
|
2763
2902
|
var LABEL_TRUNCATE_HEAD2 = LABEL_BODY_MAX - 3;
|
|
2764
2903
|
function registerNoteCommand(program) {
|
|
@@ -2784,6 +2923,12 @@ async function doRunNote(body, options, ctx) {
|
|
|
2784
2923
|
if (body.trim().length === 0) {
|
|
2785
2924
|
throw new Error("Note body must not be empty");
|
|
2786
2925
|
}
|
|
2926
|
+
const reserved = body.trim().toLowerCase();
|
|
2927
|
+
if (NOTE_SUBCOMMAND_LOOKALIKES.has(reserved)) {
|
|
2928
|
+
throw new Error(
|
|
2929
|
+
`'basou note' records a free-text note and has no '${body.trim()}' subcommand. To record a note, pass its full text (e.g. \`basou note "<your note>"\`).`
|
|
2930
|
+
);
|
|
2931
|
+
}
|
|
2787
2932
|
const cwd = ctx.cwd ?? process.cwd();
|
|
2788
2933
|
const repositoryRoot = await resolveBasouRootForCommand(cwd, "note");
|
|
2789
2934
|
const paths = basouPaths8(repositoryRoot);
|
|
@@ -4887,7 +5032,7 @@ function renderProjectRename(result) {
|
|
|
4887
5032
|
}
|
|
4888
5033
|
|
|
4889
5034
|
// src/commands/protocol.ts
|
|
4890
|
-
import { readFile as
|
|
5035
|
+
import { readFile as readFile4 } from "fs/promises";
|
|
4891
5036
|
import {
|
|
4892
5037
|
PROTOCOL_END,
|
|
4893
5038
|
PROTOCOL_START,
|
|
@@ -4898,7 +5043,7 @@ import {
|
|
|
4898
5043
|
|
|
4899
5044
|
// src/lib/durable-write.ts
|
|
4900
5045
|
import { randomUUID } from "crypto";
|
|
4901
|
-
import { lstat, open, rename, stat as
|
|
5046
|
+
import { lstat, open as open2, rename, stat as stat4, unlink as unlink2 } from "fs/promises";
|
|
4902
5047
|
import { basename as basename5, dirname as dirname3, join as join8 } from "path";
|
|
4903
5048
|
async function assertNotSymlink(targetPath) {
|
|
4904
5049
|
try {
|
|
@@ -4918,7 +5063,7 @@ async function writeFileDurable(targetPath, content) {
|
|
|
4918
5063
|
const tmpPath = join8(dir, `.${basename5(targetPath)}.tmp.${randomUUID()}`);
|
|
4919
5064
|
let mode = 420;
|
|
4920
5065
|
try {
|
|
4921
|
-
mode = (await
|
|
5066
|
+
mode = (await stat4(targetPath)).mode & 511;
|
|
4922
5067
|
} catch (error) {
|
|
4923
5068
|
if (!(error instanceof Error && error.code === "ENOENT")) {
|
|
4924
5069
|
throw error;
|
|
@@ -4926,7 +5071,7 @@ async function writeFileDurable(targetPath, content) {
|
|
|
4926
5071
|
}
|
|
4927
5072
|
let handle;
|
|
4928
5073
|
try {
|
|
4929
|
-
handle = await
|
|
5074
|
+
handle = await open2(tmpPath, "wx", mode);
|
|
4930
5075
|
await handle.writeFile(content, "utf8");
|
|
4931
5076
|
await handle.chmod(mode);
|
|
4932
5077
|
await handle.sync();
|
|
@@ -4939,7 +5084,7 @@ async function writeFileDurable(targetPath, content) {
|
|
|
4939
5084
|
throw error;
|
|
4940
5085
|
}
|
|
4941
5086
|
try {
|
|
4942
|
-
const dirHandle = await
|
|
5087
|
+
const dirHandle = await open2(dir, "r");
|
|
4943
5088
|
try {
|
|
4944
5089
|
await dirHandle.sync();
|
|
4945
5090
|
} finally {
|
|
@@ -5070,7 +5215,7 @@ async function readProtocolSources(entries) {
|
|
|
5070
5215
|
for (const entry of entries) {
|
|
5071
5216
|
let content;
|
|
5072
5217
|
try {
|
|
5073
|
-
content = await
|
|
5218
|
+
content = await readFile4(entry.source, "utf8");
|
|
5074
5219
|
} catch (error) {
|
|
5075
5220
|
if (error instanceof Error && error.code === "ENOENT") {
|
|
5076
5221
|
throw new Error(
|
|
@@ -5210,7 +5355,7 @@ import { assertBasouRootSafe as assertBasouRootSafe9, basouPaths as basouPaths11
|
|
|
5210
5355
|
import { InvalidArgumentError as InvalidArgumentError3 } from "commander";
|
|
5211
5356
|
|
|
5212
5357
|
// src/commands/refresh-watch.ts
|
|
5213
|
-
import { readdir as readdir2, stat as
|
|
5358
|
+
import { readdir as readdir2, stat as stat5 } from "fs/promises";
|
|
5214
5359
|
import { homedir as homedir7 } from "os";
|
|
5215
5360
|
import { join as join10 } from "path";
|
|
5216
5361
|
import { findErrorCode as findErrorCode8 } from "@basou/core";
|
|
@@ -5239,7 +5384,7 @@ async function scanSourceLogs(roots) {
|
|
|
5239
5384
|
await walk(full);
|
|
5240
5385
|
} else if (entry.isFile() && entry.name.endsWith(".jsonl")) {
|
|
5241
5386
|
try {
|
|
5242
|
-
const info = await
|
|
5387
|
+
const info = await stat5(full);
|
|
5243
5388
|
out.set(full, { mtimeMs: info.mtimeMs, size: info.size });
|
|
5244
5389
|
} catch (error) {
|
|
5245
5390
|
if (findErrorCode8(error, "ENOENT")) continue;
|
|
@@ -6157,7 +6302,7 @@ async function resolveRepositoryRootForRun(cwd) {
|
|
|
6157
6302
|
}
|
|
6158
6303
|
|
|
6159
6304
|
// src/commands/session.ts
|
|
6160
|
-
import { readFile as
|
|
6305
|
+
import { readFile as readFile5 } from "fs/promises";
|
|
6161
6306
|
import { basename as basename6, isAbsolute as isAbsolute6, join as join12, relative as relative3 } from "path";
|
|
6162
6307
|
import {
|
|
6163
6308
|
acquireLock as acquireLock6,
|
|
@@ -6602,7 +6747,7 @@ async function doRunSessionImport(options, ctx) {
|
|
|
6602
6747
|
}
|
|
6603
6748
|
async function readInputFile(path) {
|
|
6604
6749
|
try {
|
|
6605
|
-
return await
|
|
6750
|
+
return await readFile5(path, "utf8");
|
|
6606
6751
|
} catch (error) {
|
|
6607
6752
|
if (findErrorCode11(error, "ENOENT")) {
|
|
6608
6753
|
throw new Error("Import source not found", { cause: error });
|
|
@@ -6719,7 +6864,7 @@ async function doRunSessionNote(sessionIdInput, options, ctx) {
|
|
|
6719
6864
|
}
|
|
6720
6865
|
async function readNoteFile(path) {
|
|
6721
6866
|
try {
|
|
6722
|
-
return await
|
|
6867
|
+
return await readFile5(path, "utf8");
|
|
6723
6868
|
} catch (error) {
|
|
6724
6869
|
if (findErrorCode11(error, "ENOENT")) {
|
|
6725
6870
|
throw new Error("Note source not found", { cause: error });
|
|
@@ -7026,7 +7171,7 @@ async function resolveRepositoryRootForStatus(cwd) {
|
|
|
7026
7171
|
}
|
|
7027
7172
|
|
|
7028
7173
|
// src/commands/task.ts
|
|
7029
|
-
import { readFile as
|
|
7174
|
+
import { readFile as readFile6 } from "fs/promises";
|
|
7030
7175
|
import { join as join13 } from "path";
|
|
7031
7176
|
import {
|
|
7032
7177
|
archiveTask,
|
|
@@ -7047,7 +7192,6 @@ import {
|
|
|
7047
7192
|
reconcileTask,
|
|
7048
7193
|
refreshTaskLinkedSessions,
|
|
7049
7194
|
replayEvents as replayEvents3,
|
|
7050
|
-
resolveRepositoryRoot as resolveRepositoryRoot12,
|
|
7051
7195
|
resolveSessionId as resolveSessionId4,
|
|
7052
7196
|
resolveTaskId as resolveTaskId2,
|
|
7053
7197
|
TaskStatusSchema,
|
|
@@ -8042,7 +8186,7 @@ function parsePositiveInt2(raw) {
|
|
|
8042
8186
|
}
|
|
8043
8187
|
async function readDescriptionFile(path) {
|
|
8044
8188
|
try {
|
|
8045
|
-
return await
|
|
8189
|
+
return await readFile6(path, "utf8");
|
|
8046
8190
|
} catch (error) {
|
|
8047
8191
|
if (findErrorCode14(error, "ENOENT")) {
|
|
8048
8192
|
throw new Error("Description source not found", { cause: error });
|
|
@@ -8054,17 +8198,7 @@ async function readDescriptionFile(path) {
|
|
|
8054
8198
|
}
|
|
8055
8199
|
}
|
|
8056
8200
|
async function resolveRepositoryRootForTask(cwd, subcmd) {
|
|
8057
|
-
|
|
8058
|
-
return await resolveRepositoryRoot12(cwd);
|
|
8059
|
-
} catch (error) {
|
|
8060
|
-
if (error instanceof Error && error.message === "Not a git repository") {
|
|
8061
|
-
throw new Error(
|
|
8062
|
-
`Not a git repository. Run 'git init' first, then re-run 'basou task ${subcmd}'.`,
|
|
8063
|
-
{ cause: error }
|
|
8064
|
-
);
|
|
8065
|
-
}
|
|
8066
|
-
throw error;
|
|
8067
|
-
}
|
|
8201
|
+
return resolveBasouRootForCommand(cwd, `task ${subcmd}`);
|
|
8068
8202
|
}
|
|
8069
8203
|
async function assertWorkspaceInitialized12(basouRoot) {
|
|
8070
8204
|
try {
|
|
@@ -8162,7 +8296,7 @@ import {
|
|
|
8162
8296
|
basouPaths as basouPaths19,
|
|
8163
8297
|
enumerateSessionDirs as enumerateSessionDirs3,
|
|
8164
8298
|
findErrorCode as findErrorCode15,
|
|
8165
|
-
resolveRepositoryRoot as
|
|
8299
|
+
resolveRepositoryRoot as resolveRepositoryRoot12,
|
|
8166
8300
|
resolveSessionId as resolveSessionId5,
|
|
8167
8301
|
verifyEventsChain
|
|
8168
8302
|
} from "@basou/core";
|
|
@@ -8233,7 +8367,7 @@ function renderVerdict(row) {
|
|
|
8233
8367
|
}
|
|
8234
8368
|
async function resolveRepositoryRootForVerify(cwd) {
|
|
8235
8369
|
try {
|
|
8236
|
-
return await
|
|
8370
|
+
return await resolveRepositoryRoot12(cwd);
|
|
8237
8371
|
} catch (error) {
|
|
8238
8372
|
if (error instanceof Error && error.message === "Not a git repository") {
|
|
8239
8373
|
throw new Error("Not a git repository. Run 'git init' first, then re-run 'basou verify'.", {
|
|
@@ -8263,7 +8397,7 @@ import {
|
|
|
8263
8397
|
basouPaths as basouPaths20,
|
|
8264
8398
|
findErrorCode as findErrorCode17,
|
|
8265
8399
|
readManifest as readManifest13,
|
|
8266
|
-
resolveRepositoryRoot as
|
|
8400
|
+
resolveRepositoryRoot as resolveRepositoryRoot13
|
|
8267
8401
|
} from "@basou/core";
|
|
8268
8402
|
import { InvalidArgumentError as InvalidArgumentError7 } from "commander";
|
|
8269
8403
|
|
|
@@ -9679,7 +9813,7 @@ function waitForShutdown(signal) {
|
|
|
9679
9813
|
}
|
|
9680
9814
|
async function resolveRepositoryRootForView(cwd) {
|
|
9681
9815
|
try {
|
|
9682
|
-
return await
|
|
9816
|
+
return await resolveRepositoryRoot13(cwd);
|
|
9683
9817
|
} catch (error) {
|
|
9684
9818
|
if (error instanceof Error && error.message === "Not a git repository") {
|
|
9685
9819
|
throw new Error("Not a git repository. Run 'git init' first, then re-run 'basou view'.", {
|
|
@@ -9728,6 +9862,7 @@ function buildProgram() {
|
|
|
9728
9862
|
registerReviewGapsCommand(program);
|
|
9729
9863
|
registerProjectCommand(program);
|
|
9730
9864
|
registerProtocolCommand(program);
|
|
9865
|
+
registerHookCommand(program);
|
|
9731
9866
|
return program;
|
|
9732
9867
|
}
|
|
9733
9868
|
export {
|