@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 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 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 });
@@ -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(program2) {
@@ -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 readFile3 } from "fs/promises";
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 stat3, unlink as unlink2 } from "fs/promises";
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 stat3(targetPath)).mode & 511;
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 open(tmpPath, "wx", mode);
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 open(dir, "r");
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 readFile3(entry.source, "utf8");
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 stat4 } from "fs/promises";
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 stat4(full);
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 readFile4 } from "fs/promises";
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 readFile4(path, "utf8");
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 readFile4(path, "utf8");
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 readFile5 } from "fs/promises";
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 readFile5(path, "utf8");
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
- try {
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 resolveRepositoryRoot13,
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 resolveRepositoryRoot13(cwd);
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 resolveRepositoryRoot14
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 resolveRepositoryRoot14(cwd);
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(program2);
9729
9863
  registerProjectCommand(program2);
9730
9864
  registerProtocolCommand(program2);
9865
+ registerHookCommand(program2);
9731
9866
  return program2;
9732
9867
  }
9733
9868