reasonix 0.4.22 → 0.4.24

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
@@ -427,6 +427,174 @@ function resolveTemperatures(budget, custom) {
427
427
  return out;
428
428
  }
429
429
 
430
+ // src/hooks.ts
431
+ import { spawn } from "child_process";
432
+ import { existsSync, readFileSync } from "fs";
433
+ import { homedir } from "os";
434
+ import { join } from "path";
435
+ var HOOK_EVENTS = [
436
+ "PreToolUse",
437
+ "PostToolUse",
438
+ "UserPromptSubmit",
439
+ "Stop"
440
+ ];
441
+ var BLOCKING_EVENTS = /* @__PURE__ */ new Set(["PreToolUse", "UserPromptSubmit"]);
442
+ var DEFAULT_TIMEOUTS_MS = {
443
+ PreToolUse: 5e3,
444
+ UserPromptSubmit: 5e3,
445
+ PostToolUse: 3e4,
446
+ Stop: 3e4
447
+ };
448
+ var HOOK_SETTINGS_FILENAME = "settings.json";
449
+ var HOOK_SETTINGS_DIRNAME = ".reasonix";
450
+ function globalSettingsPath(homeDirOverride) {
451
+ return join(homeDirOverride ?? homedir(), HOOK_SETTINGS_DIRNAME, HOOK_SETTINGS_FILENAME);
452
+ }
453
+ function projectSettingsPath(projectRoot) {
454
+ return join(projectRoot, HOOK_SETTINGS_DIRNAME, HOOK_SETTINGS_FILENAME);
455
+ }
456
+ function readSettingsFile(path) {
457
+ if (!existsSync(path)) return null;
458
+ try {
459
+ const raw = readFileSync(path, "utf8");
460
+ const parsed = JSON.parse(raw);
461
+ if (parsed && typeof parsed === "object") return parsed;
462
+ } catch {
463
+ }
464
+ return null;
465
+ }
466
+ function loadHooks(opts = {}) {
467
+ const out = [];
468
+ if (opts.projectRoot) {
469
+ const projPath = projectSettingsPath(opts.projectRoot);
470
+ const settings2 = readSettingsFile(projPath);
471
+ if (settings2) appendResolved(out, settings2, "project", projPath);
472
+ }
473
+ const globalPath = globalSettingsPath(opts.homeDir);
474
+ const settings = readSettingsFile(globalPath);
475
+ if (settings) appendResolved(out, settings, "global", globalPath);
476
+ return out;
477
+ }
478
+ function appendResolved(out, settings, scope, source) {
479
+ if (!settings.hooks) return;
480
+ for (const event of HOOK_EVENTS) {
481
+ const list = settings.hooks[event];
482
+ if (!Array.isArray(list)) continue;
483
+ for (const cfg of list) {
484
+ if (!cfg || typeof cfg.command !== "string" || cfg.command.trim() === "") continue;
485
+ out.push({ ...cfg, event, scope, source });
486
+ }
487
+ }
488
+ }
489
+ function matchesTool(hook, toolName) {
490
+ if (hook.event !== "PreToolUse" && hook.event !== "PostToolUse") return true;
491
+ const m = hook.match;
492
+ if (!m || m === "*") return true;
493
+ try {
494
+ const re = new RegExp(`^(?:${m})$`);
495
+ return re.test(toolName);
496
+ } catch {
497
+ return false;
498
+ }
499
+ }
500
+ function defaultSpawner(input) {
501
+ return new Promise((resolve7) => {
502
+ const child = spawn(input.command, {
503
+ cwd: input.cwd,
504
+ shell: true,
505
+ stdio: ["pipe", "pipe", "pipe"]
506
+ });
507
+ let stdout = "";
508
+ let stderr = "";
509
+ let timedOut = false;
510
+ const timer = setTimeout(() => {
511
+ timedOut = true;
512
+ child.kill("SIGTERM");
513
+ setTimeout(() => {
514
+ try {
515
+ child.kill("SIGKILL");
516
+ } catch {
517
+ }
518
+ }, 500);
519
+ }, input.timeoutMs);
520
+ child.stdout.on("data", (chunk) => {
521
+ stdout += chunk.toString("utf8");
522
+ });
523
+ child.stderr.on("data", (chunk) => {
524
+ stderr += chunk.toString("utf8");
525
+ });
526
+ child.once("error", (err) => {
527
+ clearTimeout(timer);
528
+ resolve7({
529
+ exitCode: null,
530
+ stdout,
531
+ stderr,
532
+ timedOut: false,
533
+ spawnError: err
534
+ });
535
+ });
536
+ child.once("close", (code) => {
537
+ clearTimeout(timer);
538
+ resolve7({
539
+ exitCode: code,
540
+ stdout: stdout.trim(),
541
+ stderr: stderr.trim(),
542
+ timedOut
543
+ });
544
+ });
545
+ try {
546
+ child.stdin.write(input.stdin);
547
+ child.stdin.end();
548
+ } catch {
549
+ }
550
+ });
551
+ }
552
+ function formatHookOutcomeMessage(outcome) {
553
+ if (outcome.decision === "pass") return "";
554
+ const detail = (outcome.stderr || outcome.stdout || "").trim();
555
+ const tag = `${outcome.hook.scope}/${outcome.hook.event}`;
556
+ const cmd = outcome.hook.command.length > 60 ? `${outcome.hook.command.slice(0, 60)}\u2026` : outcome.hook.command;
557
+ const head = `hook ${tag} \`${cmd}\` ${outcome.decision}`;
558
+ return detail ? `${head}: ${detail}` : head;
559
+ }
560
+ function decideOutcome(event, raw) {
561
+ if (raw.spawnError) return "error";
562
+ if (raw.timedOut) return BLOCKING_EVENTS.has(event) ? "block" : "warn";
563
+ if (raw.exitCode === 0) return "pass";
564
+ if (raw.exitCode === 2 && BLOCKING_EVENTS.has(event)) return "block";
565
+ return "warn";
566
+ }
567
+ async function runHooks(opts) {
568
+ const spawner = opts.spawner ?? defaultSpawner;
569
+ const event = opts.payload.event;
570
+ const toolName = opts.payload.toolName ?? "";
571
+ const matching = opts.hooks.filter((h) => h.event === event && matchesTool(h, toolName));
572
+ const outcomes = [];
573
+ let blocked = false;
574
+ const stdin = `${JSON.stringify(opts.payload)}
575
+ `;
576
+ for (const hook of matching) {
577
+ const start = Date.now();
578
+ const timeoutMs = hook.timeout ?? DEFAULT_TIMEOUTS_MS[event];
579
+ const cwd = hook.cwd ?? opts.payload.cwd;
580
+ const raw = await spawner({ command: hook.command, cwd, stdin, timeoutMs });
581
+ const decision = decideOutcome(event, raw);
582
+ outcomes.push({
583
+ hook,
584
+ decision,
585
+ exitCode: raw.exitCode,
586
+ stdout: raw.stdout,
587
+ stderr: raw.stderr || (raw.spawnError ? raw.spawnError.message : "") || (raw.timedOut ? `hook timed out after ${timeoutMs}ms` : ""),
588
+ durationMs: Date.now() - start
589
+ });
590
+ if (decision === "block") {
591
+ blocked = true;
592
+ break;
593
+ }
594
+ }
595
+ return { event, outcomes, blocked };
596
+ }
597
+
430
598
  // src/repair/flatten.ts
