reasonix 0.12.16 → 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/index.js CHANGED
@@ -532,6 +532,7 @@ function matchesTool(hook, toolName) {
532
532
  return false;
533
533
  }
534
534
  }
535
+ var HOOK_OUTPUT_CAP_BYTES = 256 * 1024;
535
536
  function defaultSpawner(input) {
536
537
  return new Promise((resolve9) => {
537
538
  const child = spawn(input.command, {
@@ -539,8 +540,11 @@ function defaultSpawner(input) {
539
540
  shell: true,
540
541
  stdio: ["pipe", "pipe", "pipe"]
541
542
  });
542
- let stdout = "";
543
- let stderr = "";
543
+ const stdoutChunks = [];
544
+ const stderrChunks = [];
545
+ let stdoutBytes = 0;
546
+ let stderrBytes = 0;
547
+ let truncated = false;
544
548
  let timedOut = false;
545
549
  const timer = setTimeout(() => {
546
550
  timedOut = true;
@@ -552,29 +556,46 @@ function defaultSpawner(input) {
552
556
  }
553
557
  }, 500);
554
558
  }, input.timeoutMs);
555
- child.stdout.on("data", (chunk) => {
556
- stdout += chunk.toString("utf8");
557
- });
558
- child.stderr.on("data", (chunk) => {
559
- stderr += chunk.toString("utf8");
560
- });
559
+ const onChunk = (kind, chunk) => {
560
+ const target = kind === "stdout" ? stdoutChunks : stderrChunks;
561
+ const seen = kind === "stdout" ? stdoutBytes : stderrBytes;
562
+ if (seen >= HOOK_OUTPUT_CAP_BYTES) {
563
+ truncated = true;
564
+ return;
565
+ }
566
+ const remaining = HOOK_OUTPUT_CAP_BYTES - seen;
567
+ if (chunk.length > remaining) {
568
+ target.push(chunk.subarray(0, remaining));
569
+ if (kind === "stdout") stdoutBytes = HOOK_OUTPUT_CAP_BYTES;
570
+ else stderrBytes = HOOK_OUTPUT_CAP_BYTES;
571
+ truncated = true;
572
+ } else {
573
+ target.push(chunk);
574
+ if (kind === "stdout") stdoutBytes += chunk.length;
575
+ else stderrBytes += chunk.length;
576
+ }
577
+ };
578
+ child.stdout.on("data", (chunk) => onChunk("stdout", chunk));
579
+ child.stderr.on("data", (chunk) => onChunk("stderr", chunk));
561
580
  child.once("error", (err) => {
562
581
  clearTimeout(timer);
563
582
  resolve9({
564
583
  exitCode: null,
565
- stdout,
566
- stderr,
584
+ stdout: Buffer.concat(stdoutChunks).toString("utf8"),
585
+ stderr: Buffer.concat(stderrChunks).toString("utf8"),
567
586
  timedOut: false,
568
- spawnError: err
587
+ spawnError: err,
588
+ truncated: truncated || void 0
569
589
  });
570
590
  });
571
591
  child.once("close", (code) => {
572
592
  clearTimeout(timer);
573
593
  resolve9({
574
594
  exitCode: code,
575
- stdout: stdout.trim(),
576
- stderr: stderr.trim(),
577
- timedOut
595
+ stdout: Buffer.concat(stdoutChunks).toString("utf8").trim(),
596
+ stderr: Buffer.concat(stderrChunks).toString("utf8").trim(),
597
+ timedOut,
598
+ truncated: truncated || void 0
578
599
  });
579
600
  });
580
601
  try {
@@ -589,7 +610,8 @@ function formatHookOutcomeMessage(outcome) {
589
610
  const detail = (outcome.stderr || outcome.stdout || "").trim();
590
611
  const tag = `${outcome.hook.scope}/${outcome.hook.event}`;
591
612
  const cmd = outcome.hook.command.length > 60 ? `${outcome.hook.command.slice(0, 60)}\u2026` : outcome.hook.command;
592
- const head = `hook ${tag} \`${cmd}\` ${outcome.decision}`;
613
+ const truncTag = outcome.truncated ? " (output truncated at 256KB)" : "";
614
+ const head = `hook ${tag} \`${cmd}\` ${outcome.decision}${truncTag}`;
593
615
  return detail ? `${head}: ${detail}` : head;
594
616
  }
595
617
  function decideOutcome(event, raw) {
@@ -620,7 +642,8 @@ async function runHooks(opts) {
620
642
  exitCode: raw.exitCode,
621
643
  stdout: raw.stdout,
622
644
  stderr: raw.stderr || (raw.spawnError ? raw.spawnError.message : "") || (raw.timedOut ? `hook timed out after ${timeoutMs}ms` : ""),
623
- durationMs: Date.now() - start
645
+ durationMs: Date.now() - start,
646
+ truncated: raw.truncated
624
647
  });
625
648
  if (decision === "block") {
626
649
  blocked = true;
@@ -1168,6 +1191,23 @@ var ImmutablePrefix = class {
1168
1191
  */
1169
1192
  _toolSpecs;
1170
1193
  fewShots;
1194
+ /**
1195
+ * Cached SHA-256 of the prefix payload. Computed lazily on first
1196
+ * `fingerprint` access, invalidated only by mutations that go
1197
+ * through `addTool` (the one legitimate post-construction mutation
1198
+ * path). The TUI reads `fingerprint` on every render — without the
1199
+ * cache, that means a fresh `JSON.stringify` + sha256 over the
1200
+ * full prefix (system prompt + tools list + few-shots, typically
1201
+ * 5-10KB) on every keystroke.
1202
+ *
1203
+ * The lazy-init also acts as a cheap drift guard: if some future
1204
+ * code path mutates `_toolSpecs` directly without going through
1205
+ * `addTool`, `fingerprint` will return the stale cached value
1206
+ * while the actual prefix sent to DeepSeek diverges — the cache
1207
+ * miss would be the first symptom. {@link verifyFingerprint}
1208
+ * lets dev / test code assert the cache matches reality.
1209
+ */
1210
+ _fingerprintCache = null;
1171
1211
  constructor(opts) {
1172
1212
  this.system = opts.system;
1173
1213
  this._toolSpecs = [...opts.toolSpecs ?? []];
@@ -1193,9 +1233,33 @@ var ImmutablePrefix = class {
1193
1233
  if (!name) return false;
1194
1234
  if (this._toolSpecs.some((t) => t.function?.name === name)) return false;
1195
1235
  this._toolSpecs.push(spec);
1236
+ this._fingerprintCache = null;
1196
1237
  return true;
1197
1238
  }
1198
1239
  get fingerprint() {
1240
+ if (this._fingerprintCache !== null) return this._fingerprintCache;
1241
+ this._fingerprintCache = this.computeFingerprint();
1242
+ return this._fingerprintCache;
1243
+ }
1244
+ /**
1245
+ * Recompute the fingerprint from scratch and assert it matches the
1246
+ * cached value. Returns the freshly-computed hash on success; throws
1247
+ * with a diff if the cache drifted, which always indicates a bug —
1248
+ * either a non-`addTool` mutation path was added, or `addTool`
1249
+ * forgot to invalidate the cache. Dev / test only; the live loop
1250
+ * doesn't call this on the hot path.
1251
+ */
1252
+ verifyFingerprint() {
1253
+ const fresh = this.computeFingerprint();
1254
+ if (this._fingerprintCache !== null && this._fingerprintCache !== fresh) {
1255
+ throw new Error(
1256
+ `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.`
1257
+ );
1258
+ }
1259
+ this._fingerprintCache = fresh;
1260
+ return fresh;
1261
+ }
1262
+ computeFingerprint() {
1199
1263
  const blob = JSON.stringify({
1200
1264
  system: this.system,
1201
1265
  tools: this._toolSpecs,
@@ -1614,10 +1678,10 @@ function listSessions() {
1614
1678
  const files = readdirSync(dir).filter((f) => f.endsWith(".jsonl"));
1615
1679
  return files.map((file) => {
1616
1680
  const path = join3(dir, file);
1617
- const stat = statSync(path);
1681
+ const stat2 = statSync(path);
1618
1682
  const name = file.replace(/\.jsonl$/, "");
1619
1683
  const messageCount = countLines(path);
1620
- return { name, path, size: stat.size, messageCount, mtime: stat.mtime };
1684
+ return { name, path, size: stat2.size, messageCount, mtime: stat2.mtime };
1621
1685
  }).sort((a, b) => b.mtime.getTime() - a.mtime.getTime());
1622
1686
  } catch {
1623
1687
  return [];
@@ -3118,6 +3182,7 @@ function extractDeepSeekErrorMessage(body) {
3118
3182
 
3119
3183
  // src/at-mentions.ts
3120
3184
  import { existsSync as existsSync4, readFileSync as readFileSync4, readdirSync as readdirSync2, statSync as statSync2 } from "fs";
3185
+ import { readdir, stat } from "fs/promises";
3121
3186
  import { isAbsolute, join as join4, relative, resolve } from "path";
3122
3187
  var DEFAULT_AT_MENTION_MAX_BYTES = 64 * 1024;
3123
3188
  var DEFAULT_PICKER_IGNORE_DIRS = [
@@ -3172,6 +3237,58 @@ function listFilesWithStatsSync(root, opts = {}) {
3172
3237
  walk2(rootAbs, "");
3173
3238
  return out;
3174
3239
  }
3240
+ async function listFilesWithStatsAsync(root, opts = {}) {
3241
+ const maxResults = Math.max(1, opts.maxResults ?? 500);
3242
+ const ignore = new Set(opts.ignoreDirs ?? DEFAULT_PICKER_IGNORE_DIRS);
3243
+ const rootAbs = resolve(root);
3244
+ const out = [];
3245
+ const walk2 = async (dirAbs, dirRel) => {
3246
+ if (out.length >= maxResults) return;
3247
+ let entries;
3248
+ try {
3249
+ entries = await readdir(dirAbs, { withFileTypes: true });
3250
+ } catch {
3251
+ return;
3252
+ }
3253
+ entries.sort((a, b) => a.name.localeCompare(b.name));
3254
+ const fileEnts = [];
3255
+ for (const ent of entries) {
3256
+ if (out.length >= maxResults) break;
3257
+ if (ent.isDirectory()) {
3258
+ if (ent.name.startsWith(".") || ignore.has(ent.name)) continue;
3259
+ if (fileEnts.length > 0) {
3260
+ await statBatch(fileEnts, dirAbs, dirRel, out, maxResults);
3261
+ fileEnts.length = 0;
3262
+ if (out.length >= maxResults) return;
3263
+ }
3264
+ await walk2(join4(dirAbs, ent.name), dirRel ? `${dirRel}/${ent.name}` : ent.name);
3265
+ } else if (ent.isFile()) {
3266
+ fileEnts.push(ent);
3267
+ }
3268
+ }
3269
+ if (fileEnts.length > 0 && out.length < maxResults) {
3270
+ await statBatch(fileEnts, dirAbs, dirRel, out, maxResults);
3271
+ }
3272
+ };
3273
+ await walk2(rootAbs, "");
3274
+ return out;
3275
+ }
3276
+ async function statBatch(ents, dirAbs, dirRel, out, maxResults) {
3277
+ const remaining = Math.max(0, maxResults - out.length);
3278
+ const batch = ents.slice(0, remaining);
3279
+ const stats = await Promise.all(
3280
+ batch.map(
3281
+ (e) => stat(join4(dirAbs, e.name)).then((s) => s.mtimeMs).catch(() => 0)
3282
+ )
3283
+ );
3284
+ for (let i = 0; i < batch.length; i++) {
3285
+ const ent = batch[i];
3286
+ out.push({
3287
+ path: dirRel ? `${dirRel}/${ent.name}` : ent.name,
3288
+ mtimeMs: stats[i] ?? 0
3289
+ });
3290
+ }
3291
+ }
3175
3292
  var AT_PICKER_PREFIX = /(?:^|\s)@([a-zA-Z0-9_./\\-]*)$/;
3176
3293
  function detectAtPicker(input) {
3177
3294
  const m = AT_PICKER_PREFIX.exec(input);
@@ -4180,8 +4297,8 @@ When none of these is given AND the file is longer than ${DEFAULT_AUTO_PREVIEW_L
4180
4297
  },
4181
4298
  fn: async (args) => {
4182
4299
  const abs = safePath(args.path);
4183
- const stat = await fs.stat(abs);
4184
- if (stat.isDirectory()) {
4300
+ const stat2 = await fs.stat(abs);
4301
+ if (stat2.isDirectory()) {
4185
4302
  throw new Error(`not a file: ${args.path} (it's a directory)`);
4186
4303
  }
4187
4304
  const raw = await fs.readFile(abs);
@@ -4450,13 +4567,13 @@ Prefer \`list_directory\` for a single-level view, \`search_files\` to find spec
4450
4567
  if (nameFilter && !e.name.toLowerCase().includes(nameFilter)) continue;
4451
4568
  if (isLikelyBinaryByName(e.name)) continue;
4452
4569
  const full = pathMod.join(dir, e.name);
4453
- let stat;
4570
+ let stat2;
4454
4571
  try {
4455
- stat = await fs.stat(full);
4572
+ stat2 = await fs.stat(full);
4456
4573
  } catch {
4457
4574
  continue;
4458
4575
  }
4459
- if (stat.size > 2 * 1024 * 1024) continue;
4576
+ if (stat2.size > 2 * 1024 * 1024) continue;
4460
4577
  let raw;
4461
4578
  try {
4462
4579
  raw = await fs.readFile(full);
@@ -5491,6 +5608,8 @@ var JobRegistry = class {
5491
5608
  };
5492
5609
  this.jobs.set(id, job);
5493
5610
  let readyMatched = false;
5611
+ let recentForReady = "";
5612
+ const READY_WINDOW = 1024;
5494
5613
  const onData = (chunk) => {
5495
5614
  const s = chunk.toString();
5496
5615
  job.totalBytesWritten += s.length;
@@ -5503,8 +5622,9 @@ var JobRegistry = class {
5503
5622
  ${job.output.slice(start)}`;
5504
5623
  }
5505
5624
  if (!readyMatched) {
5625
+ recentForReady = (recentForReady + s).slice(-READY_WINDOW);
5506
5626
  for (const re of READY_SIGNALS) {
5507
- if (re.test(s) || re.test(job.output)) {
5627
+ if (re.test(recentForReady)) {
5508
5628
  readyMatched = true;
5509
5629
  job.signalReady();
5510
5630
  break;
@@ -6188,6 +6308,7 @@ ${r.output}` : header;
6188
6308
  var DEFAULT_FETCH_MAX_CHARS = 32e3;
6189
6309
  var DEFAULT_FETCH_TIMEOUT_MS = 15e3;
6190
6310
  var DEFAULT_TOPK = 5;
6311
+ var FETCH_MAX_BYTES = 10 * 1024 * 1024;
6191
6312
  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";
6192
6313
  var MOJEEK_ENDPOINT = "https://www.mojeek.com/search";
6193
6314
  async function webSearch(query, opts = {}) {
@@ -6267,7 +6388,13 @@ async function webFetch(url, opts = {}) {
6267
6388
  }
6268
6389
  if (!resp.ok) throw new Error(`web_fetch ${resp.status} for ${url}`);
6269
6390
  const contentType = resp.headers.get("content-type") ?? "";
6270
- const raw = await resp.text();
6391
+ const declaredLen = Number(resp.headers.get("content-length") ?? "");
6392
+ if (Number.isFinite(declaredLen) && declaredLen > FETCH_MAX_BYTES) {
6393
+ throw new Error(
6394
+ `web_fetch refused: content-length ${declaredLen} bytes exceeds ${FETCH_MAX_BYTES}-byte cap (${url})`
6395
+ );
6396
+ }
6397
+ const raw = await readBodyCapped(resp, FETCH_MAX_BYTES);
6271
6398
  const title = extractTitle(raw);
6272
6399
  const text = contentType.includes("text/html") ? htmlToText(raw) : raw;
6273
6400
  const truncated = text.length > maxChars;
@@ -6276,6 +6403,37 @@ async function webFetch(url, opts = {}) {
6276
6403
  [\u2026 truncated ${text.length - maxChars} chars \u2026]` : text;
6277
6404
  return { url, title, text: finalText, truncated };
6278
6405
  }
6406
+ async function readBodyCapped(resp, maxBytes) {
6407
+ if (!resp.body) return await resp.text();
6408
+ const reader = resp.body.getReader();
6409
+ const decoder = new TextDecoder("utf-8");
6410
+ let total = 0;
6411
+ let out = "";
6412
+ try {
6413
+ while (true) {
6414
+ const { value, done } = await reader.read();
6415
+ if (done) break;
6416
+ total += value.byteLength;
6417
+ if (total > maxBytes) {
6418
+ try {
6419
+ await reader.cancel();
6420
+ } catch {
6421
+ }
6422
+ throw new Error(
6423
+ `web_fetch refused: response body exceeded ${maxBytes}-byte cap (${total} bytes seen)`
6424
+ );
6425
+ }
6426
+ out += decoder.decode(value, { stream: true });
6427
+ }
6428
+ out += decoder.decode();
6429
+ } finally {
6430
+ try {
6431
+ reader.releaseLock();
6432
+ } catch {
6433
+ }
6434
+ }
6435
+ return out;
6436
+ }
6279
6437
  function htmlToText(html) {
6280
6438
  let s = html;
6281
6439
  s = s.replace(/<script[\s\S]*?<\/script>/gi, "");
@@ -6889,6 +7047,107 @@ function truncate(s, n) {
6889
7047
  return s.length > n ? `${s.slice(0, n)}\u2026` : s;
6890
7048
  }
6891
7049
 
7050
+ // src/version.ts
7051
+ import { existsSync as existsSync9, mkdirSync as mkdirSync3, readFileSync as readFileSync10, writeFileSync as writeFileSync3 } from "fs";
7052
+ import { homedir as homedir5 } from "os";
7053
+ import { dirname as dirname4, join as join9 } from "path";
7054
+ import { fileURLToPath as fileURLToPath2 } from "url";
7055
+ var REGISTRY_URL = "https://registry.npmjs.org/reasonix/latest";
7056
+ var LATEST_CACHE_TTL_MS = 24 * 60 * 60 * 1e3;
7057
+ var LATEST_FETCH_TIMEOUT_MS = 2e3;
7058
+ function readPackageVersion() {
7059
+ try {
7060
+ let dir = dirname4(fileURLToPath2(import.meta.url));
7061
+ for (let i = 0; i < 6; i++) {
7062
+ const p = join9(dir, "package.json");
7063
+ if (existsSync9(p)) {
7064
+ const pkg = JSON.parse(readFileSync10(p, "utf8"));
7065
+ if (pkg?.name === "reasonix" && typeof pkg.version === "string") {
7066
+ return pkg.version;
7067
+ }
7068
+ }
7069
+ const parent = dirname4(dir);
7070
+ if (parent === dir) break;
7071
+ dir = parent;
7072
+ }
7073
+ } catch {
7074
+ }
7075
+ return "0.0.0-dev";
7076
+ }
7077
+ var VERSION = readPackageVersion();
7078
+ function cachePath(homeDirOverride) {
7079
+ return join9(homeDirOverride ?? homedir5(), ".reasonix", "version-cache.json");
7080
+ }
7081
+ function readCache(homeDirOverride) {
7082
+ try {
7083
+ const raw = readFileSync10(cachePath(homeDirOverride), "utf8");
7084
+ const parsed = JSON.parse(raw);
7085
+ if (parsed && typeof parsed.version === "string" && typeof parsed.checkedAt === "number") {
7086
+ return parsed;
7087
+ }
7088
+ } catch {
7089
+ }
7090
+ return null;
7091
+ }
7092
+ function writeCache(entry, homeDirOverride) {
7093
+ try {
7094
+ const p = cachePath(homeDirOverride);
7095
+ mkdirSync3(dirname4(p), { recursive: true });
7096
+ writeFileSync3(p, JSON.stringify(entry), "utf8");
7097
+ } catch {
7098
+ }
7099
+ }
7100
+ async function getLatestVersion(opts = {}) {
7101
+ const ttl = opts.ttlMs ?? LATEST_CACHE_TTL_MS;
7102
+ if (!opts.force) {
7103
+ const cached2 = readCache(opts.homeDir);
7104
+ if (cached2 && Date.now() - cached2.checkedAt < ttl) return cached2.version;
7105
+ }
7106
+ const fetchImpl = opts.fetchImpl ?? globalThis.fetch;
7107
+ if (!fetchImpl) return null;
7108
+ const url = opts.registryUrl ?? REGISTRY_URL;
7109
+ const timeout = opts.timeoutMs ?? LATEST_FETCH_TIMEOUT_MS;
7110
+ const controller = new AbortController();
7111
+ const timer = setTimeout(() => controller.abort(), timeout);
7112
+ try {
7113
+ const res = await fetchImpl(url, {
7114
+ signal: controller.signal,
7115
+ headers: { accept: "application/json" }
7116
+ });
7117
+ if (!res.ok) return null;
7118
+ const body = await res.json();
7119
+ if (typeof body.version !== "string") return null;
7120
+ writeCache({ version: body.version, checkedAt: Date.now() }, opts.homeDir);
7121
+ return body.version;
7122
+ } catch {
7123
+ return null;
7124
+ } finally {
7125
+ clearTimeout(timer);
7126
+ }
7127
+ }
7128
+ function compareVersions(a, b) {
7129
+ const [aCore = "0", aPre = ""] = a.split("-", 2);
7130
+ const [bCore = "0", bPre = ""] = b.split("-", 2);
7131
+ const aParts = aCore.split(".").map((p) => Number.parseInt(p, 10) || 0);
7132
+ const bParts = bCore.split(".").map((p) => Number.parseInt(p, 10) || 0);
7133
+ for (let i = 0; i < 3; i++) {
7134
+ const diff = (aParts[i] ?? 0) - (bParts[i] ?? 0);
7135
+ if (diff !== 0) return diff;
7136
+ }
7137
+ if (!aPre && !bPre) return 0;
7138
+ if (!aPre) return 1;
7139
+ if (!bPre) return -1;
7140
+ return aPre < bPre ? -1 : aPre > bPre ? 1 : 0;
7141
+ }
7142
+ function isNpxInstall() {
7143
+ const bin = process.argv[1] ?? "";
7144
+ if (/[/\\]_npx[/\\]/.test(bin)) return true;
7145
+ if (/[/\\]\.pnpm[/\\]/.test(bin) && /dlx/i.test(bin)) return true;
7146
+ const ua = process.env.npm_config_user_agent ?? "";
7147
+ if (ua.includes("npx/")) return true;
7148
+ return false;
7149
+ }
7150
+
6892
7151
  // src/mcp/types.ts
6893
7152
  var MCP_PROTOCOL_VERSION = "2024-11-05";
6894
7153
  function isJsonRpcError(msg) {
@@ -6917,7 +7176,7 @@ var McpClient = class {
6917
7176
  nextProgressToken = 1;
6918
7177
  constructor(opts) {
6919
7178
  this.transport = opts.transport;
6920
- this.clientInfo = opts.clientInfo ?? { name: "reasonix", version: "0.3.0-dev" };
7179
+ this.clientInfo = opts.clientInfo ?? { name: "reasonix", version: VERSION };
6921
7180
  this.requestTimeoutMs = opts.requestTimeoutMs ?? 6e4;
6922
7181
  }
6923
7182
  /** Server's advertised capabilities, available after initialize(). */
@@ -7657,8 +7916,8 @@ async function trySection(load) {
7657
7916
  }
7658
7917
 
7659
7918
  // src/code/edit-blocks.ts
7660
- import { existsSync as existsSync9, mkdirSync as mkdirSync3, readFileSync as readFileSync10, unlinkSync as unlinkSync3, writeFileSync as writeFileSync3 } from "fs";
7661
- import { dirname as dirname4, resolve as resolve8 } from "path";
7919
+ import { existsSync as existsSync10, mkdirSync as mkdirSync4, readFileSync as readFileSync11, unlinkSync as unlinkSync3, writeFileSync as writeFileSync4 } from "fs";
7920
+ import { dirname as dirname5, resolve as resolve8 } from "path";
7662
7921
  var BLOCK_RE = /^(\S[^\n]*)\n<{7} SEARCH\n([\s\S]*?)\n?={7}\n([\s\S]*?)\n?>{7} REPLACE/gm;
7663
7922
  function parseEditBlocks(text) {
7664
7923
  const out = [];
@@ -7686,7 +7945,7 @@ function applyEditBlock(block, rootDir) {
7686
7945
  };
7687
7946
  }
7688
7947
  const searchEmpty = block.search.length === 0;
7689
- const exists = existsSync9(absTarget);
7948
+ const exists = existsSync10(absTarget);
7690
7949
  try {
7691
7950
  if (!exists) {
7692
7951
  if (!searchEmpty) {
@@ -7696,11 +7955,11 @@ function applyEditBlock(block, rootDir) {
7696
7955
  message: "file does not exist; to create it, use an empty SEARCH block"
7697
7956
  };
7698
7957
  }
7699
- mkdirSync3(dirname4(absTarget), { recursive: true });
7700
- writeFileSync3(absTarget, block.replace, "utf8");
7958
+ mkdirSync4(dirname5(absTarget), { recursive: true });
7959
+ writeFileSync4(absTarget, block.replace, "utf8");
7701
7960
  return { path: block.path, status: "created" };
7702
7961
  }
7703
- const content = readFileSync10(absTarget, "utf8");
7962
+ const content = readFileSync11(absTarget, "utf8");
7704
7963
  if (searchEmpty) {
7705
7964
  return {
7706
7965
  path: block.path,
@@ -7717,7 +7976,7 @@ function applyEditBlock(block, rootDir) {
7717
7976
  };
7718
7977
  }
7719
7978
  const replaced = `${content.slice(0, idx)}${block.replace}${content.slice(idx + block.search.length)}`;
7720
- writeFileSync3(absTarget, replaced, "utf8");
7979
+ writeFileSync4(absTarget, replaced, "utf8");
7721
7980
  return { path: block.path, status: "applied" };
7722
7981
  } catch (err) {
7723
7982
  return { path: block.path, status: "error", message: err.message };
@@ -7734,12 +7993,12 @@ function snapshotBeforeEdits(blocks, rootDir) {
7734
7993
  if (seen.has(b.path)) continue;
7735
7994
  seen.add(b.path);
7736
7995
  const abs = resolve8(absRoot, b.path);
7737
- if (!existsSync9(abs)) {
7996
+ if (!existsSync10(abs)) {
7738
7997
  snapshots.push({ path: b.path, prevContent: null });
7739
7998
  continue;
7740
7999
  }
7741
8000
  try {
7742
- snapshots.push({ path: b.path, prevContent: readFileSync10(abs, "utf8") });
8001
+ snapshots.push({ path: b.path, prevContent: readFileSync11(abs, "utf8") });
7743
8002
  } catch {
7744
8003
  snapshots.push({ path: b.path, prevContent: null });
7745
8004
  }
@@ -7759,14 +8018,14 @@ function restoreSnapshots(snapshots, rootDir) {
7759
8018
  }
7760
8019
  try {
7761
8020
  if (snap.prevContent === null) {
7762
- if (existsSync9(abs)) unlinkSync3(abs);
8021
+ if (existsSync10(abs)) unlinkSync3(abs);
7763
8022
  return {
7764
8023
  path: snap.path,
7765
8024
  status: "applied",
7766
8025
  message: "removed (the edit had created it)"
7767
8026
  };
7768
8027
  }
7769
- writeFileSync3(abs, snap.prevContent, "utf8");
8028
+ writeFileSync4(abs, snap.prevContent, "utf8");
7770
8029
  return {
7771
8030
  path: snap.path,
7772
8031
  status: "applied",
@@ -7782,8 +8041,8 @@ function sep() {
7782
8041
  }
7783
8042
 
7784
8043
  // src/code/prompt.ts
7785
- import { existsSync as existsSync10, readFileSync as readFileSync11 } from "fs";
7786
- import { join as join9 } from "path";
8044
+ import { existsSync as existsSync11, readFileSync as readFileSync12 } from "fs";
8045
+ import { join as join10 } from "path";
7787
8046
  var CODE_SYSTEM_PROMPT = `You are Reasonix Code, a coding assistant. You have filesystem tools (read_file, write_file, edit_file, list_directory, directory_tree, search_files, search_content, get_file_info) rooted at the user's working directory, plus run_command / run_background for shell.
7788
8047
 
7789
8048
  # Cite or shut up \u2014 non-negotiable
@@ -7989,11 +8248,11 @@ If \`semantic_search\` returns nothing useful (low scores, off-topic), THEN fall
7989
8248
  function codeSystemPrompt(rootDir, opts = {}) {
7990
8249
  const base = opts.hasSemanticSearch ? `${CODE_SYSTEM_PROMPT}${SEMANTIC_SEARCH_ROUTING}` : CODE_SYSTEM_PROMPT;
7991
8250
  const withMemory = applyMemoryStack(base, rootDir);
7992
- const gitignorePath = join9(rootDir, ".gitignore");
7993
- if (!existsSync10(gitignorePath)) return withMemory;
8251
+ const gitignorePath = join10(rootDir, ".gitignore");
8252
+ if (!existsSync11(gitignorePath)) return withMemory;
7994
8253
  let content;
7995
8254
  try {
7996
- content = readFileSync11(gitignorePath, "utf8");
8255
+ content = readFileSync12(gitignorePath, "utf8");
7997
8256
  } catch {
7998
8257
  return withMemory;
7999
8258
  }
@@ -8013,15 +8272,15 @@ ${truncated}
8013
8272
  }
8014
8273
 
8015
8274
  // src/config.ts
8016
- import { chmodSync as chmodSync2, mkdirSync as mkdirSync4, readFileSync as readFileSync12, writeFileSync as writeFileSync4 } from "fs";
8017
- import { homedir as homedir5 } from "os";
8018
- import { dirname as dirname5, join as join10 } from "path";
8275
+ import { chmodSync as chmodSync2, mkdirSync as mkdirSync5, readFileSync as readFileSync13, writeFileSync as writeFileSync5 } from "fs";
8276
+ import { homedir as homedir6 } from "os";
8277
+ import { dirname as dirname6, join as join11 } from "path";
8019
8278
  function defaultConfigPath() {
8020
- return join10(homedir5(), ".reasonix", "config.json");
8279
+ return join11(homedir6(), ".reasonix", "config.json");
8021
8280
  }
8022
8281
  function readConfig(path = defaultConfigPath()) {
8023
8282
  try {
8024
- const raw = readFileSync12(path, "utf8");
8283
+ const raw = readFileSync13(path, "utf8");
8025
8284
  const parsed = JSON.parse(raw);
8026
8285
  if (parsed && typeof parsed === "object") return parsed;
8027
8286
  } catch {
@@ -8029,8 +8288,8 @@ function readConfig(path = defaultConfigPath()) {
8029
8288
  return {};
8030
8289
  }
8031
8290
  function writeConfig(cfg, path = defaultConfigPath()) {
8032
- mkdirSync4(dirname5(path), { recursive: true });
8033
- writeFileSync4(path, JSON.stringify(cfg, null, 2), "utf8");
8291
+ mkdirSync5(dirname6(path), { recursive: true });
8292
+ writeFileSync5(path, JSON.stringify(cfg, null, 2), "utf8");
8034
8293
  try {
8035
8294
  chmodSync2(path, 384);
8036
8295
  } catch {
@@ -8055,113 +8314,53 @@ function redactKey(key) {
8055
8314
  return `${key.slice(0, 6)}\u2026${key.slice(-4)}`;
8056
8315
  }
8057
8316
 
8058
- // src/version.ts
8059
- import { existsSync as existsSync11, mkdirSync as mkdirSync5, readFileSync as readFileSync13, writeFileSync as writeFileSync5 } from "fs";
8060
- import { homedir as homedir6 } from "os";
8061
- import { dirname as dirname6, join as join11 } from "path";
8062
- import { fileURLToPath as fileURLToPath2 } from "url";
8063
- var REGISTRY_URL = "https://registry.npmjs.org/reasonix/latest";
8064
- var LATEST_CACHE_TTL_MS = 24 * 60 * 60 * 1e3;
8065
- var LATEST_FETCH_TIMEOUT_MS = 2e3;
8066
- function readPackageVersion() {
8067
- try {
8068
- let dir = dirname6(fileURLToPath2(import.meta.url));
8069
- for (let i = 0; i < 6; i++) {
8070
- const p = join11(dir, "package.json");
8071
- if (existsSync11(p)) {
8072
- const pkg = JSON.parse(readFileSync13(p, "utf8"));
8073
- if (pkg?.name === "reasonix" && typeof pkg.version === "string") {
8074
- return pkg.version;
8075
- }
8076
- }
8077
- const parent = dirname6(dir);
8078
- if (parent === dir) break;
8079
- dir = parent;
8080
- }
8081
- } catch {
8082
- }
8083
- return "0.0.0-dev";
8084
- }
8085
- var VERSION = readPackageVersion();
8086
- function cachePath(homeDirOverride) {
8087
- return join11(homeDirOverride ?? homedir6(), ".reasonix", "version-cache.json");
8317
+ // src/usage.ts
8318
+ import {
8319
+ appendFileSync as appendFileSync2,
8320
+ existsSync as existsSync12,
8321
+ mkdirSync as mkdirSync6,
8322
+ readFileSync as readFileSync14,
8323
+ statSync as statSync5,
8324
+ writeFileSync as writeFileSync6
8325
+ } from "fs";
8326
+ import { homedir as homedir7 } from "os";
8327
+ import { dirname as dirname7, join as join12 } from "path";
8328
+ function defaultUsageLogPath(homeDirOverride) {
8329
+ return join12(homeDirOverride ?? homedir7(), ".reasonix", "usage.jsonl");
8088
8330
  }
8089
- function readCache(homeDirOverride) {
8331
+ var USAGE_COMPACTION_THRESHOLD_BYTES = 5 * 1024 * 1024;
8332
+ var USAGE_RETENTION_DAYS = 365;
8333
+ function compactUsageLogIfLarge(path, now) {
8334
+ let size;
8090
8335
  try {
8091
- const raw = readFileSync13(cachePath(homeDirOverride), "utf8");
8092
- const parsed = JSON.parse(raw);
8093
- if (parsed && typeof parsed.version === "string" && typeof parsed.checkedAt === "number") {
8094
- return parsed;
8095
- }
8336
+ size = statSync5(path).size;
8096
8337
  } catch {
8338
+ return;
8097
8339
  }
8098
- return null;
8099
- }
8100
- function writeCache(entry, homeDirOverride) {
8340
+ if (size < USAGE_COMPACTION_THRESHOLD_BYTES) return;
8341
+ const cutoff = now - USAGE_RETENTION_DAYS * 24 * 60 * 60 * 1e3;
8342
+ let raw;
8101
8343
  try {
8102
- const p = cachePath(homeDirOverride);
8103
- mkdirSync5(dirname6(p), { recursive: true });
8104
- writeFileSync5(p, JSON.stringify(entry), "utf8");
8344
+ raw = readFileSync14(path, "utf8");
8105
8345
  } catch {
8346
+ return;
8106
8347
  }
8107
- }
8108
- async function getLatestVersion(opts = {}) {
8109
- const ttl = opts.ttlMs ?? LATEST_CACHE_TTL_MS;
8110
- if (!opts.force) {
8111
- const cached2 = readCache(opts.homeDir);
8112
- if (cached2 && Date.now() - cached2.checkedAt < ttl) return cached2.version;
8348
+ const lines = raw.split(/\r?\n/);
8349
+ const kept = [];
8350
+ for (const line of lines) {
8351
+ if (!line.trim()) continue;
8352
+ try {
8353
+ const rec = JSON.parse(line);
8354
+ if (isValidRecord(rec) && rec.ts >= cutoff) kept.push(line);
8355
+ } catch {
8356
+ }
8113
8357
  }
8114
- const fetchImpl = opts.fetchImpl ?? globalThis.fetch;
8115
- if (!fetchImpl) return null;
8116
- const url = opts.registryUrl ?? REGISTRY_URL;
8117
- const timeout = opts.timeoutMs ?? LATEST_FETCH_TIMEOUT_MS;
8118
- const controller = new AbortController();
8119
- const timer = setTimeout(() => controller.abort(), timeout);
8358
+ if (kept.length === lines.filter((l) => l.trim()).length) return;
8120
8359
  try {
8121
- const res = await fetchImpl(url, {
8122
- signal: controller.signal,
8123
- headers: { accept: "application/json" }
8124
- });
8125
- if (!res.ok) return null;
8126
- const body = await res.json();
8127
- if (typeof body.version !== "string") return null;
8128
- writeCache({ version: body.version, checkedAt: Date.now() }, opts.homeDir);
8129
- return body.version;
8360
+ writeFileSync6(path, kept.length > 0 ? `${kept.join("\n")}
8361
+ ` : "", "utf8");
8130
8362
  } catch {
8131
- return null;
8132
- } finally {
8133
- clearTimeout(timer);
8134
- }
8135
- }
8136
- function compareVersions(a, b) {
8137
- const [aCore = "0", aPre = ""] = a.split("-", 2);
8138
- const [bCore = "0", bPre = ""] = b.split("-", 2);
8139
- const aParts = aCore.split(".").map((p) => Number.parseInt(p, 10) || 0);
8140
- const bParts = bCore.split(".").map((p) => Number.parseInt(p, 10) || 0);
8141
- for (let i = 0; i < 3; i++) {
8142
- const diff = (aParts[i] ?? 0) - (bParts[i] ?? 0);
8143
- if (diff !== 0) return diff;
8144
8363
  }
8145
- if (!aPre && !bPre) return 0;
8146
- if (!aPre) return 1;
8147
- if (!bPre) return -1;
8148
- return aPre < bPre ? -1 : aPre > bPre ? 1 : 0;
8149
- }
8150
- function isNpxInstall() {
8151
- const bin = process.argv[1] ?? "";
8152
- if (/[/\\]_npx[/\\]/.test(bin)) return true;
8153
- if (/[/\\]\.pnpm[/\\]/.test(bin) && /dlx/i.test(bin)) return true;
8154
- const ua = process.env.npm_config_user_agent ?? "";
8155
- if (ua.includes("npx/")) return true;
8156
- return false;
8157
- }
8158
-
8159
- // src/usage.ts
8160
- import { appendFileSync as appendFileSync2, existsSync as existsSync12, mkdirSync as mkdirSync6, readFileSync as readFileSync14, statSync as statSync5 } from "fs";
8161
- import { homedir as homedir7 } from "os";
8162
- import { dirname as dirname7, join as join12 } from "path";
8163
- function defaultUsageLogPath(homeDirOverride) {
8164
- return join12(homeDirOverride ?? homedir7(), ".reasonix", "usage.jsonl");
8165
8364
  }
8166
8365
  function appendUsage(input) {
8167
8366
  const record = {
@@ -8182,6 +8381,7 @@ function appendUsage(input) {
8182
8381
  mkdirSync6(dirname7(path), { recursive: true });
8183
8382
  appendFileSync2(path, `${JSON.stringify(record)}
8184
8383
  `, "utf8");
8384
+ compactUsageLogIfLarge(path, record.ts);
8185
8385
  } catch {
8186
8386
  }
8187
8387
  return record;
@@ -8401,6 +8601,7 @@ export {
8401
8601
  isPlanStateEmpty,
8402
8602
  isPlausibleKey,
8403
8603
  listFilesSync,
8604
+ listFilesWithStatsAsync,
8404
8605
  listFilesWithStatsSync,
8405
8606
  listSessions,
8406
8607
  loadApiKey,