@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/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 readFile2, rm, stat as stat2 } from "fs/promises";
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 stat2(file);
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 stat2(file)).size;
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 readFile2(file);
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 readFile3 } from "fs/promises";
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 stat3, unlink as unlink2 } from "fs/promises";
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 stat3(targetPath)).mode & 511;
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 open(tmpPath, "wx", mode);
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 open(dir, "r");
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 readFile3(entry.source, "utf8");
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 stat4 } from "fs/promises";
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 stat4(full);
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 readFile4 } from "fs/promises";
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 readFile4(path, "utf8");
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 readFile4(path, "utf8");
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 readFile5 } from "fs/promises";
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 readFile5(path, "utf8");
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(program);
9729
9855
  registerProjectCommand(program);
9730
9856
  registerProtocolCommand(program);
9857
+ registerHookCommand(program);
9731
9858
  return program;
9732
9859
  }
9733
9860
  export {