reasonix 0.12.15 → 0.12.19

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/cli/index.js CHANGED
@@ -666,6 +666,7 @@ function matchesTool(hook, toolName) {
666
666
  return false;
667
667
  }
668
668
  }
669
+ var HOOK_OUTPUT_CAP_BYTES = 256 * 1024;
669
670
  function defaultSpawner(input) {
670
671
  return new Promise((resolve13) => {
671
672
  const child = spawn(input.command, {
@@ -673,8 +674,11 @@ function defaultSpawner(input) {
673
674
  shell: true,
674
675
  stdio: ["pipe", "pipe", "pipe"]
675
676
  });
676
- let stdout3 = "";
677
- let stderr = "";
677
+ const stdoutChunks = [];
678
+ const stderrChunks = [];
679
+ let stdoutBytes = 0;
680
+ let stderrBytes = 0;
681
+ let truncated = false;
678
682
  let timedOut = false;
679
683
  const timer = setTimeout(() => {
680
684
  timedOut = true;
@@ -686,29 +690,46 @@ function defaultSpawner(input) {
686
690
  }
687
691
  }, 500);
688
692
  }, input.timeoutMs);
689
- child.stdout.on("data", (chunk) => {
690
- stdout3 += chunk.toString("utf8");
691
- });
692
- child.stderr.on("data", (chunk) => {
693
- stderr += chunk.toString("utf8");
694
- });
693
+ const onChunk = (kind, chunk) => {
694
+ const target = kind === "stdout" ? stdoutChunks : stderrChunks;
695
+ const seen = kind === "stdout" ? stdoutBytes : stderrBytes;
696
+ if (seen >= HOOK_OUTPUT_CAP_BYTES) {
697
+ truncated = true;
698
+ return;
699
+ }
700
+ const remaining = HOOK_OUTPUT_CAP_BYTES - seen;
701
+ if (chunk.length > remaining) {
702
+ target.push(chunk.subarray(0, remaining));
703
+ if (kind === "stdout") stdoutBytes = HOOK_OUTPUT_CAP_BYTES;
704
+ else stderrBytes = HOOK_OUTPUT_CAP_BYTES;
705
+ truncated = true;
706
+ } else {
707
+ target.push(chunk);
708
+ if (kind === "stdout") stdoutBytes += chunk.length;
709
+ else stderrBytes += chunk.length;
710
+ }
711
+ };
712
+ child.stdout.on("data", (chunk) => onChunk("stdout", chunk));
713
+ child.stderr.on("data", (chunk) => onChunk("stderr", chunk));
695
714
  child.once("error", (err) => {
696
715
  clearTimeout(timer);
697
716
  resolve13({
698
717
  exitCode: null,
699
- stdout: stdout3,
700
- stderr,
718
+ stdout: Buffer.concat(stdoutChunks).toString("utf8"),
719
+ stderr: Buffer.concat(stderrChunks).toString("utf8"),
701
720
  timedOut: false,
702
- spawnError: err
721
+ spawnError: err,
722
+ truncated: truncated || void 0
703
723
  });
704
724
  });
705
725
  child.once("close", (code) => {
706
726
  clearTimeout(timer);
707
727
  resolve13({
708
728
  exitCode: code,
709
- stdout: stdout3.trim(),
710
- stderr: stderr.trim(),
711
- timedOut
729
+ stdout: Buffer.concat(stdoutChunks).toString("utf8").trim(),
730
+ stderr: Buffer.concat(stderrChunks).toString("utf8").trim(),
731
+ timedOut,
732
+ truncated: truncated || void 0
712
733
  });
713
734
  });
714
735
  try {
@@ -723,7 +744,8 @@ function formatHookOutcomeMessage(outcome) {
723
744
  const detail = (outcome.stderr || outcome.stdout || "").trim();
724
745
  const tag = `${outcome.hook.scope}/${outcome.hook.event}`;
725
746
  const cmd = outcome.hook.command.length > 60 ? `${outcome.hook.command.slice(0, 60)}\u2026` : outcome.hook.command;
726
- const head = `hook ${tag} \`${cmd}\` ${outcome.decision}`;
747
+ const truncTag = outcome.truncated ? " (output truncated at 256KB)" : "";
748
+ const head = `hook ${tag} \`${cmd}\` ${outcome.decision}${truncTag}`;
727
749
  return detail ? `${head}: ${detail}` : head;
728
750
  }
729
751
  function decideOutcome(event, raw) {
@@ -754,7 +776,8 @@ async function runHooks(opts) {
754
776
  exitCode: raw.exitCode,
755
777
  stdout: raw.stdout,
756
778
  stderr: raw.stderr || (raw.spawnError ? raw.spawnError.message : "") || (raw.timedOut ? `hook timed out after ${timeoutMs}ms` : ""),
757
- durationMs: Date.now() - start
779
+ durationMs: Date.now() - start,
780
+ truncated: raw.truncated
758
781
  });
759
782
  if (decision === "block") {
760
783
  blocked = true;
@@ -1302,6 +1325,23 @@ var ImmutablePrefix = class {
1302
1325
  */
1303
1326
  _toolSpecs;
1304
1327
  fewShots;
1328
+ /**
1329
+ * Cached SHA-256 of the prefix payload. Computed lazily on first
1330
+ * `fingerprint` access, invalidated only by mutations that go
1331
+ * through `addTool` (the one legitimate post-construction mutation
1332
+ * path). The TUI reads `fingerprint` on every render — without the
1333
+ * cache, that means a fresh `JSON.stringify` + sha256 over the
1334
+ * full prefix (system prompt + tools list + few-shots, typically
1335
+ * 5-10KB) on every keystroke.
1336
+ *
1337
+ * The lazy-init also acts as a cheap drift guard: if some future
1338
+ * code path mutates `_toolSpecs` directly without going through
1339
+ * `addTool`, `fingerprint` will return the stale cached value
1340
+ * while the actual prefix sent to DeepSeek diverges — the cache
1341
+ * miss would be the first symptom. {@link verifyFingerprint}
1342
+ * lets dev / test code assert the cache matches reality.
1343
+ */
1344
+ _fingerprintCache = null;
1305
1345
  constructor(opts) {
1306
1346
  this.system = opts.system;
1307
1347
  this._toolSpecs = [...opts.toolSpecs ?? []];
@@ -1327,9 +1367,33 @@ var ImmutablePrefix = class {
1327
1367
  if (!name) return false;
1328
1368
  if (this._toolSpecs.some((t2) => t2.function?.name === name)) return false;
1329
1369
  this._toolSpecs.push(spec);
1370
+ this._fingerprintCache = null;
1330
1371
  return true;
1331
1372
  }
1332
1373
  get fingerprint() {
1374
+ if (this._fingerprintCache !== null) return this._fingerprintCache;
1375
+ this._fingerprintCache = this.computeFingerprint();
1376
+ return this._fingerprintCache;
1377
+ }
1378
+ /**
1379
+ * Recompute the fingerprint from scratch and assert it matches the
1380
+ * cached value. Returns the freshly-computed hash on success; throws
1381
+ * with a diff if the cache drifted, which always indicates a bug —
1382
+ * either a non-`addTool` mutation path was added, or `addTool`
1383
+ * forgot to invalidate the cache. Dev / test only; the live loop
1384
+ * doesn't call this on the hot path.
1385
+ */
1386
+ verifyFingerprint() {
1387
+ const fresh = this.computeFingerprint();
1388
+ if (this._fingerprintCache !== null && this._fingerprintCache !== fresh) {
1389
+ throw new Error(
1390
+ `ImmutablePrefix fingerprint drift: cached=${this._fingerprintCache}, fresh=${fresh}. A mutation path bypassed addTool's cache invalidation \u2014 DeepSeek will see prefix churn that the TUI / transcript log don't know about.`
1391
+ );
1392
+ }
1393
+ this._fingerprintCache = fresh;
1394
+ return fresh;
1395
+ }
1396
+ computeFingerprint() {
1333
1397
  const blob = JSON.stringify({
1334
1398
  system: this.system,
1335
1399
  tools: this._toolSpecs,
@@ -1748,15 +1812,25 @@ function listSessions() {
1748
1812
  const files = readdirSync(dir).filter((f) => f.endsWith(".jsonl"));
1749
1813
  return files.map((file) => {
1750
1814
  const path5 = join4(dir, file);
1751
- const stat = statSync(path5);
1815
+ const stat2 = statSync(path5);
1752
1816
  const name = file.replace(/\.jsonl$/, "");
1753
1817
  const messageCount = countLines(path5);
1754
- return { name, path: path5, size: stat.size, messageCount, mtime: stat.mtime };
1818
+ return { name, path: path5, size: stat2.size, messageCount, mtime: stat2.mtime };
1755
1819
  }).sort((a, b) => b.mtime.getTime() - a.mtime.getTime());
1756
1820
  } catch {
1757
1821
  return [];
1758
1822
  }
1759
1823
  }
1824
+ function pruneStaleSessions(daysOld = 90) {
1825
+ const cutoff = Date.now() - daysOld * 24 * 60 * 60 * 1e3;
1826
+ const deleted = [];
1827
+ for (const s of listSessions()) {
1828
+ if (s.mtime.getTime() < cutoff) {
1829
+ if (deleteSession(s.name)) deleted.push(s.name);
1830
+ }
1831
+ }
1832
+ return deleted;
1833
+ }
1760
1834
  function deleteSession(name) {
1761
1835
  const path5 = sessionPath(name);
1762
1836
  try {
@@ -3252,6 +3326,7 @@ function extractDeepSeekErrorMessage(body) {
3252
3326
 
3253
3327
  // src/at-mentions.ts
3254
3328
  import { existsSync as existsSync4, readFileSync as readFileSync5, readdirSync as readdirSync2, statSync as statSync2 } from "fs";
3329
+ import { readdir, stat } from "fs/promises";
3255
3330
  import { isAbsolute, join as join5, relative, resolve } from "path";
3256
3331
  var DEFAULT_AT_MENTION_MAX_BYTES = 64 * 1024;
3257
3332
  var DEFAULT_PICKER_IGNORE_DIRS = [
@@ -3306,6 +3381,58 @@ function listFilesWithStatsSync(root, opts = {}) {
3306
3381
  walk3(rootAbs, "");
3307
3382
  return out;
3308
3383
  }
3384
+ async function listFilesWithStatsAsync(root, opts = {}) {
3385
+ const maxResults = Math.max(1, opts.maxResults ?? 500);
3386
+ const ignore = new Set(opts.ignoreDirs ?? DEFAULT_PICKER_IGNORE_DIRS);
3387
+ const rootAbs = resolve(root);
3388
+ const out = [];
3389
+ const walk3 = async (dirAbs, dirRel) => {
3390
+ if (out.length >= maxResults) return;
3391
+ let entries;
3392
+ try {
3393
+ entries = await readdir(dirAbs, { withFileTypes: true });
3394
+ } catch {
3395
+ return;
3396
+ }
3397
+ entries.sort((a, b) => a.name.localeCompare(b.name));
3398
+ const fileEnts = [];
3399
+ for (const ent of entries) {
3400
+ if (out.length >= maxResults) break;
3401
+ if (ent.isDirectory()) {
3402
+ if (ent.name.startsWith(".") || ignore.has(ent.name)) continue;
3403
+ if (fileEnts.length > 0) {
3404
+ await statBatch(fileEnts, dirAbs, dirRel, out, maxResults);
3405
+ fileEnts.length = 0;
3406
+ if (out.length >= maxResults) return;
3407
+ }
3408
+ await walk3(join5(dirAbs, ent.name), dirRel ? `${dirRel}/${ent.name}` : ent.name);
3409
+ } else if (ent.isFile()) {
3410
+ fileEnts.push(ent);
3411
+ }
3412
+ }
3413
+ if (fileEnts.length > 0 && out.length < maxResults) {
3414
+ await statBatch(fileEnts, dirAbs, dirRel, out, maxResults);
3415
+ }
3416
+ };
3417
+ await walk3(rootAbs, "");
3418
+ return out;
3419
+ }
3420
+ async function statBatch(ents, dirAbs, dirRel, out, maxResults) {
3421
+ const remaining = Math.max(0, maxResults - out.length);
3422
+ const batch = ents.slice(0, remaining);
3423
+ const stats2 = await Promise.all(
3424
+ batch.map(
3425
+ (e) => stat(join5(dirAbs, e.name)).then((s) => s.mtimeMs).catch(() => 0)
3426
+ )
3427
+ );
3428
+ for (let i = 0; i < batch.length; i++) {
3429
+ const ent = batch[i];
3430
+ out.push({
3431
+ path: dirRel ? `${dirRel}/${ent.name}` : ent.name,
3432
+ mtimeMs: stats2[i] ?? 0
3433
+ });
3434
+ }
3435
+ }
3309
3436
  var AT_PICKER_PREFIX = /(?:^|\s)@([a-zA-Z0-9_./\\-]*)$/;
3310
3437
  function detectAtPicker(input) {
3311
3438
  const m = AT_PICKER_PREFIX.exec(input);
@@ -3666,8 +3793,8 @@ When none of these is given AND the file is longer than ${DEFAULT_AUTO_PREVIEW_L
3666
3793
  },
3667
3794
  fn: async (args) => {
3668
3795
  const abs = safePath(args.path);
3669
- const stat = await fs.stat(abs);
3670
- if (stat.isDirectory()) {
3796
+ const stat2 = await fs.stat(abs);
3797
+ if (stat2.isDirectory()) {
3671
3798
  throw new Error(`not a file: ${args.path} (it's a directory)`);
3672
3799
  }
3673
3800
  const raw = await fs.readFile(abs);
@@ -3936,13 +4063,13 @@ Prefer \`list_directory\` for a single-level view, \`search_files\` to find spec
3936
4063
  if (nameFilter && !e.name.toLowerCase().includes(nameFilter)) continue;
3937
4064
  if (isLikelyBinaryByName(e.name)) continue;
3938
4065
  const full = pathMod.join(dir, e.name);
3939
- let stat;
4066
+ let stat2;
3940
4067
  try {
3941
- stat = await fs.stat(full);
4068
+ stat2 = await fs.stat(full);
3942
4069
  } catch {
3943
4070
  continue;
3944
4071
  }
3945
- if (stat.size > 2 * 1024 * 1024) continue;
4072
+ if (stat2.size > 2 * 1024 * 1024) continue;
3946
4073
  let raw;
3947
4074
  try {
3948
4075
  raw = await fs.readFile(full);
@@ -4923,6 +5050,8 @@ var JobRegistry = class {
4923
5050
  };
4924
5051
  this.jobs.set(id, job);
4925
5052
  let readyMatched = false;
5053
+ let recentForReady = "";
5054
+ const READY_WINDOW = 1024;
4926
5055
  const onData = (chunk) => {
4927
5056
  const s = chunk.toString();
4928
5057
  job.totalBytesWritten += s.length;
@@ -4935,8 +5064,9 @@ var JobRegistry = class {
4935
5064
  ${job.output.slice(start)}`;
4936
5065
  }
4937
5066
  if (!readyMatched) {
5067
+ recentForReady = (recentForReady + s).slice(-READY_WINDOW);
4938
5068
  for (const re of READY_SIGNALS) {
4939
- if (re.test(s) || re.test(job.output)) {
5069
+ if (re.test(recentForReady)) {
4940
5070
  readyMatched = true;
4941
5071
  job.signalReady();
4942
5072
  break;
@@ -5620,6 +5750,7 @@ ${r.output}` : header2;
5620
5750
  var DEFAULT_FETCH_MAX_CHARS = 32e3;
5621
5751
  var DEFAULT_FETCH_TIMEOUT_MS = 15e3;
5622
5752
  var DEFAULT_TOPK = 5;
5753
+ var FETCH_MAX_BYTES = 10 * 1024 * 1024;
5623
5754
  var USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36";
5624
5755
  var MOJEEK_ENDPOINT = "https://www.mojeek.com/search";
5625
5756
  async function webSearch(query, opts = {}) {
@@ -5699,7 +5830,13 @@ async function webFetch(url, opts = {}) {
5699
5830
  }
5700
5831
  if (!resp.ok) throw new Error(`web_fetch ${resp.status} for ${url}`);
5701
5832
  const contentType = resp.headers.get("content-type") ?? "";
5702
- const raw = await resp.text();
5833
+ const declaredLen = Number(resp.headers.get("content-length") ?? "");
5834
+ if (Number.isFinite(declaredLen) && declaredLen > FETCH_MAX_BYTES) {
5835
+ throw new Error(
5836
+ `web_fetch refused: content-length ${declaredLen} bytes exceeds ${FETCH_MAX_BYTES}-byte cap (${url})`
5837
+ );
5838
+ }
5839
+ const raw = await readBodyCapped(resp, FETCH_MAX_BYTES);
5703
5840
  const title = extractTitle(raw);
5704
5841
  const text = contentType.includes("text/html") ? htmlToText(raw) : raw;
5705
5842
  const truncated = text.length > maxChars;
@@ -5708,6 +5845,37 @@ async function webFetch(url, opts = {}) {
5708
5845
  [\u2026 truncated ${text.length - maxChars} chars \u2026]` : text;
5709
5846
  return { url, title, text: finalText, truncated };
5710
5847
  }
5848
+ async function readBodyCapped(resp, maxBytes) {
5849
+ if (!resp.body) return await resp.text();
5850
+ const reader = resp.body.getReader();
5851
+ const decoder = new TextDecoder("utf-8");
5852
+ let total = 0;
5853
+ let out = "";
5854
+ try {
5855
+ while (true) {
5856
+ const { value, done } = await reader.read();
5857
+ if (done) break;
5858
+ total += value.byteLength;
5859
+ if (total > maxBytes) {
5860
+ try {
5861
+ await reader.cancel();
5862
+ } catch {
5863
+ }
5864
+ throw new Error(
5865
+ `web_fetch refused: response body exceeded ${maxBytes}-byte cap (${total} bytes seen)`
5866
+ );
5867
+ }
5868
+ out += decoder.decode(value, { stream: true });
5869
+ }
5870
+ out += decoder.decode();
5871
+ } finally {
5872
+ try {
5873
+ reader.releaseLock();
5874
+ } catch {
5875
+ }
5876
+ }
5877
+ return out;
5878
+ }
5711
5879
  function htmlToText(html) {
5712
5880
  let s = html;
5713
5881
  s = s.replace(/<script[\s\S]*?<\/script>/gi, "");
@@ -6352,6 +6520,107 @@ function truncate(s, n) {
6352
6520
  return s.length > n ? `${s.slice(0, n)}\u2026` : s;
6353
6521
  }
6354
6522
 
6523
+ // src/version.ts
6524
+ import { existsSync as existsSync6, mkdirSync as mkdirSync3, readFileSync as readFileSync8, writeFileSync as writeFileSync3 } from "fs";
6525
+ import { homedir as homedir4 } from "os";
6526
+ import { dirname as dirname5, join as join7 } from "path";
6527
+ import { fileURLToPath as fileURLToPath2 } from "url";
6528
+ var REGISTRY_URL = "https://registry.npmjs.org/reasonix/latest";
6529
+ var LATEST_CACHE_TTL_MS = 24 * 60 * 60 * 1e3;
6530
+ var LATEST_FETCH_TIMEOUT_MS = 2e3;
6531
+ function readPackageVersion() {
6532
+ try {
6533
+ let dir = dirname5(fileURLToPath2(import.meta.url));
6534
+ for (let i = 0; i < 6; i++) {
6535
+ const p = join7(dir, "package.json");
6536
+ if (existsSync6(p)) {
6537
+ const pkg = JSON.parse(readFileSync8(p, "utf8"));
6538
+ if (pkg?.name === "reasonix" && typeof pkg.version === "string") {
6539
+ return pkg.version;
6540
+ }
6541
+ }
6542
+ const parent = dirname5(dir);
6543
+ if (parent === dir) break;
6544
+ dir = parent;
6545
+ }
6546
+ } catch {
6547
+ }
6548
+ return "0.0.0-dev";
6549
+ }
6550
+ var VERSION = readPackageVersion();
6551
+ function cachePath(homeDirOverride) {
6552
+ return join7(homeDirOverride ?? homedir4(), ".reasonix", "version-cache.json");
6553
+ }
6554
+ function readCache(homeDirOverride) {
6555
+ try {
6556
+ const raw = readFileSync8(cachePath(homeDirOverride), "utf8");
6557
+ const parsed = JSON.parse(raw);
6558
+ if (parsed && typeof parsed.version === "string" && typeof parsed.checkedAt === "number") {
6559
+ return parsed;
6560
+ }
6561
+ } catch {
6562
+ }
6563
+ return null;
6564
+ }
6565
+ function writeCache(entry, homeDirOverride) {
6566
+ try {
6567
+ const p = cachePath(homeDirOverride);
6568
+ mkdirSync3(dirname5(p), { recursive: true });
6569
+ writeFileSync3(p, JSON.stringify(entry), "utf8");
6570
+ } catch {
6571
+ }
6572
+ }
6573
+ async function getLatestVersion(opts = {}) {
6574
+ const ttl = opts.ttlMs ?? LATEST_CACHE_TTL_MS;
6575
+ if (!opts.force) {
6576
+ const cached2 = readCache(opts.homeDir);
6577
+ if (cached2 && Date.now() - cached2.checkedAt < ttl) return cached2.version;
6578
+ }
6579
+ const fetchImpl = opts.fetchImpl ?? globalThis.fetch;
6580
+ if (!fetchImpl) return null;
6581
+ const url = opts.registryUrl ?? REGISTRY_URL;
6582
+ const timeout = opts.timeoutMs ?? LATEST_FETCH_TIMEOUT_MS;
6583
+ const controller = new AbortController();
6584
+ const timer = setTimeout(() => controller.abort(), timeout);
6585
+ try {
6586
+ const res = await fetchImpl(url, {
6587
+ signal: controller.signal,
6588
+ headers: { accept: "application/json" }
6589
+ });
6590
+ if (!res.ok) return null;
6591
+ const body = await res.json();
6592
+ if (typeof body.version !== "string") return null;
6593
+ writeCache({ version: body.version, checkedAt: Date.now() }, opts.homeDir);
6594
+ return body.version;
6595
+ } catch {
6596
+ return null;
6597
+ } finally {
6598
+ clearTimeout(timer);
6599
+ }
6600
+ }
6601
+ function compareVersions(a, b) {
6602
+ const [aCore = "0", aPre = ""] = a.split("-", 2);
6603
+ const [bCore = "0", bPre = ""] = b.split("-", 2);
6604
+ const aParts = aCore.split(".").map((p) => Number.parseInt(p, 10) || 0);
6605
+ const bParts = bCore.split(".").map((p) => Number.parseInt(p, 10) || 0);
6606
+ for (let i = 0; i < 3; i++) {
6607
+ const diff = (aParts[i] ?? 0) - (bParts[i] ?? 0);
6608
+ if (diff !== 0) return diff;
6609
+ }
6610
+ if (!aPre && !bPre) return 0;
6611
+ if (!aPre) return 1;
6612
+ if (!bPre) return -1;
6613
+ return aPre < bPre ? -1 : aPre > bPre ? 1 : 0;
6614
+ }
6615
+ function isNpxInstall() {
6616
+ const bin = process.argv[1] ?? "";
6617
+ if (/[/\\]_npx[/\\]/.test(bin)) return true;
6618
+ if (/[/\\]\.pnpm[/\\]/.test(bin) && /dlx/i.test(bin)) return true;
6619
+ const ua = process.env.npm_config_user_agent ?? "";
6620
+ if (ua.includes("npx/")) return true;
6621
+ return false;
6622
+ }
6623
+
6355
6624
  // src/mcp/types.ts
6356
6625
  var MCP_PROTOCOL_VERSION = "2024-11-05";
6357
6626
  function isJsonRpcError(msg) {
@@ -6380,7 +6649,7 @@ var McpClient = class {
6380
6649
  nextProgressToken = 1;
6381
6650
  constructor(opts) {
6382
6651
  this.transport = opts.transport;
6383
- this.clientInfo = opts.clientInfo ?? { name: "reasonix", version: "0.3.0-dev" };
6652
+ this.clientInfo = opts.clientInfo ?? { name: "reasonix", version: VERSION };
6384
6653
  this.requestTimeoutMs = opts.requestTimeoutMs ?? 6e4;
6385
6654
  }
6386
6655
  /** Server's advertised capabilities, available after initialize(). */
@@ -7120,8 +7389,8 @@ async function trySection(load) {
7120
7389
  }
7121
7390
 
7122
7391
  // src/code/edit-blocks.ts
7123
- import { existsSync as existsSync6, mkdirSync as mkdirSync3, readFileSync as readFileSync8, unlinkSync as unlinkSync2, writeFileSync as writeFileSync3 } from "fs";
7124
- import { dirname as dirname5, resolve as resolve6 } from "path";
7392
+ import { existsSync as existsSync7, mkdirSync as mkdirSync4, readFileSync as readFileSync9, unlinkSync as unlinkSync2, writeFileSync as writeFileSync4 } from "fs";
7393
+ import { dirname as dirname6, resolve as resolve6 } from "path";
7125
7394
  var BLOCK_RE = /^(\S[^\n]*)\n<{7} SEARCH\n([\s\S]*?)\n?={7}\n([\s\S]*?)\n?>{7} REPLACE/gm;
7126
7395
  function parseEditBlocks(text) {
7127
7396
  const out = [];
@@ -7149,7 +7418,7 @@ function applyEditBlock(block, rootDir) {
7149
7418
  };
7150
7419
  }
7151
7420
  const searchEmpty = block.search.length === 0;
7152
- const exists = existsSync6(absTarget);
7421
+ const exists = existsSync7(absTarget);
7153
7422
  try {
7154
7423
  if (!exists) {
7155
7424
  if (!searchEmpty) {
@@ -7159,11 +7428,11 @@ function applyEditBlock(block, rootDir) {
7159
7428
  message: "file does not exist; to create it, use an empty SEARCH block"
7160
7429
  };
7161
7430
  }
7162
- mkdirSync3(dirname5(absTarget), { recursive: true });
7163
- writeFileSync3(absTarget, block.replace, "utf8");
7431
+ mkdirSync4(dirname6(absTarget), { recursive: true });
7432
+ writeFileSync4(absTarget, block.replace, "utf8");
7164
7433
  return { path: block.path, status: "created" };
7165
7434
  }
7166
- const content = readFileSync8(absTarget, "utf8");
7435
+ const content = readFileSync9(absTarget, "utf8");
7167
7436
  if (searchEmpty) {
7168
7437
  return {
7169
7438
  path: block.path,
@@ -7180,7 +7449,7 @@ function applyEditBlock(block, rootDir) {
7180
7449
  };
7181
7450
  }
7182
7451
  const replaced = `${content.slice(0, idx)}${block.replace}${content.slice(idx + block.search.length)}`;
7183
- writeFileSync3(absTarget, replaced, "utf8");
7452
+ writeFileSync4(absTarget, replaced, "utf8");
7184
7453
  return { path: block.path, status: "applied" };
7185
7454
  } catch (err) {
7186
7455
  return { path: block.path, status: "error", message: err.message };
@@ -7192,9 +7461,9 @@ function applyEditBlocks(blocks, rootDir) {
7192
7461
  function toWholeFileEditBlock(path5, content, rootDir) {
7193
7462
  const abs = resolve6(rootDir, path5);
7194
7463
  let search = "";
7195
- if (existsSync6(abs)) {
7464
+ if (existsSync7(abs)) {
7196
7465
  try {
7197
- search = readFileSync8(abs, "utf8");
7466
+ search = readFileSync9(abs, "utf8");
7198
7467
  } catch {
7199
7468
  search = "";
7200
7469
  }
@@ -7209,12 +7478,12 @@ function snapshotBeforeEdits(blocks, rootDir) {
7209
7478
  if (seen.has(b.path)) continue;
7210
7479
  seen.add(b.path);
7211
7480
  const abs = resolve6(absRoot, b.path);
7212
- if (!existsSync6(abs)) {
7481
+ if (!existsSync7(abs)) {
7213
7482
  snapshots.push({ path: b.path, prevContent: null });
7214
7483
  continue;
7215
7484
  }
7216
7485
  try {
7217
- snapshots.push({ path: b.path, prevContent: readFileSync8(abs, "utf8") });
7486
+ snapshots.push({ path: b.path, prevContent: readFileSync9(abs, "utf8") });
7218
7487
  } catch {
7219
7488
  snapshots.push({ path: b.path, prevContent: null });
7220
7489
  }
@@ -7234,14 +7503,14 @@ function restoreSnapshots(snapshots, rootDir) {
7234
7503
  }
7235
7504
  try {
7236
7505
  if (snap.prevContent === null) {
7237
- if (existsSync6(abs)) unlinkSync2(abs);
7506
+ if (existsSync7(abs)) unlinkSync2(abs);
7238
7507
  return {
7239
7508
  path: snap.path,
7240
7509
  status: "applied",
7241
7510
  message: "removed (the edit had created it)"
7242
7511
  };
7243
7512
  }
7244
- writeFileSync3(abs, snap.prevContent, "utf8");
7513
+ writeFileSync4(abs, snap.prevContent, "utf8");
7245
7514
  return {
7246
7515
  path: snap.path,
7247
7516
  status: "applied",
@@ -7256,114 +7525,54 @@ function sep() {
7256
7525
  return process.platform === "win32" ? "\\" : "/";
7257
7526
  }
7258
7527
 
7259
- // src/version.ts
7260
- import { existsSync as existsSync7, mkdirSync as mkdirSync4, readFileSync as readFileSync9, writeFileSync as writeFileSync4 } from "fs";
7261
- import { homedir as homedir4 } from "os";
7262
- import { dirname as dirname6, join as join7 } from "path";
7263
- import { fileURLToPath as fileURLToPath2 } from "url";
7264
- var REGISTRY_URL = "https://registry.npmjs.org/reasonix/latest";
7265
- var LATEST_CACHE_TTL_MS = 24 * 60 * 60 * 1e3;
7266
- var LATEST_FETCH_TIMEOUT_MS = 2e3;
7267
- function readPackageVersion() {
7268
- try {
7269
- let dir = dirname6(fileURLToPath2(import.meta.url));
7270
- for (let i = 0; i < 6; i++) {
7271
- const p = join7(dir, "package.json");
7272
- if (existsSync7(p)) {
7273
- const pkg = JSON.parse(readFileSync9(p, "utf8"));
7274
- if (pkg?.name === "reasonix" && typeof pkg.version === "string") {
7275
- return pkg.version;
7276
- }
7277
- }
7278
- const parent = dirname6(dir);
7279
- if (parent === dir) break;
7280
- dir = parent;
7281
- }
7282
- } catch {
7283
- }
7284
- return "0.0.0-dev";
7285
- }
7286
- var VERSION = readPackageVersion();
7287
- function cachePath(homeDirOverride) {
7288
- return join7(homeDirOverride ?? homedir4(), ".reasonix", "version-cache.json");
7528
+ // src/usage.ts
7529
+ import {
7530
+ appendFileSync as appendFileSync2,
7531
+ existsSync as existsSync8,
7532
+ mkdirSync as mkdirSync5,
7533
+ readFileSync as readFileSync10,
7534
+ statSync as statSync4,
7535
+ writeFileSync as writeFileSync5
7536
+ } from "fs";
7537
+ import { homedir as homedir5 } from "os";
7538
+ import { dirname as dirname7, join as join8 } from "path";
7539
+ function defaultUsageLogPath(homeDirOverride) {
7540
+ return join8(homeDirOverride ?? homedir5(), ".reasonix", "usage.jsonl");
7289
7541
  }
7290
- function readCache(homeDirOverride) {
7542
+ var USAGE_COMPACTION_THRESHOLD_BYTES = 5 * 1024 * 1024;
7543
+ var USAGE_RETENTION_DAYS = 365;
7544
+ function compactUsageLogIfLarge(path5, now) {
7545
+ let size;
7291
7546
  try {
7292
- const raw = readFileSync9(cachePath(homeDirOverride), "utf8");
7293
- const parsed = JSON.parse(raw);
7294
- if (parsed && typeof parsed.version === "string" && typeof parsed.checkedAt === "number") {
7295
- return parsed;
7296
- }
7547
+ size = statSync4(path5).size;
7297
7548
  } catch {
7549
+ return;
7298
7550
  }
7299
- return null;
7300
- }
7301
- function writeCache(entry, homeDirOverride) {
7551
+ if (size < USAGE_COMPACTION_THRESHOLD_BYTES) return;
7552
+ const cutoff = now - USAGE_RETENTION_DAYS * 24 * 60 * 60 * 1e3;
7553
+ let raw;
7302
7554
  try {
7303
- const p = cachePath(homeDirOverride);
7304
- mkdirSync4(dirname6(p), { recursive: true });
7305
- writeFileSync4(p, JSON.stringify(entry), "utf8");
7555
+ raw = readFileSync10(path5, "utf8");
7306
7556
  } catch {
7557
+ return;
7307
7558
  }
7308
- }
7309
- async function getLatestVersion(opts = {}) {
7310
- const ttl = opts.ttlMs ?? LATEST_CACHE_TTL_MS;
7311
- if (!opts.force) {
7312
- const cached2 = readCache(opts.homeDir);
7313
- if (cached2 && Date.now() - cached2.checkedAt < ttl) return cached2.version;
7559
+ const lines = raw.split(/\r?\n/);
7560
+ const kept = [];
7561
+ for (const line of lines) {
7562
+ if (!line.trim()) continue;
7563
+ try {
7564
+ const rec = JSON.parse(line);
7565
+ if (isValidRecord(rec) && rec.ts >= cutoff) kept.push(line);
7566
+ } catch {
7567
+ }
7314
7568
  }
7315
- const fetchImpl = opts.fetchImpl ?? globalThis.fetch;
7316
- if (!fetchImpl) return null;
7317
- const url = opts.registryUrl ?? REGISTRY_URL;
7318
- const timeout = opts.timeoutMs ?? LATEST_FETCH_TIMEOUT_MS;
7319
- const controller = new AbortController();
7320
- const timer = setTimeout(() => controller.abort(), timeout);
7569
+ if (kept.length === lines.filter((l) => l.trim()).length) return;
7321
7570
  try {
7322
- const res = await fetchImpl(url, {
7323
- signal: controller.signal,
7324
- headers: { accept: "application/json" }
7325
- });
7326
- if (!res.ok) return null;
7327
- const body = await res.json();
7328
- if (typeof body.version !== "string") return null;
7329
- writeCache({ version: body.version, checkedAt: Date.now() }, opts.homeDir);
7330
- return body.version;
7571
+ writeFileSync5(path5, kept.length > 0 ? `${kept.join("\n")}
7572
+ ` : "", "utf8");
7331
7573
  } catch {
7332
- return null;
7333
- } finally {
7334
- clearTimeout(timer);
7335
7574
  }
7336
7575
  }
7337
- function compareVersions(a, b) {
7338
- const [aCore = "0", aPre = ""] = a.split("-", 2);
7339
- const [bCore = "0", bPre = ""] = b.split("-", 2);
7340
- const aParts = aCore.split(".").map((p) => Number.parseInt(p, 10) || 0);
7341
- const bParts = bCore.split(".").map((p) => Number.parseInt(p, 10) || 0);
7342
- for (let i = 0; i < 3; i++) {
7343
- const diff = (aParts[i] ?? 0) - (bParts[i] ?? 0);
7344
- if (diff !== 0) return diff;
7345
- }
7346
- if (!aPre && !bPre) return 0;
7347
- if (!aPre) return 1;
7348
- if (!bPre) return -1;
7349
- return aPre < bPre ? -1 : aPre > bPre ? 1 : 0;
7350
- }
7351
- function isNpxInstall() {
7352
- const bin = process.argv[1] ?? "";
7353
- if (/[/\\]_npx[/\\]/.test(bin)) return true;
7354
- if (/[/\\]\.pnpm[/\\]/.test(bin) && /dlx/i.test(bin)) return true;
7355
- const ua = process.env.npm_config_user_agent ?? "";
7356
- if (ua.includes("npx/")) return true;
7357
- return false;
7358
- }
7359
-
7360
- // src/usage.ts
7361
- import { appendFileSync as appendFileSync2, existsSync as existsSync8, mkdirSync as mkdirSync5, readFileSync as readFileSync10, statSync as statSync4 } from "fs";
7362
- import { homedir as homedir5 } from "os";
7363
- import { dirname as dirname7, join as join8 } from "path";
7364
- function defaultUsageLogPath(homeDirOverride) {
7365
- return join8(homeDirOverride ?? homedir5(), ".reasonix", "usage.jsonl");
7366
- }
7367
7576
  function appendUsage(input) {
7368
7577
  const record = {
7369
7578
  ts: input.now ?? Date.now(),
@@ -7383,6 +7592,7 @@ function appendUsage(input) {
7383
7592
  mkdirSync5(dirname7(path5), { recursive: true });
7384
7593
  appendFileSync2(path5, `${JSON.stringify(record)}
7385
7594
  `, "utf8");
7595
+ compactUsageLogIfLarge(path5, record.ts);
7386
7596
  } catch {
7387
7597
  }
7388
7598
  return record;
@@ -7521,7 +7731,7 @@ import { Box as Box22, Static, Text as Text20, useStdout as useStdout8 } from "i
7521
7731
  import React24, { useCallback as useCallback4, useEffect as useEffect6, useMemo as useMemo3, useRef as useRef6, useState as useState10 } from "react";
7522
7732
 
7523
7733
  // src/code/pending-edits.ts
7524
- import { existsSync as existsSync9, mkdirSync as mkdirSync6, readFileSync as readFileSync11, unlinkSync as unlinkSync3, writeFileSync as writeFileSync5 } from "fs";
7734
+ import { existsSync as existsSync9, mkdirSync as mkdirSync6, readFileSync as readFileSync11, unlinkSync as unlinkSync3, writeFileSync as writeFileSync6 } from "fs";
7525
7735
  import { dirname as dirname8, join as join9 } from "path";
7526
7736
  function pendingEditsPath(sessionName) {
7527
7737
  return join9(sessionsDir(), `${sanitizeName(sessionName)}.pending.json`);
@@ -7535,7 +7745,7 @@ function savePendingEdits(sessionName, blocks) {
7535
7745
  return;
7536
7746
  }
7537
7747
  mkdirSync6(dirname8(path5), { recursive: true });
7538
- writeFileSync5(path5, JSON.stringify(blocks, null, 2), "utf8");
7748
+ writeFileSync6(path5, JSON.stringify(blocks, null, 2), "utf8");
7539
7749
  } catch {
7540
7750
  }
7541
7751
  }
@@ -7581,7 +7791,7 @@ import {
7581
7791
  renameSync,
7582
7792
  statSync as statSync5,
7583
7793
  unlinkSync as unlinkSync4,
7584
- writeFileSync as writeFileSync6
7794
+ writeFileSync as writeFileSync7
7585
7795
  } from "fs";
7586
7796
  import { dirname as dirname9, join as join10 } from "path";
7587
7797
  function planStatePath(sessionName) {
@@ -7638,7 +7848,7 @@ function savePlanState(sessionName, steps, completedStepIds, extras) {
7638
7848
  };
7639
7849
  if (extras?.body) state.body = extras.body;
7640
7850
  if (extras?.summary) state.summary = extras.summary;
7641
- writeFileSync6(path5, `${JSON.stringify(state, null, 2)}
7851
+ writeFileSync7(path5, `${JSON.stringify(state, null, 2)}
7642
7852
  `, "utf8");
7643
7853
  } catch (err) {
7644
7854
  process.stderr.write(
@@ -7906,7 +8116,7 @@ import {
7906
8116
  readFileSync as readFileSync14,
7907
8117
  readSync,
7908
8118
  statSync as statSync6,
7909
- writeFileSync as writeFileSync7
8119
+ writeFileSync as writeFileSync8
7910
8120
  } from "fs";
7911
8121
  import { dirname as dirname11, isAbsolute as isAbsolute4, resolve as resolve7, sep as sep2 } from "path";
7912
8122
  var MAX_BYTES = 4 * 1024 * 1024;
@@ -7986,14 +8196,14 @@ async function handleFile(method, rest, body, ctx) {
7986
8196
  if (!existsSync11(target)) {
7987
8197
  return { status: 404, body: { error: "file not found" } };
7988
8198
  }
7989
- const stat = statSync6(target);
7990
- if (stat.isDirectory()) {
8199
+ const stat2 = statSync6(target);
8200
+ if (stat2.isDirectory()) {
7991
8201
  return { status: 400, body: { error: "path is a directory" } };
7992
8202
  }
7993
- if (stat.size > MAX_BYTES) {
8203
+ if (stat2.size > MAX_BYTES) {
7994
8204
  return {
7995
8205
  status: 413,
7996
- body: { error: `file too large (${stat.size} bytes; cap ${MAX_BYTES})` }
8206
+ body: { error: `file too large (${stat2.size} bytes; cap ${MAX_BYTES})` }
7997
8207
  };
7998
8208
  }
7999
8209
  if (looksBinary(target)) {
@@ -8008,8 +8218,8 @@ async function handleFile(method, rest, body, ctx) {
8008
8218
  body: {
8009
8219
  path: requested,
8010
8220
  absolute: target,
8011
- size: stat.size,
8012
- mtime: stat.mtime.getTime(),
8221
+ size: stat2.size,
8222
+ mtime: stat2.mtime.getTime(),
8013
8223
  content
8014
8224
  }
8015
8225
  };
@@ -8029,20 +8239,20 @@ async function handleFile(method, rest, body, ctx) {
8029
8239
  if (!existsSync11(parent)) {
8030
8240
  mkdirSync8(parent, { recursive: true });
8031
8241
  }
8032
- writeFileSync7(target, content, "utf8");
8242
+ writeFileSync8(target, content, "utf8");
8033
8243
  ctx.audit?.({
8034
8244
  ts: Date.now(),
8035
8245
  action: "save-file",
8036
8246
  payload: { path: requested, bytes: Buffer.byteLength(content, "utf8") }
8037
8247
  });
8038
- const stat = statSync6(target);
8248
+ const stat2 = statSync6(target);
8039
8249
  return {
8040
8250
  status: 200,
8041
8251
  body: {
8042
8252
  saved: true,
8043
8253
  path: requested,
8044
- size: stat.size,
8045
- mtime: stat.mtime.getTime()
8254
+ size: stat2.size,
8255
+ mtime: stat2.mtime.getTime()
8046
8256
  }
8047
8257
  };
8048
8258
  }
@@ -8139,7 +8349,7 @@ async function handleHealth(method, _rest, _body, ctx) {
8139
8349
  }
8140
8350
 
8141
8351
  // src/server/api/hooks.ts
8142
- import { existsSync as existsSync13, mkdirSync as mkdirSync9, readFileSync as readFileSync15, writeFileSync as writeFileSync8 } from "fs";
8352
+ import { existsSync as existsSync13, mkdirSync as mkdirSync9, readFileSync as readFileSync15, writeFileSync as writeFileSync9 } from "fs";
8143
8353
  import { dirname as dirname12 } from "path";
8144
8354
  function parseBody3(raw) {
8145
8355
  if (!raw) return {};
@@ -8164,7 +8374,7 @@ function writeSettingsFile(path5, hooksBlock) {
8164
8374
  const existing = readSettingsFile2(path5);
8165
8375
  existing.hooks = hooksBlock;
8166
8376
  mkdirSync9(dirname12(path5), { recursive: true });
8167
- writeFileSync8(path5, `${JSON.stringify(existing, null, 2)}
8377
+ writeFileSync9(path5, `${JSON.stringify(existing, null, 2)}
8168
8378
  `, "utf8");
8169
8379
  }
8170
8380
  async function handleHooks(method, rest, body, ctx) {
@@ -8345,7 +8555,7 @@ import {
8345
8555
  readdirSync as readdirSync5,
8346
8556
  statSync as statSync8,
8347
8557
  unlinkSync as unlinkSync5,
8348
- writeFileSync as writeFileSync9
8558
+ writeFileSync as writeFileSync10
8349
8559
  } from "fs";
8350
8560
  import { homedir as homedir7 } from "os";
8351
8561
  import { dirname as dirname13, join as join13, resolve as resolvePath } from "path";
@@ -8372,11 +8582,11 @@ function listMemoryFiles(dir) {
8372
8582
  if (!existsSync14(dir)) return [];
8373
8583
  try {
8374
8584
  return readdirSync5(dir).filter((f) => f.endsWith(".md")).map((f) => {
8375
- const stat = statSync8(join13(dir, f));
8585
+ const stat2 = statSync8(join13(dir, f));
8376
8586
  return {
8377
8587
  name: f.replace(/\.md$/, ""),
8378
- size: stat.size,
8379
- mtime: stat.mtime.getTime()
8588
+ size: stat2.size,
8589
+ mtime: stat2.mtime.getTime()
8380
8590
  };
8381
8591
  }).sort((a, b) => b.mtime - a.mtime);
8382
8592
  } catch {
@@ -8436,7 +8646,7 @@ async function handleMemory(method, rest, body, ctx) {
8436
8646
  if (!cwd2) return { status: 503, body: { error: "no active project" } };
8437
8647
  const path5 = join13(cwd2, PROJECT_MEMORY_FILE);
8438
8648
  mkdirSync10(dirname13(path5), { recursive: true });
8439
- writeFileSync9(path5, contents, "utf8");
8649
+ writeFileSync10(path5, contents, "utf8");
8440
8650
  ctx.audit?.({ ts: Date.now(), action: "save-memory", payload: { scope, path: path5 } });
8441
8651
  return { status: 200, body: { saved: true, path: path5 } };
8442
8652
  }
@@ -8445,7 +8655,7 @@ async function handleMemory(method, rest, body, ctx) {
8445
8655
  if (!dir) return { status: 503, body: { error: "no project root for project-mem" } };
8446
8656
  mkdirSync10(dir, { recursive: true });
8447
8657
  const path5 = join13(dir, `${name}.md`);
8448
- writeFileSync9(path5, contents, "utf8");
8658
+ writeFileSync10(path5, contents, "utf8");
8449
8659
  ctx.audit?.({ ts: Date.now(), action: "save-memory", payload: { scope, name, path: path5 } });
8450
8660
  return { status: 200, body: { saved: true, path: path5 } };
8451
8661
  }
@@ -8783,13 +8993,13 @@ async function* walkChunks(root, opts = {}) {
8783
8993
  const ext = path.extname(name).toLowerCase();
8784
8994
  if (BINARY_EXTS.has(ext)) continue;
8785
8995
  const abs = path.join(dir, name);
8786
- let stat;
8996
+ let stat2;
8787
8997
  try {
8788
- stat = await fs2.stat(abs);
8998
+ stat2 = await fs2.stat(abs);
8789
8999
  } catch {
8790
9000
  continue;
8791
9001
  }
8792
- if (stat.size > maxFileBytes) continue;
9002
+ if (stat2.size > maxFileBytes) continue;
8793
9003
  let text;
8794
9004
  try {
8795
9005
  text = await fs2.readFile(abs, "utf8");
@@ -9165,8 +9375,8 @@ async function buildIndex(root, opts = {}) {
9165
9375
  const abs = path3.join(root, chunk.path);
9166
9376
  let mtimeMs = 0;
9167
9377
  try {
9168
- const stat = await fs4.stat(abs);
9169
- mtimeMs = stat.mtimeMs;
9378
+ const stat2 = await fs4.stat(abs);
9379
+ mtimeMs = stat2.mtimeMs;
9170
9380
  } catch {
9171
9381
  continue;
9172
9382
  }
@@ -9969,7 +10179,7 @@ import {
9969
10179
  readdirSync as readdirSync6,
9970
10180
  rmSync,
9971
10181
  statSync as statSync9,
9972
- writeFileSync as writeFileSync10
10182
+ writeFileSync as writeFileSync11
9973
10183
  } from "fs";
9974
10184
  import { homedir as homedir8 } from "os";
9975
10185
  import { dirname as dirname14, join as join14 } from "path";
@@ -10008,14 +10218,14 @@ function listSkills(dir, scope) {
10008
10218
  const skillPath = join14(dir, entry, SKILL_FILE);
10009
10219
  if (!existsSync16(skillPath)) continue;
10010
10220
  try {
10011
- const stat = statSync9(skillPath);
10221
+ const stat2 = statSync9(skillPath);
10012
10222
  const raw = readFileSync18(skillPath, "utf8");
10013
10223
  const item = {
10014
10224
  name: entry,
10015
10225
  scope,
10016
10226
  path: skillPath,
10017
- size: stat.size,
10018
- mtime: stat.mtime.getTime()
10227
+ size: stat2.size,
10228
+ mtime: stat2.mtime.getTime()
10019
10229
  };
10020
10230
  const desc = parseFrontmatterDescription(raw);
10021
10231
  if (desc) item.description = desc;
@@ -10084,7 +10294,7 @@ async function handleSkills(method, rest, body, ctx) {
10084
10294
  return { status: 400, body: { error: "body (string) required" } };
10085
10295
  }
10086
10296
  mkdirSync11(dirname14(skillPath), { recursive: true });
10087
- writeFileSync10(skillPath, contents, "utf8");
10297
+ writeFileSync11(skillPath, contents, "utf8");
10088
10298
  ctx.audit?.({
10089
10299
  ts: Date.now(),
10090
10300
  action: "save-skill",
@@ -11613,6 +11823,22 @@ function extOf(p) {
11613
11823
  const m = /\.[^./\\]+$/.exec(p);
11614
11824
  return m ? m[0] : "";
11615
11825
  }
11826
+ var lineCountCache = /* @__PURE__ */ new Map();
11827
+ var LINE_COUNT_CACHE_LIMIT = 256;
11828
+ function getCachedLineCount(fullPath, mtimeMs) {
11829
+ const hit = lineCountCache.get(fullPath);
11830
+ if (!hit || hit.mtimeMs !== mtimeMs) return null;
11831
+ lineCountCache.delete(fullPath);
11832
+ lineCountCache.set(fullPath, hit);
11833
+ return hit.lineCount;
11834
+ }
11835
+ function setCachedLineCount(fullPath, mtimeMs, lineCount) {
11836
+ if (lineCountCache.size >= LINE_COUNT_CACHE_LIMIT) {
11837
+ const oldest = lineCountCache.keys().next().value;
11838
+ if (oldest !== void 0) lineCountCache.delete(oldest);
11839
+ }
11840
+ lineCountCache.set(fullPath, { mtimeMs, lineCount });
11841
+ }
11616
11842
  function validateCitation(url, projectRoot) {
11617
11843
  const parts = parseCitationUrl(url);
11618
11844
  if (!parts || !parts.path) return { ok: false, reason: "empty path" };
@@ -11624,23 +11850,26 @@ function validateCitation(url, projectRoot) {
11624
11850
  ...siblings.map((ext) => baseFullPath.replace(/\.[^./\\]+$/, ext))
11625
11851
  ];
11626
11852
  let fullPath = baseFullPath;
11627
- let stat = null;
11853
+ let stat2 = null;
11628
11854
  for (const candidate of candidates) {
11629
11855
  try {
11630
- stat = statSync11(candidate);
11856
+ stat2 = statSync11(candidate);
11631
11857
  fullPath = candidate;
11632
11858
  break;
11633
11859
  } catch {
11634
11860
  }
11635
11861
  }
11636
- if (!stat) return { ok: false, reason: "file not found" };
11637
- if (!stat.isFile()) return { ok: false, reason: "not a file" };
11862
+ if (!stat2) return { ok: false, reason: "file not found" };
11863
+ if (!stat2.isFile()) return { ok: false, reason: "not a file" };
11638
11864
  if (parts.startLine === void 0) return { ok: true };
11639
- let lineCount;
11640
- try {
11641
- lineCount = readFileSync19(fullPath, "utf8").split("\n").length;
11642
- } catch {
11643
- return { ok: false, reason: "unreadable" };
11865
+ let lineCount = getCachedLineCount(fullPath, stat2.mtimeMs);
11866
+ if (lineCount === null) {
11867
+ try {
11868
+ lineCount = readFileSync19(fullPath, "utf8").split("\n").length;
11869
+ } catch {
11870
+ return { ok: false, reason: "unreadable" };
11871
+ }
11872
+ setCachedLineCount(fullPath, stat2.mtimeMs, lineCount);
11644
11873
  }
11645
11874
  if (parts.startLine < 1 || parts.startLine > lineCount) {
11646
11875
  return { ok: false, reason: `line ${parts.startLine} > ${lineCount}` };
@@ -12216,23 +12445,33 @@ function gradientCells(width, glyph = GLYPH.block) {
12216
12445
 
12217
12446
  // src/cli/ui/ticker.tsx
12218
12447
  import React10, { createContext as createContext2, useContext as useContext2, useEffect as useEffect2, useState as useState3 } from "react";
12219
- var TICK_MS = 120;
12220
- var TickContext = createContext2(0);
12448
+ var FAST_TICK_MS = 120;
12449
+ var SLOW_TICK_MS = 1e3;
12450
+ var FastTickContext = createContext2(0);
12451
+ var SlowTickContext = createContext2(0);
12221
12452
  function TickerProvider({ children, disabled }) {
12222
- const [tick, setTick] = useState3(0);
12453
+ const [fast, setFast] = useState3(0);
12454
+ const [slow, setSlow] = useState3(0);
12223
12455
  useEffect2(() => {
12224
12456
  if (disabled) return;
12225
- const id = setInterval(() => setTick((t2) => t2 + 1), TICK_MS);
12226
- return () => clearInterval(id);
12457
+ const fastId = setInterval(() => setFast((t2) => t2 + 1), FAST_TICK_MS);
12458
+ const slowId = setInterval(() => setSlow((t2) => t2 + 1), SLOW_TICK_MS);
12459
+ return () => {
12460
+ clearInterval(fastId);
12461
+ clearInterval(slowId);
12462
+ };
12227
12463
  }, [disabled]);
12228
- return /* @__PURE__ */ React10.createElement(TickContext.Provider, { value: tick }, children);
12464
+ return /* @__PURE__ */ React10.createElement(FastTickContext.Provider, { value: fast }, /* @__PURE__ */ React10.createElement(SlowTickContext.Provider, { value: slow }, children));
12229
12465
  }
12230
12466
  function useTick() {
12231
- return useContext2(TickContext);
12467
+ return useContext2(FastTickContext);
12468
+ }
12469
+ function useSlowTick() {
12470
+ return useContext2(SlowTickContext);
12232
12471
  }
12233
12472
  function useElapsedSeconds() {
12234
12473
  const [start] = useState3(() => Date.now());
12235
- useTick();
12474
+ useSlowTick();
12236
12475
  return Math.floor((Date.now() - start) / 1e3);
12237
12476
  }
12238
12477
 
@@ -12425,7 +12664,7 @@ var EventRow = React11.memo(function EventRow2({
12425
12664
  event.branch ? /* @__PURE__ */ React11.createElement(BranchBlock, { branch: event.branch }) : null,
12426
12665
  event.reasoning ? /* @__PURE__ */ React11.createElement(ReasoningBlock, { reasoning: event.reasoning }) : null,
12427
12666
  !isPlanStateEmpty(event.planState) ? /* @__PURE__ */ React11.createElement(PlanStateBlock, { planState: event.planState }) : null,
12428
- event.text ? /* @__PURE__ */ React11.createElement(Markdown, { text: event.text, projectRoot }) : /* @__PURE__ */ React11.createElement(Text8, { dimColor: true }, "(no content)"),
12667
+ event.text ? /* @__PURE__ */ React11.createElement(Markdown, { text: event.text, projectRoot }) : /* @__PURE__ */ React11.createElement(Text8, { dimColor: true }, "(empty body \u2014 likely tool-call only)"),
12429
12668
  event.stats ? /* @__PURE__ */ React11.createElement(StatsLine, { stats: event.stats }) : null,
12430
12669
  event.repair ? /* @__PURE__ */ React11.createElement(Text8, { color: COLOR.accent }, event.repair) : null
12431
12670
  ));
@@ -12563,9 +12802,7 @@ function Elapsed() {
12563
12802
  return /* @__PURE__ */ React11.createElement(Text8, { dimColor: true }, `${mm}:${ss}`);
12564
12803
  }
12565
12804
  function PulsingAssistantGlyph() {
12566
- const tick = useTick();
12567
- const on = Math.floor(tick / 4) % 2 === 0;
12568
- return /* @__PURE__ */ React11.createElement(Text8, { color: "green", bold: true }, on ? ROLE_GLYPH.assistant : ROLE_GLYPH.assistantPulse);
12805
+ return /* @__PURE__ */ React11.createElement(Text8, { color: "green", bold: true }, ROLE_GLYPH.assistant);
12569
12806
  }
12570
12807
  function StreamingAssistant({ event }) {
12571
12808
  if (event.branchProgress) {
@@ -12582,6 +12819,7 @@ function StreamingAssistant({ event }) {
12582
12819
  const preFirstByte = !event.text && !event.reasoning && !toolCallBuild;
12583
12820
  const reasoningOnly = !event.text && !!event.reasoning && !toolCallBuild;
12584
12821
  const toolCallOnly = !event.text && !event.reasoning && !!toolCallBuild;
12822
+ const PILL_WIDTH = 8;
12585
12823
  let pillBg;
12586
12824
  let pillText;
12587
12825
  let label;
@@ -12609,6 +12847,7 @@ function StreamingAssistant({ event }) {
12609
12847
  }
12610
12848
  label = parts.join(" \xB7 ");
12611
12849
  }
12850
+ pillText = pillText.padEnd(PILL_WIDTH);
12612
12851
  return /* @__PURE__ */ React11.createElement(Box9, { flexDirection: "column", marginTop: 1 }, /* @__PURE__ */ React11.createElement(Box9, null, /* @__PURE__ */ React11.createElement(PulsingAssistantGlyph, null), /* @__PURE__ */ React11.createElement(Text8, null, " "), /* @__PURE__ */ React11.createElement(Pulse, null), /* @__PURE__ */ React11.createElement(Text8, null, " "), /* @__PURE__ */ React11.createElement(Text8, { backgroundColor: pillBg, color: "black", bold: true }, ` ${pillText} `), /* @__PURE__ */ React11.createElement(Text8, { dimColor: true }, ` ${label} `), /* @__PURE__ */ React11.createElement(Elapsed, null)), reasoningTail ? /* @__PURE__ */ React11.createElement(Box9, { paddingLeft: 3 }, /* @__PURE__ */ React11.createElement(Text8, { color: "#c4b5fd", italic: true, dimColor: true }, "\u21B3 ", reasoningTail)) : null, tail ? /* @__PURE__ */ React11.createElement(Box9, { paddingLeft: 3 }, /* @__PURE__ */ React11.createElement(Text8, { dimColor: true }, "\u25B8 ", tail)) : preFirstByte ? (
12613
12852
  // Non-dim amber: first-time users misread the dim version as
12614
12853
  // "app frozen". The reassurance has to be VISIBLE to do its job.
@@ -12631,7 +12870,8 @@ function formatReadyTail(tb) {
12631
12870
  return ` \xB7 ${n} ready`;
12632
12871
  }
12633
12872
  function lastLine(s, maxChars) {
12634
- const flat = s.replace(/\s+/g, " ").trim();
12873
+ const tailSlice = s.length > maxChars * 4 ? s.slice(-maxChars * 4) : s;
12874
+ const flat = tailSlice.replace(/\s+/g, " ").trim();
12635
12875
  if (!flat) return "";
12636
12876
  return flat.length <= maxChars ? flat : `\u2026${flat.slice(-maxChars)}`;
12637
12877
  }
@@ -12658,7 +12898,7 @@ function ModeStatusBar({
12658
12898
  undoArmed,
12659
12899
  jobs: jobs2
12660
12900
  }) {
12661
- useTick();
12901
+ useSlowTick();
12662
12902
  const running = jobs2?.runningCount() ?? 0;
12663
12903
  const jobsTag = running > 0 ? /* @__PURE__ */ React12.createElement(Text9, { color: "yellow", bold: true }, ` \xB7 \u23F5 ${running} job${running === 1 ? "" : "s"}`) : null;
12664
12904
  if (planMode) {
@@ -12685,7 +12925,7 @@ function ModePill({
12685
12925
  function UndoBanner({
12686
12926
  banner
12687
12927
  }) {
12688
- useTick();
12928
+ useSlowTick();
12689
12929
  const remainingMs = Math.max(0, banner.expiresAt - Date.now());
12690
12930
  const remainingSec = Math.ceil(remainingMs / 1e3);
12691
12931
  const ok = banner.results.filter((r) => r.status === "applied" || r.status === "created").length;
@@ -14165,7 +14405,7 @@ function describeRepair(repair) {
14165
14405
  }
14166
14406
 
14167
14407
  // src/cli/ui/hash-memory.ts
14168
- import { appendFileSync as appendFileSync3, existsSync as existsSync18, mkdirSync as mkdirSync12, readFileSync as readFileSync20, writeFileSync as writeFileSync11 } from "fs";
14408
+ import { appendFileSync as appendFileSync3, existsSync as existsSync18, mkdirSync as mkdirSync12, readFileSync as readFileSync20, writeFileSync as writeFileSync12 } from "fs";
14169
14409
  import { homedir as homedir9 } from "os";
14170
14410
  import { dirname as dirname15, join as join17 } from "path";
14171
14411
  var PROJECT_HEADER = `# Reasonix project memory
@@ -14216,7 +14456,7 @@ function appendBulletToFile(path5, note, newFileHeader) {
14216
14456
  `;
14217
14457
  if (!existsSync18(path5)) {
14218
14458
  mkdirSync12(dirname15(path5), { recursive: true });
14219
- writeFileSync11(path5, `${newFileHeader}${bullet}`, "utf8");
14459
+ writeFileSync12(path5, `${newFileHeader}${bullet}`, "utf8");
14220
14460
  return { path: path5, created: true };
14221
14461
  }
14222
14462
  let prefix = "";
@@ -14662,6 +14902,11 @@ var SLASH_COMMANDS = [
14662
14902
  },
14663
14903
  { cmd: "sessions", summary: "list saved sessions (current marked with \u25B8)" },
14664
14904
  { cmd: "forget", summary: "delete the current session from disk" },
14905
+ {
14906
+ cmd: "prune-sessions",
14907
+ summary: "delete sessions idle \u2265N days (default 90) \u2014 frees disk on long-time installs",
14908
+ argsHint: "[days]"
14909
+ },
14665
14910
  { cmd: "setup", summary: "reminds you to exit and run `reasonix setup`" },
14666
14911
  {
14667
14912
  cmd: "semantic",
@@ -16631,9 +16876,9 @@ async function indexFileExists(rootDir) {
16631
16876
  async function readIndexMeta(rootDir) {
16632
16877
  const dataPath = path4.join(rootDir, ".reasonix", "semantic", "index.jsonl");
16633
16878
  try {
16634
- const stat = await fs5.stat(dataPath);
16635
- if (stat.size > 10 * 1024 * 1024) {
16636
- return { chunks: Math.round(stat.size / 500), files: 0 };
16879
+ const stat2 = await fs5.stat(dataPath);
16880
+ if (stat2.size > 10 * 1024 * 1024) {
16881
+ return { chunks: Math.round(stat2.size / 500), files: 0 };
16637
16882
  }
16638
16883
  const raw = await fs5.readFile(dataPath, "utf8");
16639
16884
  const seenPaths = /* @__PURE__ */ new Set();
@@ -16657,6 +16902,7 @@ var handlers13 = {
16657
16902
  };
16658
16903
 
16659
16904
  // src/cli/ui/slash/handlers/sessions.ts
16905
+ var STALE_THRESHOLD_DAYS = 90;
16660
16906
  var sessions = (_args, loop2) => {
16661
16907
  const items = listSessions();
16662
16908
  if (items.length === 0) {
@@ -16664,17 +16910,28 @@ var sessions = (_args, loop2) => {
16664
16910
  info: "no saved sessions yet \u2014 chat normally and your messages will be saved automatically"
16665
16911
  };
16666
16912
  }
16913
+ const now = Date.now();
16667
16914
  const lines = ["Saved sessions:"];
16915
+ let staleCount = 0;
16668
16916
  for (const s of items) {
16669
16917
  const sizeKb = (s.size / 1024).toFixed(1);
16670
16918
  const when = s.mtime.toISOString().replace("T", " ").slice(0, 16);
16671
16919
  const marker = s.name === loop2.sessionName ? "\u25B8" : " ";
16920
+ const ageDays = Math.floor((now - s.mtime.getTime()) / (24 * 60 * 60 * 1e3));
16921
+ const isStale = ageDays >= STALE_THRESHOLD_DAYS;
16922
+ const ageTag = isStale ? ` (${ageDays}d \u2014 stale)` : "";
16923
+ if (isStale) staleCount++;
16672
16924
  lines.push(
16673
- ` ${marker} ${s.name.padEnd(22)} ${String(s.messageCount).padStart(5)} msgs ${sizeKb.padStart(7)} KB ${when}`
16925
+ ` ${marker} ${s.name.padEnd(22)} ${String(s.messageCount).padStart(5)} msgs ${sizeKb.padStart(7)} KB ${when}${ageTag}`
16674
16926
  );
16675
16927
  }
16676
16928
  lines.push("");
16677
16929
  lines.push("Resume with: reasonix chat --session <name>");
16930
+ if (staleCount > 0) {
16931
+ lines.push(
16932
+ `${staleCount} session${staleCount === 1 ? "" : "s"} idle \u2265${STALE_THRESHOLD_DAYS} days \u2014 /prune-sessions to remove`
16933
+ );
16934
+ }
16678
16935
  return { info: lines.join("\n") };
16679
16936
  };
16680
16937
  var forget = (_args, loop2) => {
@@ -16687,9 +16944,26 @@ var forget = (_args, loop2) => {
16687
16944
  info: ok ? `\u25B8 deleted session "${name}" \u2014 current screen still shows the conversation, but next launch starts fresh` : `could not delete session "${name}" (already gone?)`
16688
16945
  };
16689
16946
  };
16947
+ var pruneSessions = (args) => {
16948
+ const raw = args?.[0];
16949
+ const days = raw ? Number.parseInt(raw, 10) : STALE_THRESHOLD_DAYS;
16950
+ if (!Number.isFinite(days) || days < 1) {
16951
+ return {
16952
+ info: `\u25B8 usage: /prune-sessions [days] \u2014 defaults to ${STALE_THRESHOLD_DAYS}, must be \u22651`
16953
+ };
16954
+ }
16955
+ const removed = pruneStaleSessions(days);
16956
+ if (removed.length === 0) {
16957
+ return { info: `\u25B8 nothing to prune \u2014 no sessions idle \u2265${days} days` };
16958
+ }
16959
+ return {
16960
+ info: `\u25B8 pruned ${removed.length} session${removed.length === 1 ? "" : "s"} idle \u2265${days} days: ${removed.join(", ")}`
16961
+ };
16962
+ };
16690
16963
  var handlers14 = {
16691
16964
  sessions,
16692
- forget
16965
+ forget,
16966
+ "prune-sessions": pruneSessions
16693
16967
  };
16694
16968
 
16695
16969
  // src/cli/ui/slash/handlers/skill.ts
@@ -16815,13 +17089,21 @@ function useCompletionPickers({
16815
17089
  });
16816
17090
  }, [slashMatches]);
16817
17091
  const [atSelected, setAtSelected] = useState6(0);
16818
- const atFiles = useMemo2(() => {
16819
- if (!codeMode) return [];
16820
- try {
16821
- return listFilesWithStatsSync(rootDir, { maxResults: 500 });
16822
- } catch {
16823
- return [];
17092
+ const [atFiles, setAtFiles] = useState6([]);
17093
+ useEffect3(() => {
17094
+ if (!codeMode) {
17095
+ setAtFiles([]);
17096
+ return;
16824
17097
  }
17098
+ let cancelled = false;
17099
+ listFilesWithStatsAsync(rootDir, { maxResults: 500 }).then((files) => {
17100
+ if (!cancelled) setAtFiles(files);
17101
+ }).catch(() => {
17102
+ if (!cancelled) setAtFiles([]);
17103
+ });
17104
+ return () => {
17105
+ cancelled = true;
17106
+ };
16825
17107
  }, [codeMode, rootDir]);
16826
17108
  const recentFilesRef = useRef3([]);
16827
17109
  const recordRecentFile = useCallback((p) => {
@@ -17270,7 +17552,13 @@ function useSubagent({ session, setHistorical }) {
17270
17552
  }
17271
17553
 
17272
17554
  // src/cli/ui/App.tsx
17273
- var FLUSH_INTERVAL_MS = 100;
17555
+ var FLUSH_INTERVAL_MS = (() => {
17556
+ const raw = process.env.REASONIX_FLUSH_MS;
17557
+ if (!raw) return 33;
17558
+ const parsed = Number(raw);
17559
+ if (!Number.isFinite(parsed) || parsed < 16 || parsed > 1e3) return 33;
17560
+ return Math.round(parsed);
17561
+ })();
17274
17562
  var PLAIN_UI = process.env.REASONIX_UI === "plain";
17275
17563
  function LoopStatusRow({
17276
17564
  loop: loop2
@@ -20201,7 +20489,7 @@ async function codeCommand(opts = {}) {
20201
20489
  }
20202
20490
 
20203
20491
  // src/cli/commands/diff.ts
20204
- import { writeFileSync as writeFileSync12 } from "fs";
20492
+ import { writeFileSync as writeFileSync13 } from "fs";
20205
20493
  import { basename as basename3 } from "path";
20206
20494
  import { render as render2 } from "ink";
20207
20495
  import React30 from "react";
@@ -20348,7 +20636,7 @@ async function diffCommand(opts) {
20348
20636
  if (wantMarkdown) {
20349
20637
  console.log(renderSummaryTable(report));
20350
20638
  const md = renderMarkdown(report);
20351
- writeFileSync12(opts.mdPath, md, "utf8");
20639
+ writeFileSync13(opts.mdPath, md, "utf8");
20352
20640
  console.log(`
20353
20641
  markdown report written to ${opts.mdPath}`);
20354
20642
  return;