431
599
  function analyzeSchema(schema) {
432
600
  if (!schema) return { shouldFlatten: false, leafCount: 0, maxDepth: 0 };
@@ -1049,21 +1217,21 @@ function signature2(call) {
1049
1217
  import {
1050
1218
  appendFileSync,
1051
1219
  chmodSync,
1052
- existsSync,
1220
+ existsSync as existsSync2,
1053
1221
  mkdirSync,
1054
- readFileSync,
1222
+ readFileSync as readFileSync2,
1055
1223
  readdirSync,
1056
1224
  statSync,
1057
1225
  unlinkSync,
1058
1226
  writeFileSync
1059
1227
  } from "fs";
1060
- import { homedir } from "os";
1061
- import { dirname, join } from "path";
1228
+ import { homedir as homedir2 } from "os";
1229
+ import { dirname, join as join2 } from "path";
1062
1230
  function sessionsDir() {
1063
- return join(homedir(), ".reasonix", "sessions");
1231
+ return join2(homedir2(), ".reasonix", "sessions");
1064
1232
  }
1065
1233
  function sessionPath(name) {
1066
- return join(sessionsDir(), `${sanitizeName(name)}.jsonl`);
1234
+ return join2(sessionsDir(), `${sanitizeName(name)}.jsonl`);
1067
1235
  }
1068
1236
  function sanitizeName(name) {
1069
1237
  const cleaned = name.replace(/[^\w\-\u4e00-\u9fa5]/g, "_").slice(0, 64);
@@ -1071,9 +1239,9 @@ function sanitizeName(name) {
1071
1239
  }
1072
1240
  function loadSessionMessages(name) {
1073
1241
  const path = sessionPath(name);
1074
- if (!existsSync(path)) return [];
1242
+ if (!existsSync2(path)) return [];
1075
1243
  try {
1076
- const raw = readFileSync(path, "utf8");
1244
+ const raw = readFileSync2(path, "utf8");
1077
1245
  const out = [];
1078
1246
  for (const line of raw.split(/\r?\n/)) {
1079
1247
  const trimmed = line.trim();
@@ -1101,11 +1269,11 @@ function appendSessionMessage(name, message) {
1101
1269
  }
1102
1270
  function listSessions() {
1103
1271
  const dir = sessionsDir();
1104
- if (!existsSync(dir)) return [];
1272
+ if (!existsSync2(dir)) return [];
1105
1273
  try {
1106
1274
  const files = readdirSync(dir).filter((f) => f.endsWith(".jsonl"));
1107
1275
  return files.map((file) => {
1108
- const path = join(dir, file);
1276
+ const path = join2(dir, file);
1109
1277
  const stat = statSync(path);
1110
1278
  const name = file.replace(/\.jsonl$/, "");
1111
1279
  const messageCount = countLines(path);
@@ -1137,7 +1305,7 @@ function rewriteSession(name, messages) {
1137
1305
  }
1138
1306
  function countLines(path) {
1139
1307
  try {
1140
- const raw = readFileSync(path, "utf8");
1308
+ const raw = readFileSync2(path, "utf8");
1141
1309
  return raw.split(/\r?\n/).filter((l) => l.trim()).length;
1142
1310
  } catch {
1143
1311
  return 0;
@@ -1251,6 +1419,14 @@ var CacheFirstLoop = class {
1251
1419
  branchEnabled;
1252
1420
  branchOptions;
1253
1421
  sessionName;
1422
+ /**
1423
+ * Hook list, mutable so `/hooks reload` can swap it without
1424
+ * reconstructing the loop. Default empty — the filter cost on a
1425
+ * tool call is one array length check.
1426
+ */
1427
+ hooks;
1428
+ /** `cwd` reported to hook stdin. Resolved once at construction. */
1429
+ hookCwd;
1254
1430
  /** Number of messages that were pre-loaded from the session file. */
1255
1431
  resumedMessageCount;
1256
1432
  _turn = 0;
@@ -1269,6 +1445,8 @@ var CacheFirstLoop = class {
1269
1445
  this.tools = opts.tools ?? new ToolRegistry();
1270
1446
  this.model = opts.model ?? "deepseek-chat";
1271
1447
  this.maxToolIters = opts.maxToolIters ?? 64;
1448
+ this.hooks = opts.hooks ?? [];
1449
+ this.hookCwd = opts.hookCwd ?? process.cwd();
1272
1450
  if (typeof opts.branch === "number") {
1273
1451
  this.branchOptions = { budget: opts.branch };
1274
1452
  } else if (opts.branch && typeof opts.branch === "object") {
@@ -1732,7 +1910,37 @@ var CacheFirstLoop = class {
1732
1910
  toolName: name,
1733
1911
  toolArgs: args
1734
1912
  };
1735
- const result = await this.tools.dispatch(name, args, { signal });
1913
+ const parsedArgs = safeParseToolArgs(args);
1914
+ const preReport = await runHooks({
1915
+ hooks: this.hooks,
1916
+ payload: {
1917
+ event: "PreToolUse",
1918
+ cwd: this.hookCwd,
1919
+ toolName: name,
1920
+ toolArgs: parsedArgs
1921
+ }
1922
+ });
1923
+ for (const w of hookWarnings(preReport.outcomes, this._turn)) yield w;
1924
+ let result;
1925
+ if (preReport.blocked) {
1926
+ const blocking = preReport.outcomes[preReport.outcomes.length - 1];
1927
+ const reason = (blocking?.stderr || blocking?.stdout || "blocked by PreToolUse hook").trim();
1928
+ result = `[hook block] ${blocking?.hook.command ?? "<unknown>"}
1929
+ ${reason}`;
1930
+ } else {
1931
+ result = await this.tools.dispatch(name, args, { signal });
1932
+ const postReport = await runHooks({
1933
+ hooks: this.hooks,
1934
+ payload: {
1935
+ event: "PostToolUse",
1936
+ cwd: this.hookCwd,
1937
+ toolName: name,
1938
+ toolArgs: parsedArgs,
1939
+ toolResult: result
1940
+ }
1941
+ });
1942
+ for (const w of hookWarnings(postReport.outcomes, this._turn)) yield w;
1943
+ }
1736
1944
  this.appendAndPersist({
1737
1945
  role: "tool",
1738
1946
  tool_call_id: call.id ?? "",
@@ -1819,6 +2027,19 @@ function stripHallucinatedToolMarkup(s) {
1819
2027
  out = out.replace(/<|DSML|[\s\S]*$/g, "");
1820
2028
  return out.trim();
1821
2029
  }
2030
+ function safeParseToolArgs(raw) {
2031
+ try {
2032
+ return JSON.parse(raw);
2033
+ } catch {
2034
+ return raw;
2035
+ }
2036
+ }
2037
+ function* hookWarnings(outcomes, turn) {
2038
+ for (const o of outcomes) {
2039
+ if (o.decision === "pass") continue;
2040
+ yield { turn, role: "warning", content: formatHookOutcomeMessage(o) };
2041
+ }
2042
+ }
1822
2043
  function reasonPrefixFor(reason, iterCap) {
1823
2044
  if (reason === "aborted") return "[aborted by user (Esc) \u2014 summarizing what I found so far]";
1824
2045
  if (reason === "context-guard") {
@@ -1908,16 +2129,16 @@ function formatLoopError(err) {
1908
2129
  }
1909
2130
 
1910
2131
  // src/project-memory.ts
1911
- import { existsSync as existsSync2, readFileSync as readFileSync2 } from "fs";
1912
- import { join as join2 } from "path";
2132
+ import { existsSync as existsSync3, readFileSync as readFileSync3 } from "fs";
2133
+ import { join as join3 } from "path";
1913
2134
  var PROJECT_MEMORY_FILE = "REASONIX.md";
1914
2135
  var PROJECT_MEMORY_MAX_CHARS = 8e3;
1915
2136
  function readProjectMemory(rootDir) {
1916
- const path = join2(rootDir, PROJECT_MEMORY_FILE);
1917
- if (!existsSync2(path)) return null;
2137
+ const path = join3(rootDir, PROJECT_MEMORY_FILE);
2138
+ if (!existsSync3(path)) return null;
1918
2139
  let raw;
1919
2140
  try {
1920
- raw = readFileSync2(path, "utf8");
2141
+ raw = readFileSync3(path, "utf8");
1921
2142
  } catch {
1922
2143
  return null;
1923
2144
  }
@@ -1953,20 +2174,20 @@ ${mem.content}
1953
2174
  // src/user-memory.ts
1954
2175
  import { createHash as createHash2 } from "crypto";
1955
2176
  import {
1956
- existsSync as existsSync4,
2177
+ existsSync as existsSync5,
1957
2178
  mkdirSync as mkdirSync2,
1958
- readFileSync as readFileSync4,
2179
+ readFileSync as readFileSync5,
1959
2180
  readdirSync as readdirSync3,
1960
2181
  unlinkSync as unlinkSync2,
1961
2182
  writeFileSync as writeFileSync2
1962
2183
  } from "fs";
1963
- import { homedir as homedir3 } from "os";
1964
- import { join as join4, resolve as resolve2 } from "path";
2184
+ import { homedir as homedir4 } from "os";
2185
+ import { join as join5, resolve as resolve2 } from "path";
1965
2186
 
1966
2187
  // src/skills.ts
1967
- import { existsSync as existsSync3, readFileSync as readFileSync3, readdirSync as readdirSync2, statSync as statSync2 } from "fs";
1968
- import { homedir as homedir2 } from "os";
1969
- import { join as join3, resolve } from "path";
2188
+ import { existsSync as existsSync4, readFileSync as readFileSync4, readdirSync as readdirSync2, statSync as statSync2 } from "fs";
2189
+ import { homedir as homedir3 } from "os";
2190
+ import { join as join4, resolve } from "path";
1970
2191
  var SKILLS_DIRNAME = "skills";
1971
2192
  var SKILL_FILE = "SKILL.md";
1972
2193
  var SKILLS_INDEX_MAX_CHARS = 4e3;
@@ -1995,7 +2216,7 @@ var SkillStore = class {
1995
2216
  homeDir;
1996
2217
  projectRoot;
1997
2218
  constructor(opts = {}) {
1998
- this.homeDir = opts.homeDir ?? homedir2();
2219
+ this.homeDir = opts.homeDir ?? homedir3();
1999
2220
  this.projectRoot = opts.projectRoot ? resolve(opts.projectRoot) : void 0;
2000
2221
  }
2001
2222
  /** True iff this store was configured with a project root. */
@@ -2011,11 +2232,11 @@ var SkillStore = class {
2011
2232
  const out = [];
2012
2233
  if (this.projectRoot) {
2013
2234
  out.push({
2014
- dir: join3(this.projectRoot, ".reasonix", SKILLS_DIRNAME),
2235
+ dir: join4(this.projectRoot, ".reasonix", SKILLS_DIRNAME),
2015
2236
  scope: "project"
2016
2237
  });
2017
2238
  }
2018
- out.push({ dir: join3(this.homeDir, ".reasonix", SKILLS_DIRNAME), scope: "global" });
2239
+ out.push({ dir: join4(this.homeDir, ".reasonix", SKILLS_DIRNAME), scope: "global" });
2019
2240
  return out;
2020
2241
  }
2021
2242
  /**
@@ -2026,7 +2247,7 @@ var SkillStore = class {
2026
2247
  list() {
2027
2248
  const byName = /* @__PURE__ */ new Map();
2028
2249
  for (const { dir, scope } of this.roots()) {
2029
- if (!existsSync3(dir)) continue;
2250
+ if (!existsSync4(dir)) continue;
2030
2251
  let entries;
2031
2252
  try {
2032
2253
  entries = readdirSync2(dir, { withFileTypes: true });
@@ -2045,13 +2266,13 @@ var SkillStore = class {
2045
2266
  read(name) {
2046
2267
  if (!isValidSkillName(name)) return null;
2047
2268
  for (const { dir, scope } of this.roots()) {
2048
- if (!existsSync3(dir)) continue;
2049
- const dirCandidate = join3(dir, name, SKILL_FILE);
2050
- if (existsSync3(dirCandidate) && statSync2(dirCandidate).isFile()) {
2269
+ if (!existsSync4(dir)) continue;
2270
+ const dirCandidate = join4(dir, name, SKILL_FILE);
2271
+ if (existsSync4(dirCandidate) && statSync2(dirCandidate).isFile()) {
2051
2272
  return this.parse(dirCandidate, name, scope);
2052
2273
  }
2053
- const flatCandidate = join3(dir, `${name}.md`);
2054
- if (existsSync3(flatCandidate) && statSync2(flatCandidate).isFile()) {
2274
+ const flatCandidate = join4(dir, `${name}.md`);
2275
+ if (existsSync4(flatCandidate) && statSync2(flatCandidate).isFile()) {
2055
2276
  return this.parse(flatCandidate, name, scope);
2056
2277
  }
2057
2278
  }
@@ -2060,21 +2281,21 @@ var SkillStore = class {
2060
2281
  readEntry(dir, scope, entry) {
2061
2282
  if (entry.isDirectory()) {
2062
2283
  if (!isValidSkillName(entry.name)) return null;
2063
- const file = join3(dir, entry.name, SKILL_FILE);
2064
- if (!existsSync3(file)) return null;
2284
+ const file = join4(dir, entry.name, SKILL_FILE);
2285
+ if (!existsSync4(file)) return null;
2065
2286
  return this.parse(file, entry.name, scope);
2066
2287
  }
2067
2288
  if (entry.isFile() && entry.name.endsWith(".md")) {
2068
2289
  const stem = entry.name.slice(0, -3);
2069
2290
  if (!isValidSkillName(stem)) return null;
2070
- return this.parse(join3(dir, entry.name), stem, scope);
2291
+ return this.parse(join4(dir, entry.name), stem, scope);
2071
2292
  }
2072
2293
  return null;
2073
2294
  }
2074
2295
  parse(path, stem, scope) {
2075
2296
  let raw;
2076
2297
  try {
2077
- raw = readFileSync3(path, "utf8");
2298
+ raw = readFileSync4(path, "utf8");
2078
2299
  } catch {
2079
2300
  return null;
2080
2301
  }
@@ -2137,15 +2358,15 @@ function projectHash(rootDir) {
2137
2358
  }
2138
2359
  function scopeDir(opts) {
2139
2360
  if (opts.scope === "global") {
2140
- return join4(opts.homeDir, USER_MEMORY_DIR, "global");
2361
+ return join5(opts.homeDir, USER_MEMORY_DIR, "global");
2141
2362
  }
2142
2363
  if (!opts.projectRoot) {
2143
2364
  throw new Error("scope=project requires a projectRoot on MemoryStore");
2144
2365
  }
2145
- return join4(opts.homeDir, USER_MEMORY_DIR, projectHash(opts.projectRoot));
2366
+ return join5(opts.homeDir, USER_MEMORY_DIR, projectHash(opts.projectRoot));
2146
2367
  }
2147
2368
  function ensureDir(p) {
2148
- if (!existsSync4(p)) mkdirSync2(p, { recursive: true });
2369
+ if (!existsSync5(p)) mkdirSync2(p, { recursive: true });
2149
2370
  }
2150
2371
  function parseFrontmatter2(raw) {
2151
2372
  const lines = raw.split(/\r?\n/);
@@ -2190,7 +2411,7 @@ var MemoryStore = class {
2190
2411
  homeDir;
2191
2412
  projectRoot;
2192
2413
  constructor(opts = {}) {
2193
- this.homeDir = opts.homeDir ?? join4(homedir3(), ".reasonix");
2414
+ this.homeDir = opts.homeDir ?? join5(homedir4(), ".reasonix");
2194
2415
  this.projectRoot = opts.projectRoot ? resolve2(opts.projectRoot) : void 0;
2195
2416
  }
2196
2417
  /** Directory this store writes `scope` files into, creating it if needed. */
@@ -2201,7 +2422,7 @@ var MemoryStore = class {
2201
2422
  }
2202
2423
  /** Absolute path to a memory file (no existence check). */
2203
2424
  pathFor(scope, name) {
2204
- return join4(this.dir(scope), `${sanitizeMemoryName(name)}.md`);
2425
+ return join5(this.dir(scope), `${sanitizeMemoryName(name)}.md`);
2205
2426
  }
2206
2427
  /** True iff this store is configured with a project scope available. */
2207
2428
  hasProjectScope() {
@@ -2213,14 +2434,14 @@ var MemoryStore = class {
2213
2434
  */
2214
2435
  loadIndex(scope) {
2215
2436
  if (scope === "project" && !this.projectRoot) return null;
2216
- const file = join4(
2437
+ const file = join5(
2217
2438
  scopeDir({ homeDir: this.homeDir, scope, projectRoot: this.projectRoot }),
2218
2439
  MEMORY_INDEX_FILE
2219
2440
  );
2220
- if (!existsSync4(file)) return null;
2441
+ if (!existsSync5(file)) return null;
2221
2442
  let raw;
2222
2443
  try {
2223
- raw = readFileSync4(file, "utf8");
2444
+ raw = readFileSync5(file, "utf8");
2224
2445
  } catch {
2225
2446
  return null;
2226
2447
  }
@@ -2235,10 +2456,10 @@ var MemoryStore = class {
2235
2456
  /** Read one memory file's body (frontmatter stripped). Throws if missing. */
2236
2457
  read(scope, name) {
2237
2458
  const file = this.pathFor(scope, name);
2238
- if (!existsSync4(file)) {
2459
+ if (!existsSync5(file)) {
2239
2460
  throw new Error(`memory not found: scope=${scope} name=${name}`);
2240
2461
  }
2241
- const raw = readFileSync4(file, "utf8");
2462
+ const raw = readFileSync5(file, "utf8");
2242
2463
  const { data, body } = parseFrontmatter2(raw);
2243
2464
  return {
2244
2465
  name: data.name ?? name,
@@ -2259,7 +2480,7 @@ var MemoryStore = class {
2259
2480
  const scopes = this.projectRoot ? ["global", "project"] : ["global"];
2260
2481
  for (const scope of scopes) {
2261
2482
  const dir = scopeDir({ homeDir: this.homeDir, scope, projectRoot: this.projectRoot });
2262
- if (!existsSync4(dir)) continue;
2483
+ if (!existsSync5(dir)) continue;
2263
2484
  let entries;
2264
2485
  try {
2265
2486
  entries = readdirSync3(dir);
@@ -2300,7 +2521,7 @@ var MemoryStore = class {
2300
2521
  createdAt: todayIso()
2301
2522
  };
2302
2523
  const dir = this.dir(input.scope);
2303
- const file = join4(dir, `${name}.md`);
2524
+ const file = join5(dir, `${name}.md`);
2304
2525
  const content = `${formatFrontmatter(entry)}${body}
2305
2526
  `;
2306
2527
  writeFileSync2(file, content, "utf8");
@@ -2313,7 +2534,7 @@ var MemoryStore = class {
2313
2534
  throw new Error("cannot delete project-scoped memory: no projectRoot configured");
2314
2535
  }
2315
2536
  const file = this.pathFor(scope, rawName);
2316
- if (!existsSync4(file)) return false;
2537
+ if (!existsSync5(file)) return false;
2317
2538
  unlinkSync2(file);
2318
2539
  this.regenerateIndex(scope);
2319
2540
  return true;
@@ -2326,7 +2547,7 @@ var MemoryStore = class {
2326
2547
  */
2327
2548
  regenerateIndex(scope) {
2328
2549
  const dir = scopeDir({ homeDir: this.homeDir, scope, projectRoot: this.projectRoot });
2329
- if (!existsSync4(dir)) return;
2550
+ if (!existsSync5(dir)) return;
2330
2551
  let files;
2331
2552
  try {
2332
2553
  files = readdirSync3(dir);
@@ -2334,9 +2555,9 @@ var MemoryStore = class {
2334
2555
  return;
2335
2556
  }
2336
2557
  const mdFiles = files.filter((f) => f !== MEMORY_INDEX_FILE && f.endsWith(".md")).sort((a, b) => a.localeCompare(b));
2337
- const indexPath = join4(dir, MEMORY_INDEX_FILE);
2558
+ const indexPath = join5(dir, MEMORY_INDEX_FILE);
2338
2559
  if (mdFiles.length === 0) {
2339
- if (existsSync4(indexPath)) unlinkSync2(indexPath);
2560
+ if (existsSync5(indexPath)) unlinkSync2(indexPath);
2340
2561
  return;
2341
2562
  }
2342
2563
  const lines = [];
@@ -2899,8 +3120,8 @@ function registerPlanTool(registry, opts = {}) {
2899
3120
  }
2900
3121
 
2901
3122
  // src/tools/shell.ts
2902
- import { spawn } from "child_process";
2903
- import { existsSync as existsSync5, statSync as statSync3 } from "fs";
3123
+ import { spawn as spawn2 } from "child_process";
3124
+ import { existsSync as existsSync6, statSync as statSync3 } from "fs";
2904
3125
  import * as pathMod2 from "path";
2905
3126
  var DEFAULT_TIMEOUT_SEC = 60;
2906
3127
  var DEFAULT_MAX_OUTPUT_CHARS = 32e3;
@@ -3023,7 +3244,7 @@ async function runCommand(cmd, opts) {
3023
3244
  return await new Promise((resolve7, reject) => {
3024
3245
  let child;
3025
3246
  try {
3026
- child = spawn(bin, args, effectiveSpawnOpts);
3247
+ child = spawn2(bin, args, effectiveSpawnOpts);
3027
3248
  } catch (err) {
3028
3249
  reject(err);
3029
3250
  return;
@@ -3078,7 +3299,7 @@ function resolveExecutable(cmd, opts = {}) {
3078
3299
  }
3079
3300
  function defaultIsFile(full) {
3080
3301
  try {
3081
- return existsSync5(full) && statSync3(full).isFile();
3302
+ return existsSync6(full) && statSync3(full).isFile();
3082
3303
  } catch {
3083
3304
  return false;
3084
3305
  }
@@ -3381,12 +3602,12 @@ ${i + 1}. ${r.title}`);
3381
3602
  }
3382
3603
 
3383
3604
  // src/env.ts
3384
- import { readFileSync as readFileSync5 } from "fs";
3605
+ import { readFileSync as readFileSync6 } from "fs";
3385
3606
  import { resolve as resolve5 } from "path";
3386
3607
  function loadDotenv(path = ".env") {
3387
3608
  let raw;
3388
3609
  try {
3389
- raw = readFileSync5(resolve5(process.cwd(), path), "utf8");
3610
+ raw = readFileSync6(resolve5(process.cwd(), path), "utf8");
3390
3611
  } catch {
3391
3612
  return;
3392
3613
  }
@@ -3405,7 +3626,7 @@ function loadDotenv(path = ".env") {
3405
3626
  }
3406
3627
 
3407
3628
  // src/transcript.ts
3408
- import { createWriteStream, readFileSync as readFileSync6 } from "fs";
3629
+ import { createWriteStream, readFileSync as readFileSync7 } from "fs";
3409
3630
  function recordFromLoopEvent(ev, extra) {
3410
3631
  const rec = {
3411
3632
  ts: (/* @__PURE__ */ new Date()).toISOString(),
@@ -3456,7 +3677,7 @@ function openTranscriptFile(path, meta) {
3456
3677
  return stream;
3457
3678
  }
3458
3679
  function readTranscript(path) {
3459
- const raw = readFileSync6(path, "utf8");
3680
+ const raw = readFileSync7(path, "utf8");
3460
3681
  return parseTranscript(raw);
3461
3682
  }
3462
3683
  function isPlanStateEmptyShape(s) {
@@ -4159,7 +4380,7 @@ var McpClient = class {
4159
4380
  };
4160
4381
 
4161
4382
  // src/mcp/stdio.ts
4162
- import { spawn as spawn2 } from "child_process";
4383
+ import { spawn as spawn3 } from "child_process";
4163
4384
  var StdioTransport = class {
4164
4385
  child;
4165
4386
  queue = [];
@@ -4174,14 +4395,14 @@ var StdioTransport = class {
4174
4395
  opts.command,
4175
4396
  ...(opts.args ?? []).map((a) => quoteArg(a, process.platform === "win32"))
4176
4397
  ].join(" ");
4177
- this.child = spawn2(line, [], {
4398
+ this.child = spawn3(line, [], {
4178
4399
  env,
4179
4400
  cwd: opts.cwd,
4180
4401
  stdio: ["pipe", "pipe", "inherit"],
4181
4402
  shell: true
4182
4403
  });
4183
4404
  } else {
4184
- this.child = spawn2(opts.command, opts.args ?? [], {
4405
+ this.child = spawn3(opts.command, opts.args ?? [], {
4185
4406
  env,
4186
4407
  cwd: opts.cwd,
4187
4408
  stdio: ["pipe", "pipe", "inherit"]
@@ -4511,7 +4732,7 @@ async function trySection(load) {
4511
4732
  }
4512
4733
 
4513
4734
  // src/code/edit-blocks.ts
4514
- import { existsSync as existsSync6, mkdirSync as mkdirSync3, readFileSync as readFileSync7, unlinkSync as unlinkSync3, writeFileSync as writeFileSync3 } from "fs";
4735
+ import { existsSync as existsSync7, mkdirSync as mkdirSync3, readFileSync as readFileSync8, unlinkSync as unlinkSync3, writeFileSync as writeFileSync3 } from "fs";
4515
4736
  import { dirname as dirname3, resolve as resolve6 } from "path";
4516
4737
  var BLOCK_RE = /^(\S[^\n]*)\n<{7} SEARCH\n([\s\S]*?)\n?={7}\n([\s\S]*?)\n?>{7} REPLACE/gm;
4517
4738
  function parseEditBlocks(text) {
@@ -4540,7 +4761,7 @@ function applyEditBlock(block, rootDir) {
4540
4761
  };
4541
4762
  }
4542
4763
  const searchEmpty = block.search.length === 0;
4543
- const exists = existsSync6(absTarget);
4764
+ const exists = existsSync7(absTarget);
4544
4765
  try {
4545
4766
  if (!exists) {
4546
4767
  if (!searchEmpty) {
@@ -4554,7 +4775,7 @@ function applyEditBlock(block, rootDir) {
4554
4775
  writeFileSync3(absTarget, block.replace, "utf8");
4555
4776
  return { path: block.path, status: "created" };
4556
4777
  }
4557
- const content = readFileSync7(absTarget, "utf8");
4778
+ const content = readFileSync8(absTarget, "utf8");
4558
4779
  if (searchEmpty) {
4559
4780
  return {
4560
4781
  path: block.path,
@@ -4588,12 +4809,12 @@ function snapshotBeforeEdits(blocks, rootDir) {
4588
4809
  if (seen.has(b.path)) continue;
4589
4810
  seen.add(b.path);
4590
4811
  const abs = resolve6(absRoot, b.path);
4591
- if (!existsSync6(abs)) {
4812
+ if (!existsSync7(abs)) {
4592
4813
  snapshots.push({ path: b.path, prevContent: null });
4593
4814
  continue;
4594
4815
  }
4595
4816
  try {
4596
- snapshots.push({ path: b.path, prevContent: readFileSync7(abs, "utf8") });
4817
+ snapshots.push({ path: b.path, prevContent: readFileSync8(abs, "utf8") });
4597
4818
  } catch {
4598
4819
  snapshots.push({ path: b.path, prevContent: null });
4599
4820
  }
@@ -4613,7 +4834,7 @@ function restoreSnapshots(snapshots, rootDir) {
4613
4834
  }
4614
4835
  try {
4615
4836
  if (snap.prevContent === null) {
4616
- if (existsSync6(abs)) unlinkSync3(abs);
4837
+ if (existsSync7(abs)) unlinkSync3(abs);
4617
4838
  return {
4618
4839
  path: snap.path,
4619
4840
  status: "applied",
@@ -4636,8 +4857,8 @@ function sep() {
4636
4857
  }
4637
4858
 
4638
4859
  // src/code/prompt.ts
4639
- import { existsSync as existsSync7, readFileSync as readFileSync8 } from "fs";
4640
- import { join as join6 } from "path";
4860
+ import { existsSync as existsSync8, readFileSync as readFileSync9 } from "fs";
4861
+ import { join as join7 } from "path";
4641
4862
  var CODE_SYSTEM_PROMPT = `You are Reasonix Code, a coding assistant. You have filesystem tools (read_file, write_file, list_directory, search_files, etc.) rooted at the user's working directory.
4642
4863
 
4643
4864
  # When to propose a plan (submit_plan)
@@ -4712,11 +4933,11 @@ Before exploring the filesystem to answer a factual question, check whether the
4712
4933
  `;
4713
4934
  function codeSystemPrompt(rootDir) {
4714
4935
  const withMemory = applyMemoryStack(CODE_SYSTEM_PROMPT, rootDir);
4715
- const gitignorePath = join6(rootDir, ".gitignore");
4716
- if (!existsSync7(gitignorePath)) return withMemory;
4936
+ const gitignorePath = join7(rootDir, ".gitignore");
4937
+ if (!existsSync8(gitignorePath)) return withMemory;
4717
4938
  let content;
4718
4939
  try {
4719
- content = readFileSync8(gitignorePath, "utf8");
4940
+ content = readFileSync9(gitignorePath, "utf8");
4720
4941
  } catch {
4721
4942
  return withMemory;
4722
4943
  }
@@ -4736,15 +4957,15 @@ ${truncated}
4736
4957
  }
4737
4958
 
4738
4959
  // src/config.ts
4739
- import { chmodSync as chmodSync2, mkdirSync as mkdirSync4, readFileSync as readFileSync9, writeFileSync as writeFileSync4 } from "fs";
4740
- import { homedir as homedir4 } from "os";
4741
- import { dirname as dirname4, join as join7 } from "path";
4960
+ import { chmodSync as chmodSync2, mkdirSync as mkdirSync4, readFileSync as readFileSync10, writeFileSync as writeFileSync4 } from "fs";
4961
+ import { homedir as homedir5 } from "os";
4962
+ import { dirname as dirname4, join as join8 } from "path";
4742
4963
  function defaultConfigPath() {
4743
- return join7(homedir4(), ".reasonix", "config.json");
4964
+ return join8(homedir5(), ".reasonix", "config.json");
4744
4965
  }
4745
4966
  function readConfig(path = defaultConfigPath()) {
4746
4967
  try {
4747
- const raw = readFileSync9(path, "utf8");
4968
+ const raw = readFileSync10(path, "utf8");
4748
4969
  const parsed = JSON.parse(raw);
4749
4970
  if (parsed && typeof parsed === "object") return parsed;
4750
4971
  } catch {
@@ -4779,9 +5000,9 @@ function redactKey(key) {
4779
5000
  }
4780
5001
 
4781
5002
  // src/version.ts
4782
- import { existsSync as existsSync8, mkdirSync as mkdirSync5, readFileSync as readFileSync10, writeFileSync as writeFileSync5 } from "fs";
4783
- import { homedir as homedir5 } from "os";
4784
- import { dirname as dirname5, join as join8 } from "path";
5003
+ import { existsSync as existsSync9, mkdirSync as mkdirSync5, readFileSync as readFileSync11, writeFileSync as writeFileSync5 } from "fs";
5004
+ import { homedir as homedir6 } from "os";
5005
+ import { dirname as dirname5, join as join9 } from "path";
4785
5006
  import { fileURLToPath } from "url";
4786
5007
  var REGISTRY_URL = "https://registry.npmjs.org/reasonix/latest";
4787
5008
  var LATEST_CACHE_TTL_MS = 24 * 60 * 60 * 1e3;
@@ -4790,9 +5011,9 @@ function readPackageVersion() {
4790
5011
  try {
4791
5012
  let dir = dirname5(fileURLToPath(import.meta.url));
4792
5013
  for (let i = 0; i < 6; i++) {
4793
- const p = join8(dir, "package.json");
4794
- if (existsSync8(p)) {
4795
- const pkg = JSON.parse(readFileSync10(p, "utf8"));
5014
+ const p = join9(dir, "package.json");
5015
+ if (existsSync9(p)) {
5016
+ const pkg = JSON.parse(readFileSync11(p, "utf8"));
4796
5017
  if (pkg?.name === "reasonix" && typeof pkg.version === "string") {
4797
5018
  return pkg.version;
4798
5019
  }
@@ -4807,11 +5028,11 @@ function readPackageVersion() {
4807
5028
  }
4808
5029
  var VERSION = readPackageVersion();
4809
5030
  function cachePath(homeDirOverride) {
4810
- return join8(homeDirOverride ?? homedir5(), ".reasonix", "version-cache.json");
5031
+ return join9(homeDirOverride ?? homedir6(), ".reasonix", "version-cache.json");
4811
5032
  }
4812
5033
  function readCache(homeDirOverride) {
4813
5034
  try {
4814
- const raw = readFileSync10(cachePath(homeDirOverride), "utf8");
5035
+ const raw = readFileSync11(cachePath(homeDirOverride), "utf8");
4815
5036
  const parsed = JSON.parse(raw);
4816
5037
  if (parsed && typeof parsed.version === "string" && typeof parsed.checkedAt === "number") {
4817
5038
  return parsed;
@@ -4878,12 +5099,141 @@ function isNpxInstall() {
4878
5099
  if (ua.includes("npx/")) return true;
4879
5100
  return false;
4880
5101
  }
5102
+
5103
+ // src/usage.ts
5104
+ import { appendFileSync as appendFileSync2, existsSync as existsSync10, mkdirSync as mkdirSync6, readFileSync as readFileSync12, statSync as statSync4 } from "fs";
5105
+ import { homedir as homedir7 } from "os";
5106
+ import { dirname as dirname6, join as join10 } from "path";
5107
+ function defaultUsageLogPath(homeDirOverride) {
5108
+ return join10(homeDirOverride ?? homedir7(), ".reasonix", "usage.jsonl");
5109
+ }
5110
+ function appendUsage(input) {
5111
+ const record = {
5112
+ ts: input.now ?? Date.now(),
5113
+ session: input.session,
5114
+ model: input.model,
5115
+ promptTokens: input.usage.promptTokens,
5116
+ completionTokens: input.usage.completionTokens,
5117
+ cacheHitTokens: input.usage.promptCacheHitTokens,
5118
+ cacheMissTokens: input.usage.promptCacheMissTokens,
5119
+ costUsd: costUsd(input.model, input.usage),
5120
+ claudeEquivUsd: claudeEquivalentCost(input.usage)
5121
+ };
5122
+ const path = input.path ?? defaultUsageLogPath();
5123
+ try {
5124
+ mkdirSync6(dirname6(path), { recursive: true });
5125
+ appendFileSync2(path, `${JSON.stringify(record)}
5126
+ `, "utf8");
5127
+ } catch {
5128
+ }
5129
+ return record;
5130
+ }
5131
+ function readUsageLog(path = defaultUsageLogPath()) {
5132
+ if (!existsSync10(path)) return [];
5133
+ let raw;
5134
+ try {
5135
+ raw = readFileSync12(path, "utf8");
5136
+ } catch {
5137
+ return [];
5138
+ }
5139
+ const out = [];
5140
+ for (const line of raw.split(/\r?\n/)) {
5141
+ if (!line.trim()) continue;
5142
+ try {
5143
+ const rec = JSON.parse(line);
5144
+ if (isValidRecord(rec)) out.push(rec);
5145
+ } catch {
5146
+ }
5147
+ }
5148
+ return out;
5149
+ }
5150
+ function isValidRecord(rec) {
5151
+ if (!rec || typeof rec !== "object") return false;
5152
+ const r = rec;
5153
+ return typeof r.ts === "number" && typeof r.model === "string" && typeof r.promptTokens === "number" && typeof r.completionTokens === "number" && typeof r.cacheHitTokens === "number" && typeof r.cacheMissTokens === "number" && typeof r.costUsd === "number" && typeof r.claudeEquivUsd === "number";
5154
+ }
5155
+ function bucketCacheHitRatio(b) {
5156
+ const denom = b.cacheHitTokens + b.cacheMissTokens;
5157
+ return denom > 0 ? b.cacheHitTokens / denom : 0;
5158
+ }
5159
+ function bucketSavingsFraction(b) {
5160
+ return b.claudeEquivUsd > 0 ? 1 - b.costUsd / b.claudeEquivUsd : 0;
5161
+ }
5162
+ function emptyBucket(label, since) {
5163
+ return {
5164
+ label,
5165
+ since,
5166
+ turns: 0,
5167
+ promptTokens: 0,
5168
+ completionTokens: 0,
5169
+ cacheHitTokens: 0,
5170
+ cacheMissTokens: 0,
5171
+ costUsd: 0,
5172
+ claudeEquivUsd: 0
5173
+ };
5174
+ }
5175
+ function addToBucket(b, r) {
5176
+ b.turns += 1;
5177
+ b.promptTokens += r.promptTokens;
5178
+ b.completionTokens += r.completionTokens;
5179
+ b.cacheHitTokens += r.cacheHitTokens;
5180
+ b.cacheMissTokens += r.cacheMissTokens;
5181
+ b.costUsd += r.costUsd;
5182
+ b.claudeEquivUsd += r.claudeEquivUsd;
5183
+ }
5184
+ function aggregateUsage(records, opts = {}) {
5185
+ const now = opts.now ?? Date.now();
5186
+ const day = 24 * 60 * 60 * 1e3;
5187
+ const today = emptyBucket("today", now - day);
5188
+ const week = emptyBucket("week", now - 7 * day);
5189
+ const month = emptyBucket("month", now - 30 * day);
5190
+ const all = emptyBucket("all-time", 0);
5191
+ const modelCounts = /* @__PURE__ */ new Map();
5192
+ const sessionCounts = /* @__PURE__ */ new Map();
5193
+ let firstSeen = null;
5194
+ let lastSeen = null;
5195
+ for (const r of records) {
5196
+ addToBucket(all, r);
5197
+ if (r.ts >= today.since) addToBucket(today, r);
5198
+ if (r.ts >= week.since) addToBucket(week, r);
5199
+ if (r.ts >= month.since) addToBucket(month, r);
5200
+ modelCounts.set(r.model, (modelCounts.get(r.model) ?? 0) + 1);
5201
+ const sessKey = r.session ?? "(ephemeral)";
5202
+ sessionCounts.set(sessKey, (sessionCounts.get(sessKey) ?? 0) + 1);
5203
+ if (firstSeen === null || r.ts < firstSeen) firstSeen = r.ts;
5204
+ if (lastSeen === null || r.ts > lastSeen) lastSeen = r.ts;
5205
+ }
5206
+ const byModel = Array.from(modelCounts.entries()).map(([model, turns]) => ({ model, turns })).sort((a, b) => b.turns - a.turns);
5207
+ const bySession = Array.from(sessionCounts.entries()).map(([session, turns]) => ({ session, turns })).sort((a, b) => b.turns - a.turns);
5208
+ return {
5209
+ buckets: [today, week, month, all],
5210
+ byModel,
5211
+ bySession,
5212
+ firstSeen,
5213
+ lastSeen
5214
+ };
5215
+ }
5216
+ function formatLogSize(path = defaultUsageLogPath()) {
5217
+ if (!existsSync10(path)) return "";
5218
+ try {
5219
+ const s = statSync4(path);
5220
+ const bytes = s.size;
5221
+ if (bytes < 1024) return `${bytes} B`;
5222
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
5223
+ return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
5224
+ } catch {
5225
+ return "";
5226
+ }
5227
+ }
4881
5228
  export {
4882
5229
  AppendOnlyLog,
4883
5230
  CODE_SYSTEM_PROMPT,
4884
5231
  CacheFirstLoop,
4885
5232
  DEFAULT_MAX_RESULT_CHARS,
4886
5233
  DeepSeekClient,
5234
+ HOOK_EVENTS,
5235
+ HOOK_SETTINGS_DIRNAME,
5236
+ HOOK_SETTINGS_FILENAME,
4887
5237
  ImmutablePrefix,
4888
5238
  LATEST_CACHE_TTL_MS,
4889
5239
  LATEST_FETCH_TIMEOUT_MS,
@@ -4907,21 +5257,27 @@ export {
4907
5257
  VERSION,
4908
5258
  VolatileScratch,
4909
5259
  aggregateBranchUsage,
5260
+ aggregateUsage,
4910
5261
  analyzeSchema,
4911
5262
  appendSessionMessage,
5263
+ appendUsage,
4912
5264
  applyEditBlock,
4913
5265
  applyEditBlocks,
4914
5266
  applyMemoryStack,
4915
5267
  applyProjectMemory,
4916
5268
  applyUserMemory,
4917
5269
  bridgeMcpTools,
5270
+ bucketCacheHitRatio,
5271
+ bucketSavingsFraction,
4918
5272
  claudeEquivalentCost,
4919
5273
  codeSystemPrompt,
4920
5274
  compareVersions,
4921
5275
  computeReplayStats,
4922
5276
  costUsd,
5277
+ decideOutcome,
4923
5278
  defaultConfigPath,
4924
5279
  defaultSelector,
5280
+ defaultUsageLogPath,
4925
5281
  deleteSession,
4926
5282
  diffTranscripts,
4927
5283
  emptyPlanState,
@@ -4929,9 +5285,12 @@ export {
4929
5285
  flattenMcpResult,
4930
5286
  flattenSchema,
4931
5287
  formatCommandResult,
5288
+ formatHookOutcomeMessage,
5289
+ formatLogSize,
4932
5290
  formatLoopError,
4933
5291
  formatSearchResults,
4934
5292
  getLatestVersion,
5293
+ globalSettingsPath,
4935
5294
  harvest,
4936
5295
  healLoadedMessages,
4937
5296
  htmlToText,
@@ -4945,7 +5304,9 @@ export {
4945
5304
  listSessions,
4946
5305
  loadApiKey,
4947
5306
  loadDotenv,
5307
+ loadHooks,
4948
5308
  loadSessionMessages,
5309
+ matchesTool,
4949
5310
  memoryEnabled,
4950
5311
  nestArguments,
4951
5312
  openTranscriptFile,
@@ -4956,10 +5317,12 @@ export {
4956
5317
  parseTranscript,
4957
5318
  prepareSpawn,
4958
5319
  projectHash,
5320
+ projectSettingsPath,
4959
5321
  quoteForCmdExe,
4960
5322
  readConfig,
4961
5323
  readProjectMemory,
4962
5324
  readTranscript,
5325
+ readUsageLog,
4963
5326
  recordFromLoopEvent,
4964
5327
  redactKey,
4965
5328
  registerFilesystemTools,
@@ -4975,6 +5338,7 @@ export {
4975
5338
  restoreSnapshots,
4976
5339
  runBranches,
4977
5340
  runCommand,
5341
+ runHooks,
4978
5342
  sanitizeMemoryName,
4979
5343
  sanitizeName as sanitizeSessionName,
4980
5344
  saveApiKey,