reasonix 0.12.16 → 0.12.20

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 [];
@@ -1791,6 +1855,19 @@ var CacheFirstLoop = class {
1791
1855
  * flip it live alongside `model`.
1792
1856
  */
1793
1857
  autoEscalate = true;
1858
+ /**
1859
+ * Soft USD budget — see {@link CacheFirstLoopOptions.budgetUsd}.
1860
+ * Mutable so `/budget` slash can set / change / clear it mid-session.
1861
+ * `null` (the default) disables all budget checks.
1862
+ */
1863
+ budgetUsd;
1864
+ /**
1865
+ * Set the first time a turn crosses 80% of the budget so the warning
1866
+ * doesn't repeat every turn afterwards. Cleared by `setBudget` (any
1867
+ * change re-arms the warning, including raising the cap above the
1868
+ * current spend).
1869
+ */
1870
+ _budgetWarned = false;
1794
1871
  sessionName;
1795
1872
  /**
1796
1873
  * Hook list, mutable so `/hooks reload` can swap it without
@@ -1856,6 +1933,7 @@ var CacheFirstLoop = class {
1856
1933
  this.model = opts.model ?? "deepseek-v4-flash";
1857
1934
  this.reasoningEffort = opts.reasoningEffort ?? "max";
1858
1935
  if (opts.autoEscalate !== void 0) this.autoEscalate = opts.autoEscalate;
1936
+ this.budgetUsd = typeof opts.budgetUsd === "number" && opts.budgetUsd > 0 ? opts.budgetUsd : null;
1859
1937
  this.maxToolIters = opts.maxToolIters ?? 64;
1860
1938
  this.hooks = opts.hooks ?? [];
1861
1939
  this.hookCwd = opts.hookCwd ?? process.cwd();
@@ -2095,6 +2173,16 @@ var CacheFirstLoop = class {
2095
2173
  }
2096
2174
  this.stream = this.branchEnabled ? false : this._streamPreference;
2097
2175
  }
2176
+ /**
2177
+ * Set / change / clear the soft USD budget. `null` (or any non-
2178
+ * positive number) disables the cap entirely. Re-arms the 80%
2179
+ * warning so a user who bumps the cap mid-session sees a fresh
2180
+ * threshold message at the new boundary.
2181
+ */
2182
+ setBudget(usd) {
2183
+ this.budgetUsd = typeof usd === "number" && usd > 0 ? usd : null;
2184
+ this._budgetWarned = false;
2185
+ }
2098
2186
  /**
2099
2187
  * Arm pro for the next turn (consumed at turn start). Called by
2100
2188
  * `/pro`. Idempotent — repeated calls stay armed, `disarmPro()`
@@ -2256,6 +2344,26 @@ var CacheFirstLoop = class {
2256
2344
  return userText;
2257
2345
  }
2258
2346
  async *step(userInput) {
2347
+ if (this.budgetUsd !== null) {
2348
+ const spent = this.stats.totalCost;
2349
+ if (spent >= this.budgetUsd) {
2350
+ yield {
2351
+ turn: this._turn,
2352
+ role: "error",
2353
+ content: "",
2354
+ error: `session budget exhausted \u2014 spent $${spent.toFixed(4)} \u2265 cap $${this.budgetUsd.toFixed(2)}. Bump the cap with /budget <usd>, clear it with /budget off, or end the session.`
2355
+ };
2356
+ return;
2357
+ }
2358
+ if (!this._budgetWarned && spent >= this.budgetUsd * 0.8) {
2359
+ this._budgetWarned = true;
2360
+ yield {
2361
+ turn: this._turn,
2362
+ role: "warning",
2363
+ content: `\u25B2 budget 80% used \u2014 $${spent.toFixed(4)} of $${this.budgetUsd.toFixed(2)}. Next turn or two likely trips the cap.`
2364
+ };
2365
+ }
2366
+ }
2259
2367
  this._turn++;
2260
2368
  this.scratch.reset();
2261
2369
  this.repair.resetStorm();
@@ -3118,6 +3226,7 @@ function extractDeepSeekErrorMessage(body) {
3118
3226
 
3119
3227
  // src/at-mentions.ts
3120
3228
  import { existsSync as existsSync4, readFileSync as readFileSync4, readdirSync as readdirSync2, statSync as statSync2 } from "fs";
3229
+ import { readdir, stat } from "fs/promises";
3121
3230
  import { isAbsolute, join as join4, relative, resolve } from "path";
3122
3231
  var DEFAULT_AT_MENTION_MAX_BYTES = 64 * 1024;
3123
3232
  var DEFAULT_PICKER_IGNORE_DIRS = [
@@ -3172,6 +3281,58 @@ function listFilesWithStatsSync(root, opts = {}) {
3172
3281
  walk2(rootAbs, "");
3173
3282
  return out;
3174
3283
  }
3284
+ async function listFilesWithStatsAsync(root, opts = {}) {
3285
+ const maxResults = Math.max(1, opts.maxResults ?? 500);
3286
+ const ignore = new Set(opts.ignoreDirs ?? DEFAULT_PICKER_IGNORE_DIRS);
3287
+ const rootAbs = resolve(root);
3288
+ const out = [];
3289
+ const walk2 = async (dirAbs, dirRel) => {
3290
+ if (out.length >= maxResults) return;
3291
+ let entries;
3292
+ try {
3293
+ entries = await readdir(dirAbs, { withFileTypes: true });
3294
+ } catch {
3295
+ return;
3296
+ }
3297
+ entries.sort((a, b) => a.name.localeCompare(b.name));
3298
+ const fileEnts = [];
3299
+ for (const ent of entries) {
3300
+ if (out.length >= maxResults) break;
3301
+ if (ent.isDirectory()) {
3302
+ if (ent.name.startsWith(".") || ignore.has(ent.name)) continue;
3303
+ if (fileEnts.length > 0) {
3304
+ await statBatch(fileEnts, dirAbs, dirRel, out, maxResults);
3305
+ fileEnts.length = 0;
3306
+ if (out.length >= maxResults) return;
3307
+ }
3308
+ await walk2(join4(dirAbs, ent.name), dirRel ? `${dirRel}/${ent.name}` : ent.name);
3309
+ } else if (ent.isFile()) {
3310
+ fileEnts.push(ent);
3311
+ }
3312
+ }
3313
+ if (fileEnts.length > 0 && out.length < maxResults) {
3314
+ await statBatch(fileEnts, dirAbs, dirRel, out, maxResults);
3315
+ }
3316
+ };
3317
+ await walk2(rootAbs, "");
3318
+ return out;
3319
+ }
3320
+ async function statBatch(ents, dirAbs, dirRel, out, maxResults) {
3321
+ const remaining = Math.max(0, maxResults - out.length);
3322
+ const batch = ents.slice(0, remaining);
3323
+ const stats = await Promise.all(
3324
+ batch.map(
3325
+ (e) => stat(join4(dirAbs, e.name)).then((s) => s.mtimeMs).catch(() => 0)
3326
+ )
3327
+ );
3328
+ for (let i = 0; i < batch.length; i++) {
3329
+ const ent = batch[i];
3330
+ out.push({
3331
+ path: dirRel ? `${dirRel}/${ent.name}` : ent.name,
3332
+ mtimeMs: stats[i] ?? 0
3333
+ });
3334
+ }
3335
+ }
3175
3336
  var AT_PICKER_PREFIX = /(?:^|\s)@([a-zA-Z0-9_./\\-]*)$/;
3176
3337
  function detectAtPicker(input) {
3177
3338
  const m = AT_PICKER_PREFIX.exec(input);
@@ -4180,8 +4341,8 @@ When none of these is given AND the file is longer than ${DEFAULT_AUTO_PREVIEW_L
4180
4341
  },
4181
4342
  fn: async (args) => {
4182
4343
  const abs = safePath(args.path);
4183
- const stat = await fs.stat(abs);
4184
- if (stat.isDirectory()) {
4344
+ const stat2 = await fs.stat(abs);
4345
+ if (stat2.isDirectory()) {
4185
4346
  throw new Error(`not a file: ${args.path} (it's a directory)`);
4186
4347
  }
4187
4348
  const raw = await fs.readFile(abs);
@@ -4450,13 +4611,13 @@ Prefer \`list_directory\` for a single-level view, \`search_files\` to find spec
4450
4611
  if (nameFilter && !e.name.toLowerCase().includes(nameFilter)) continue;
4451
4612
  if (isLikelyBinaryByName(e.name)) continue;
4452
4613
  const full = pathMod.join(dir, e.name);
4453
- let stat;
4614
+ let stat2;
4454
4615
  try {
4455
- stat = await fs.stat(full);
4616
+ stat2 = await fs.stat(full);
4456
4617
  } catch {
4457
4618
  continue;
4458
4619
  }
4459
- if (stat.size > 2 * 1024 * 1024) continue;
4620
+ if (stat2.size > 2 * 1024 * 1024) continue;
4460
4621
  let raw;
4461
4622
  try {
4462
4623
  raw = await fs.readFile(full);
@@ -5491,6 +5652,8 @@ var JobRegistry = class {
5491
5652
  };
5492
5653
  this.jobs.set(id, job);
5493
5654
  let readyMatched = false;
5655
+ let recentForReady = "";
5656
+ const READY_WINDOW = 1024;
5494
5657
  const onData = (chunk) => {
5495
5658
  const s = chunk.toString();
5496
5659
  job.totalBytesWritten += s.length;
@@ -5503,8 +5666,9 @@ var JobRegistry = class {
5503
5666
  ${job.output.slice(start)}`;
5504
5667
  }
5505
5668
  if (!readyMatched) {
5669
+ recentForReady = (recentForReady + s).slice(-READY_WINDOW);
5506
5670
  for (const re of READY_SIGNALS) {
5507
- if (re.test(s) || re.test(job.output)) {
5671
+ if (re.test(recentForReady)) {
5508
5672
  readyMatched = true;
5509
5673
  job.signalReady();
5510
5674
  break;
@@ -6188,6 +6352,7 @@ ${r.output}` : header;
6188
6352
  var DEFAULT_FETCH_MAX_CHARS = 32e3;
6189
6353
  var DEFAULT_FETCH_TIMEOUT_MS = 15e3;
6190
6354
  var DEFAULT_TOPK = 5;
6355
+ var FETCH_MAX_BYTES = 10 * 1024 * 1024;
6191
6356
  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
6357
  var MOJEEK_ENDPOINT = "https://www.mojeek.com/search";
6193
6358
  async function webSearch(query, opts = {}) {
@@ -6267,7 +6432,13 @@ async function webFetch(url, opts = {}) {
6267
6432
  }
6268
6433
  if (!resp.ok) throw new Error(`web_fetch ${resp.status} for ${url}`);
6269
6434
  const contentType = resp.headers.get("content-type") ?? "";
6270
- const raw = await resp.text();
6435
+ const declaredLen = Number(resp.headers.get("content-length") ?? "");
6436
+ if (Number.isFinite(declaredLen) && declaredLen > FETCH_MAX_BYTES) {
6437
+ throw new Error(
6438
+ `web_fetch refused: content-length ${declaredLen} bytes exceeds ${FETCH_MAX_BYTES}-byte cap (${url})`
6439
+ );
6440
+ }
6441
+ const raw = await readBodyCapped(resp, FETCH_MAX_BYTES);
6271
6442
  const title = extractTitle(raw);
6272
6443
  const text = contentType.includes("text/html") ? htmlToText(raw) : raw;
6273
6444
  const truncated = text.length > maxChars;
@@ -6276,6 +6447,37 @@ async function webFetch(url, opts = {}) {
6276
6447
  [\u2026 truncated ${text.length - maxChars} chars \u2026]` : text;
6277
6448
  return { url, title, text: finalText, truncated };
6278
6449
  }
6450
+ async function readBodyCapped(resp, maxBytes) {
6451
+ if (!resp.body) return await resp.text();
6452
+ const reader = resp.body.getReader();
6453
+ const decoder = new TextDecoder("utf-8");
6454
+ let total = 0;
6455
+ let out = "";
6456
+ try {
6457
+ while (true) {
6458
+ const { value, done } = await reader.read();
6459
+ if (done) break;
6460
+ total += value.byteLength;
6461
+ if (total > maxBytes) {
6462
+ try {
6463
+ await reader.cancel();
6464
+ } catch {
6465
+ }
6466
+ throw new Error(
6467
+ `web_fetch refused: response body exceeded ${maxBytes}-byte cap (${total} bytes seen)`
6468
+ );
6469
+ }
6470
+ out += decoder.decode(value, { stream: true });
6471
+ }
6472
+ out += decoder.decode();
6473
+ } finally {
6474
+ try {
6475
+ reader.releaseLock();
6476
+ } catch {
6477
+ }
6478
+ }
6479
+ return out;
6480
+ }
6279
6481
  function htmlToText(html) {
6280
6482
  let s = html;
6281
6483
  s = s.replace(/<script[\s\S]*?<\/script>/gi, "");
@@ -6889,6 +7091,107 @@ function truncate(s, n) {
6889
7091
  return s.length > n ? `${s.slice(0, n)}\u2026` : s;
6890
7092
  }
6891
7093
 
7094
+ // src/version.ts
7095
+ import { existsSync as existsSync9, mkdirSync as mkdirSync3, readFileSync as readFileSync10, writeFileSync as writeFileSync3 } from "fs";
7096
+ import { homedir as homedir5 } from "os";
7097
+ import { dirname as dirname4, join as join9 } from "path";
7098
+ import { fileURLToPath as fileURLToPath2 } from "url";
7099
+ var REGISTRY_URL = "https://registry.npmjs.org/reasonix/latest";
7100
+ var LATEST_CACHE_TTL_MS = 24 * 60 * 60 * 1e3;
7101
+ var LATEST_FETCH_TIMEOUT_MS = 2e3;
7102
+ function readPackageVersion() {
7103
+ try {
7104
+ let dir = dirname4(fileURLToPath2(import.meta.url));
7105
+ for (let i = 0; i < 6; i++) {
7106
+ const p = join9(dir, "package.json");
7107
+ if (existsSync9(p)) {
7108
+ const pkg = JSON.parse(readFileSync10(p, "utf8"));
7109
+ if (pkg?.name === "reasonix" && typeof pkg.version === "string") {
7110
+ return pkg.version;
7111
+ }
7112
+ }
7113
+ const parent = dirname4(dir);
7114
+ if (parent === dir) break;
7115
+ dir = parent;
7116
+ }
7117
+ } catch {
7118
+ }
7119
+ return "0.0.0-dev";
7120
+ }
7121
+ var VERSION = readPackageVersion();
7122
+ function cachePath(homeDirOverride) {
7123
+ return join9(homeDirOverride ?? homedir5(), ".reasonix", "version-cache.json");
7124
+ }
7125
+ function readCache(homeDirOverride) {
7126
+ try {
7127
+ const raw = readFileSync10(cachePath(homeDirOverride), "utf8");
7128
+ const parsed = JSON.parse(raw);
7129
+ if (parsed && typeof parsed.version === "string" && typeof parsed.checkedAt === "number") {
7130
+ return parsed;
7131
+ }
7132
+ } catch {
7133
+ }
7134
+ return null;
7135
+ }
7136
+ function writeCache(entry, homeDirOverride) {
7137
+ try {
7138
+ const p = cachePath(homeDirOverride);
7139
+ mkdirSync3(dirname4(p), { recursive: true });
7140
+ writeFileSync3(p, JSON.stringify(entry), "utf8");
7141
+ } catch {
7142
+ }
7143
+ }
7144
+ async function getLatestVersion(opts = {}) {
7145
+ const ttl = opts.ttlMs ?? LATEST_CACHE_TTL_MS;
7146
+ if (!opts.force) {
7147
+ const cached2 = readCache(opts.homeDir);
7148
+ if (cached2 && Date.now() - cached2.checkedAt < ttl) return cached2.version;
7149
+ }
7150
+ const fetchImpl = opts.fetchImpl ?? globalThis.fetch;
7151
+ if (!fetchImpl) return null;
7152
+ const url = opts.registryUrl ?? REGISTRY_URL;
7153
+ const timeout = opts.timeoutMs ?? LATEST_FETCH_TIMEOUT_MS;
7154
+ const controller = new AbortController();
7155
+ const timer = setTimeout(() => controller.abort(), timeout);
7156
+ try {
7157
+ const res = await fetchImpl(url, {
7158
+ signal: controller.signal,
7159
+ headers: { accept: "application/json" }
7160
+ });
7161
+ if (!res.ok) return null;
7162
+ const body = await res.json();
7163
+ if (typeof body.version !== "string") return null;
7164
+ writeCache({ version: body.version, checkedAt: Date.now() }, opts.homeDir);
7165
+ return body.version;
7166
+ } catch {
7167
+ return null;
7168
+ } finally {
7169
+ clearTimeout(timer);
7170
+ }
7171
+ }
7172
+ function compareVersions(a, b) {
7173
+ const [aCore = "0", aPre = ""] = a.split("-", 2);
7174
+ const [bCore = "0", bPre = ""] = b.split("-", 2);
7175
+ const aParts = aCore.split(".").map((p) => Number.parseInt(p, 10) || 0);
7176
+ const bParts = bCore.split(".").map((p) => Number.parseInt(p, 10) || 0);
7177
+ for (let i = 0; i < 3; i++) {
7178
+ const diff = (aParts[i] ?? 0) - (bParts[i] ?? 0);
7179
+ if (diff !== 0) return diff;
7180
+ }
7181
+ if (!aPre && !bPre) return 0;
7182
+ if (!aPre) return 1;
7183
+ if (!bPre) return -1;
7184
+ return aPre < bPre ? -1 : aPre > bPre ? 1 : 0;
7185
+ }
7186
+ function isNpxInstall() {
7187
+ const bin = process.argv[1] ?? "";
7188
+ if (/[/\\]_npx[/\\]/.test(bin)) return true;
7189
+ if (/[/\\]\.pnpm[/\\]/.test(bin) && /dlx/i.test(bin)) return true;
7190
+ const ua = process.env.npm_config_user_agent ?? "";
7191
+ if (ua.includes("npx/")) return true;
7192
+ return false;
7193
+ }
7194
+
6892
7195
  // src/mcp/types.ts
6893
7196
  var MCP_PROTOCOL_VERSION = "2024-11-05";
6894
7197
  function isJsonRpcError(msg) {
@@ -6917,7 +7220,7 @@ var McpClient = class {
6917
7220
  nextProgressToken = 1;
6918
7221
  constructor(opts) {
6919
7222
  this.transport = opts.transport;
6920
- this.clientInfo = opts.clientInfo ?? { name: "reasonix", version: "0.3.0-dev" };
7223
+ this.clientInfo = opts.clientInfo ?? { name: "reasonix", version: VERSION };
6921
7224
  this.requestTimeoutMs = opts.requestTimeoutMs ?? 6e4;
6922
7225
  }
6923
7226
  /** Server's advertised capabilities, available after initialize(). */
@@ -7657,8 +7960,8 @@ async function trySection(load) {
7657
7960
  }
7658
7961
 
7659
7962
  // 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";
7963
+ import { existsSync as existsSync10, mkdirSync as mkdirSync4, readFileSync as readFileSync11, unlinkSync as unlinkSync3, writeFileSync as writeFileSync4 } from "fs";
7964
+ import { dirname as dirname5, resolve as resolve8 } from "path";
7662
7965
  var BLOCK_RE = /^(\S[^\n]*)\n<{7} SEARCH\n([\s\S]*?)\n?={7}\n([\s\S]*?)\n?>{7} REPLACE/gm;
7663
7966
  function parseEditBlocks(text) {
7664
7967
  const out = [];
@@ -7686,7 +7989,7 @@ function applyEditBlock(block, rootDir) {
7686
7989
  };
7687
7990
  }
7688
7991
  const searchEmpty = block.search.length === 0;
7689
- const exists = existsSync9(absTarget);
7992
+ const exists = existsSync10(absTarget);
7690
7993
  try {
7691
7994
  if (!exists) {
7692
7995
  if (!searchEmpty) {
@@ -7696,11 +7999,11 @@ function applyEditBlock(block, rootDir) {
7696
7999
  message: "file does not exist; to create it, use an empty SEARCH block"
7697
8000
  };
7698
8001
  }
7699
- mkdirSync3(dirname4(absTarget), { recursive: true });
7700
- writeFileSync3(absTarget, block.replace, "utf8");
8002
+ mkdirSync4(dirname5(absTarget), { recursive: true });
8003
+ writeFileSync4(absTarget, block.replace, "utf8");
7701
8004
  return { path: block.path, status: "created" };
7702
8005
  }
7703
- const content = readFileSync10(absTarget, "utf8");
8006
+ const content = readFileSync11(absTarget, "utf8");
7704
8007
  if (searchEmpty) {
7705
8008
  return {
7706
8009
  path: block.path,
@@ -7717,7 +8020,7 @@ function applyEditBlock(block, rootDir) {
7717
8020
  };
7718
8021
  }
7719
8022
  const replaced = `${content.slice(0, idx)}${block.replace}${content.slice(idx + block.search.length)}`;
7720
- writeFileSync3(absTarget, replaced, "utf8");
8023
+ writeFileSync4(absTarget, replaced, "utf8");
7721
8024
  return { path: block.path, status: "applied" };
7722
8025
  } catch (err) {
7723
8026
  return { path: block.path, status: "error", message: err.message };
@@ -7734,12 +8037,12 @@ function snapshotBeforeEdits(blocks, rootDir) {
7734
8037
  if (seen.has(b.path)) continue;
7735
8038
  seen.add(b.path);
7736
8039
  const abs = resolve8(absRoot, b.path);
7737
- if (!existsSync9(abs)) {
8040
+ if (!existsSync10(abs)) {
7738
8041
  snapshots.push({ path: b.path, prevContent: null });
7739
8042
  continue;
7740
8043
  }
7741
8044
  try {
7742
- snapshots.push({ path: b.path, prevContent: readFileSync10(abs, "utf8") });
8045
+ snapshots.push({ path: b.path, prevContent: readFileSync11(abs, "utf8") });
7743
8046
  } catch {
7744
8047
  snapshots.push({ path: b.path, prevContent: null });
7745
8048
  }
@@ -7759,14 +8062,14 @@ function restoreSnapshots(snapshots, rootDir) {
7759
8062
  }
7760
8063
  try {
7761
8064
  if (snap.prevContent === null) {
7762
- if (existsSync9(abs)) unlinkSync3(abs);
8065
+ if (existsSync10(abs)) unlinkSync3(abs);
7763
8066
  return {
7764
8067
  path: snap.path,
7765
8068
  status: "applied",
7766
8069
  message: "removed (the edit had created it)"
7767
8070
  };
7768
8071
  }
7769
- writeFileSync3(abs, snap.prevContent, "utf8");
8072
+ writeFileSync4(abs, snap.prevContent, "utf8");
7770
8073
  return {
7771
8074
  path: snap.path,
7772
8075
  status: "applied",
@@ -7782,8 +8085,8 @@ function sep() {
7782
8085
  }
7783
8086
 
7784
8087
  // src/code/prompt.ts
7785
- import { existsSync as existsSync10, readFileSync as readFileSync11 } from "fs";
7786
- import { join as join9 } from "path";
8088
+ import { existsSync as existsSync11, readFileSync as readFileSync12 } from "fs";
8089
+ import { join as join10 } from "path";
7787
8090
  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
8091
 
7789
8092
  # Cite or shut up \u2014 non-negotiable
@@ -7989,11 +8292,11 @@ If \`semantic_search\` returns nothing useful (low scores, off-topic), THEN fall
7989
8292
  function codeSystemPrompt(rootDir, opts = {}) {
7990
8293
  const base = opts.hasSemanticSearch ? `${CODE_SYSTEM_PROMPT}${SEMANTIC_SEARCH_ROUTING}` : CODE_SYSTEM_PROMPT;
7991
8294
  const withMemory = applyMemoryStack(base, rootDir);
7992
- const gitignorePath = join9(rootDir, ".gitignore");
7993
- if (!existsSync10(gitignorePath)) return withMemory;
8295
+ const gitignorePath = join10(rootDir, ".gitignore");
8296
+ if (!existsSync11(gitignorePath)) return withMemory;
7994
8297
  let content;
7995
8298
  try {
7996
- content = readFileSync11(gitignorePath, "utf8");
8299
+ content = readFileSync12(gitignorePath, "utf8");
7997
8300
  } catch {
7998
8301
  return withMemory;
7999
8302
  }
@@ -8013,15 +8316,15 @@ ${truncated}
8013
8316
  }
8014
8317
 
8015
8318
  // 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";
8319
+ import { chmodSync as chmodSync2, mkdirSync as mkdirSync5, readFileSync as readFileSync13, writeFileSync as writeFileSync5 } from "fs";
8320
+ import { homedir as homedir6 } from "os";
8321
+ import { dirname as dirname6, join as join11 } from "path";
8019
8322
  function defaultConfigPath() {
8020
- return join10(homedir5(), ".reasonix", "config.json");
8323
+ return join11(homedir6(), ".reasonix", "config.json");
8021
8324
  }
8022
8325
  function readConfig(path = defaultConfigPath()) {
8023
8326
  try {
8024
- const raw = readFileSync12(path, "utf8");
8327
+ const raw = readFileSync13(path, "utf8");
8025
8328
  const parsed = JSON.parse(raw);
8026
8329
  if (parsed && typeof parsed === "object") return parsed;
8027
8330
  } catch {
@@ -8029,8 +8332,8 @@ function readConfig(path = defaultConfigPath()) {
8029
8332
  return {};
8030
8333
  }
8031
8334
  function writeConfig(cfg, path = defaultConfigPath()) {
8032
- mkdirSync4(dirname5(path), { recursive: true });
8033
- writeFileSync4(path, JSON.stringify(cfg, null, 2), "utf8");
8335
+ mkdirSync5(dirname6(path), { recursive: true });
8336
+ writeFileSync5(path, JSON.stringify(cfg, null, 2), "utf8");
8034
8337
  try {
8035
8338
  chmodSync2(path, 384);
8036
8339
  } catch {
@@ -8055,113 +8358,53 @@ function redactKey(key) {
8055
8358
  return `${key.slice(0, 6)}\u2026${key.slice(-4)}`;
8056
8359
  }
8057
8360
 
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");
8361
+ // src/usage.ts
8362
+ import {
8363
+ appendFileSync as appendFileSync2,
8364
+ existsSync as existsSync12,
8365
+ mkdirSync as mkdirSync6,
8366
+ readFileSync as readFileSync14,
8367
+ statSync as statSync5,
8368
+ writeFileSync as writeFileSync6
8369
+ } from "fs";
8370
+ import { homedir as homedir7 } from "os";
8371
+ import { dirname as dirname7, join as join12 } from "path";
8372
+ function defaultUsageLogPath(homeDirOverride) {
8373
+ return join12(homeDirOverride ?? homedir7(), ".reasonix", "usage.jsonl");
8088
8374
  }
8089
- function readCache(homeDirOverride) {
8375
+ var USAGE_COMPACTION_THRESHOLD_BYTES = 5 * 1024 * 1024;
8376
+ var USAGE_RETENTION_DAYS = 365;
8377
+ function compactUsageLogIfLarge(path, now) {
8378
+ let size;
8090
8379
  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
- }
8380
+ size = statSync5(path).size;
8096
8381
  } catch {
8382
+ return;
8097
8383
  }
8098
- return null;
8099
- }
8100
- function writeCache(entry, homeDirOverride) {
8384
+ if (size < USAGE_COMPACTION_THRESHOLD_BYTES) return;
8385
+ const cutoff = now - USAGE_RETENTION_DAYS * 24 * 60 * 60 * 1e3;
8386
+ let raw;
8101
8387
  try {
8102
- const p = cachePath(homeDirOverride);
8103
- mkdirSync5(dirname6(p), { recursive: true });
8104
- writeFileSync5(p, JSON.stringify(entry), "utf8");
8388
+ raw = readFileSync14(path, "utf8");
8105
8389
  } catch {
8390
+ return;
8106
8391
  }
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;
8392
+ const lines = raw.split(/\r?\n/);
8393
+ const kept = [];
8394
+ for (const line of lines) {
8395
+ if (!line.trim()) continue;
8396
+ try {
8397
+ const rec = JSON.parse(line);
8398
+ if (isValidRecord(rec) && rec.ts >= cutoff) kept.push(line);
8399
+ } catch {
8400
+ }
8113
8401
  }
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);
8402
+ if (kept.length === lines.filter((l) => l.trim()).length) return;
8120
8403
  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;
8404
+ writeFileSync6(path, kept.length > 0 ? `${kept.join("\n")}
8405
+ ` : "", "utf8");
8130
8406
  } 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
8407
  }
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
8408
  }
8166
8409
  function appendUsage(input) {
8167
8410
  const record = {
@@ -8182,6 +8425,7 @@ function appendUsage(input) {
8182
8425
  mkdirSync6(dirname7(path), { recursive: true });
8183
8426
  appendFileSync2(path, `${JSON.stringify(record)}
8184
8427
  `, "utf8");
8428
+ compactUsageLogIfLarge(path, record.ts);
8185
8429
  } catch {
8186
8430
  }
8187
8431
  return record;
@@ -8401,6 +8645,7 @@ export {
8401
8645
  isPlanStateEmpty,
8402
8646
  isPlausibleKey,
8403
8647
  listFilesSync,
8648
+ listFilesWithStatsAsync,
8404
8649
  listFilesWithStatsSync,
8405
8650
  listSessions,
8406
8651
  loadApiKey,