@basou/cli 0.19.0 → 0.20.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 +144 -17
- package/dist/index.js.map +1 -1
- package/dist/program.js +144 -17
- package/dist/program.js.map +1 -1
- package/package.json +2 -2
package/dist/index.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(program2) {
|
|
2044
|
+
const hook = program2.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 });
|
|
@@ -4887,7 +5013,7 @@ function renderProjectRename(result) {
|
|
|
4887
5013
|
}
|
|
4888
5014
|
|
|
4889
5015
|
// src/commands/protocol.ts
|
|
4890
|
-
import { readFile as
|
|
5016
|
+
import { readFile as readFile4 } from "fs/promises";
|
|
4891
5017
|
import {
|
|
4892
5018
|
PROTOCOL_END,
|
|
4893
5019
|
PROTOCOL_START,
|
|
@@ -4898,7 +5024,7 @@ import {
|
|
|
4898
5024
|
|
|
4899
5025
|
// src/lib/durable-write.ts
|
|
4900
5026
|
import { randomUUID } from "crypto";
|
|
4901
|
-
import { lstat, open, rename, stat as
|
|
5027
|
+
import { lstat, open as open2, rename, stat as stat4, unlink as unlink2 } from "fs/promises";
|
|
4902
5028
|
import { basename as basename5, dirname as dirname3, join as join8 } from "path";
|
|
4903
5029
|
async function assertNotSymlink(targetPath) {
|
|
4904
5030
|
try {
|
|
@@ -4918,7 +5044,7 @@ async function writeFileDurable(targetPath, content) {
|
|
|
4918
5044
|
const tmpPath = join8(dir, `.${basename5(targetPath)}.tmp.${randomUUID()}`);
|
|
4919
5045
|
let mode = 420;
|
|
4920
5046
|
try {
|
|
4921
|
-
mode = (await
|
|
5047
|
+
mode = (await stat4(targetPath)).mode & 511;
|
|
4922
5048
|
} catch (error) {
|
|
4923
5049
|
if (!(error instanceof Error && error.code === "ENOENT")) {
|
|
4924
5050
|
throw error;
|
|
@@ -4926,7 +5052,7 @@ async function writeFileDurable(targetPath, content) {
|
|
|
4926
5052
|
}
|
|
4927
5053
|
let handle;
|
|
4928
5054
|
try {
|
|
4929
|
-
handle = await
|
|
5055
|
+
handle = await open2(tmpPath, "wx", mode);
|
|
4930
5056
|
await handle.writeFile(content, "utf8");
|
|
4931
5057
|
await handle.chmod(mode);
|
|
4932
5058
|
await handle.sync();
|
|
@@ -4939,7 +5065,7 @@ async function writeFileDurable(targetPath, content) {
|
|
|
4939
5065
|
throw error;
|
|
4940
5066
|
}
|
|
4941
5067
|
try {
|
|
4942
|
-
const dirHandle = await
|
|
5068
|
+
const dirHandle = await open2(dir, "r");
|
|
4943
5069
|
try {
|
|
4944
5070
|
await dirHandle.sync();
|
|
4945
5071
|
} finally {
|
|
@@ -5070,7 +5196,7 @@ async function readProtocolSources(entries) {
|
|
|
5070
5196
|
for (const entry of entries) {
|
|
5071
5197
|
let content;
|
|
5072
5198
|
try {
|
|
5073
|
-
content = await
|
|
5199
|
+
content = await readFile4(entry.source, "utf8");
|
|
5074
5200
|
} catch (error) {
|
|
5075
5201
|
if (error instanceof Error && error.code === "ENOENT") {
|
|
5076
5202
|
throw new Error(
|
|
@@ -5210,7 +5336,7 @@ import { assertBasouRootSafe as assertBasouRootSafe9, basouPaths as basouPaths11
|
|
|
5210
5336
|
import { InvalidArgumentError as InvalidArgumentError3 } from "commander";
|
|
5211
5337
|
|
|
5212
5338
|
// src/commands/refresh-watch.ts
|
|
5213
|
-
import { readdir as readdir2, stat as
|
|
5339
|
+
import { readdir as readdir2, stat as stat5 } from "fs/promises";
|
|
5214
5340
|
import { homedir as homedir7 } from "os";
|
|
5215
5341
|
import { join as join10 } from "path";
|
|
5216
5342
|
import { findErrorCode as findErrorCode8 } from "@basou/core";
|
|
@@ -5239,7 +5365,7 @@ async function scanSourceLogs(roots) {
|
|
|
5239
5365
|
await walk(full);
|
|
5240
5366
|
} else if (entry.isFile() && entry.name.endsWith(".jsonl")) {
|
|
5241
5367
|
try {
|
|
5242
|
-
const info = await
|
|
5368
|
+
const info = await stat5(full);
|
|
5243
5369
|
out.set(full, { mtimeMs: info.mtimeMs, size: info.size });
|
|
5244
5370
|
} catch (error) {
|
|
5245
5371
|
if (findErrorCode8(error, "ENOENT")) continue;
|
|
@@ -6157,7 +6283,7 @@ async function resolveRepositoryRootForRun(cwd) {
|
|
|
6157
6283
|
}
|
|
6158
6284
|
|
|
6159
6285
|
// src/commands/session.ts
|
|
6160
|
-
import { readFile as
|
|
6286
|
+
import { readFile as readFile5 } from "fs/promises";
|
|
6161
6287
|
import { basename as basename6, isAbsolute as isAbsolute6, join as join12, relative as relative3 } from "path";
|
|
6162
6288
|
import {
|
|
6163
6289
|
acquireLock as acquireLock6,
|
|
@@ -6602,7 +6728,7 @@ async function doRunSessionImport(options, ctx) {
|
|
|
6602
6728
|
}
|
|
6603
6729
|
async function readInputFile(path) {
|
|
6604
6730
|
try {
|
|
6605
|
-
return await
|
|
6731
|
+
return await readFile5(path, "utf8");
|
|
6606
6732
|
} catch (error) {
|
|
6607
6733
|
if (findErrorCode11(error, "ENOENT")) {
|
|
6608
6734
|
throw new Error("Import source not found", { cause: error });
|
|
@@ -6719,7 +6845,7 @@ async function doRunSessionNote(sessionIdInput, options, ctx) {
|
|
|
6719
6845
|
}
|
|
6720
6846
|
async function readNoteFile(path) {
|
|
6721
6847
|
try {
|
|
6722
|
-
return await
|
|
6848
|
+
return await readFile5(path, "utf8");
|
|
6723
6849
|
} catch (error) {
|
|
6724
6850
|
if (findErrorCode11(error, "ENOENT")) {
|
|
6725
6851
|
throw new Error("Note source not found", { cause: error });
|
|
@@ -7026,7 +7152,7 @@ async function resolveRepositoryRootForStatus(cwd) {
|
|
|
7026
7152
|
}
|
|
7027
7153
|
|
|
7028
7154
|
// src/commands/task.ts
|
|
7029
|
-
import { readFile as
|
|
7155
|
+
import { readFile as readFile6 } from "fs/promises";
|
|
7030
7156
|
import { join as join13 } from "path";
|
|
7031
7157
|
import {
|
|
7032
7158
|
archiveTask,
|
|
@@ -8042,7 +8168,7 @@ function parsePositiveInt2(raw) {
|
|
|
8042
8168
|
}
|
|
8043
8169
|
async function readDescriptionFile(path) {
|
|
8044
8170
|
try {
|
|
8045
|
-
return await
|
|
8171
|
+
return await readFile6(path, "utf8");
|
|
8046
8172
|
} catch (error) {
|
|
8047
8173
|
if (findErrorCode14(error, "ENOENT")) {
|
|
8048
8174
|
throw new Error("Description source not found", { cause: error });
|
|
@@ -9728,6 +9854,7 @@ function buildProgram() {
|
|
|
9728
9854
|
registerReviewGapsCommand(program2);
|
|
9729
9855
|
registerProjectCommand(program2);
|
|
9730
9856
|
registerProtocolCommand(program2);
|
|
9857
|
+
registerHookCommand(program2);
|
|
9731
9858
|
return program2;
|
|
9732
9859
|
}
|
|
9733
9860
|
|