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/cli/index.js +442 -190
- package/dist/cli/index.js.map +1 -1
- package/dist/index.d.ts +62 -1
- package/dist/index.js +346 -145
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
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
|
-
|
|
677
|
-
|
|
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
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
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:
|
|
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:
|
|
710
|
-
stderr:
|
|
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
|
|
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
|
|
1815
|
+
const stat2 = statSync(path5);
|
|
1752
1816
|
const name = file.replace(/\.jsonl$/, "");
|
|
1753
1817
|
const messageCount = countLines(path5);
|
|
1754
|
-
return { name, path: path5, size:
|
|
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
|
|
3670
|
-
if (
|
|
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
|
|
4066
|
+
let stat2;
|
|
3940
4067
|
try {
|
|
3941
|
-
|
|
4068
|
+
stat2 = await fs.stat(full);
|
|
3942
4069
|
} catch {
|
|
3943
4070
|
continue;
|
|
3944
4071
|
}
|
|
3945
|
-
if (
|
|
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(
|
|
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
|
|
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:
|
|
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
|
|
7124
|
-
import { dirname as
|
|
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 =
|
|
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
|
-
|
|
7163
|
-
|
|
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 =
|
|
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
|
-
|
|
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 (
|
|
7464
|
+
if (existsSync7(abs)) {
|
|
7196
7465
|
try {
|
|
7197
|
-
search =
|
|
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 (!
|
|
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:
|
|
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 (
|
|
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
|
-
|
|
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/
|
|
7260
|
-
import {
|
|
7261
|
-
|
|
7262
|
-
|
|
7263
|
-
|
|
7264
|
-
|
|
7265
|
-
|
|
7266
|
-
|
|
7267
|
-
|
|
7268
|
-
|
|
7269
|
-
|
|
7270
|
-
|
|
7271
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
7300
|
-
|
|
7301
|
-
|
|
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
|
-
|
|
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
|
-
|
|
7310
|
-
const
|
|
7311
|
-
|
|
7312
|
-
|
|
7313
|
-
|
|
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
|
-
|
|
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
|
-
|
|
7323
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
7990
|
-
if (
|
|
8199
|
+
const stat2 = statSync6(target);
|
|
8200
|
+
if (stat2.isDirectory()) {
|
|
7991
8201
|
return { status: 400, body: { error: "path is a directory" } };
|
|
7992
8202
|
}
|
|
7993
|
-
if (
|
|
8203
|
+
if (stat2.size > MAX_BYTES) {
|
|
7994
8204
|
return {
|
|
7995
8205
|
status: 413,
|
|
7996
|
-
body: { error: `file too large (${
|
|
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:
|
|
8012
|
-
mtime:
|
|
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
|
-
|
|
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
|
|
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:
|
|
8045
|
-
mtime:
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
8585
|
+
const stat2 = statSync8(join13(dir, f));
|
|
8376
8586
|
return {
|
|
8377
8587
|
name: f.replace(/\.md$/, ""),
|
|
8378
|
-
size:
|
|
8379
|
-
mtime:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
8996
|
+
let stat2;
|
|
8787
8997
|
try {
|
|
8788
|
-
|
|
8998
|
+
stat2 = await fs2.stat(abs);
|
|
8789
8999
|
} catch {
|
|
8790
9000
|
continue;
|
|
8791
9001
|
}
|
|
8792
|
-
if (
|
|
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
|
|
9169
|
-
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
|
|
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
|
|
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:
|
|
10018
|
-
mtime:
|
|
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
|
-
|
|
10297
|
+
writeFileSync11(skillPath, contents, "utf8");
|
|
10088
10298
|
ctx.audit?.({
|
|
10089
10299
|
ts: Date.now(),
|
|
10090
10300
|
action: "save-skill",
|
|
@@ -11640,26 +11850,26 @@ function validateCitation(url, projectRoot) {
|
|
|
11640
11850
|
...siblings.map((ext) => baseFullPath.replace(/\.[^./\\]+$/, ext))
|
|
11641
11851
|
];
|
|
11642
11852
|
let fullPath = baseFullPath;
|
|
11643
|
-
let
|
|
11853
|
+
let stat2 = null;
|
|
11644
11854
|
for (const candidate of candidates) {
|
|
11645
11855
|
try {
|
|
11646
|
-
|
|
11856
|
+
stat2 = statSync11(candidate);
|
|
11647
11857
|
fullPath = candidate;
|
|
11648
11858
|
break;
|
|
11649
11859
|
} catch {
|
|
11650
11860
|
}
|
|
11651
11861
|
}
|
|
11652
|
-
if (!
|
|
11653
|
-
if (!
|
|
11862
|
+
if (!stat2) return { ok: false, reason: "file not found" };
|
|
11863
|
+
if (!stat2.isFile()) return { ok: false, reason: "not a file" };
|
|
11654
11864
|
if (parts.startLine === void 0) return { ok: true };
|
|
11655
|
-
let lineCount = getCachedLineCount(fullPath,
|
|
11865
|
+
let lineCount = getCachedLineCount(fullPath, stat2.mtimeMs);
|
|
11656
11866
|
if (lineCount === null) {
|
|
11657
11867
|
try {
|
|
11658
11868
|
lineCount = readFileSync19(fullPath, "utf8").split("\n").length;
|
|
11659
11869
|
} catch {
|
|
11660
11870
|
return { ok: false, reason: "unreadable" };
|
|
11661
11871
|
}
|
|
11662
|
-
setCachedLineCount(fullPath,
|
|
11872
|
+
setCachedLineCount(fullPath, stat2.mtimeMs, lineCount);
|
|
11663
11873
|
}
|
|
11664
11874
|
if (parts.startLine < 1 || parts.startLine > lineCount) {
|
|
11665
11875
|
return { ok: false, reason: `line ${parts.startLine} > ${lineCount}` };
|
|
@@ -12454,7 +12664,7 @@ var EventRow = React11.memo(function EventRow2({
|
|
|
12454
12664
|
event.branch ? /* @__PURE__ */ React11.createElement(BranchBlock, { branch: event.branch }) : null,
|
|
12455
12665
|
event.reasoning ? /* @__PURE__ */ React11.createElement(ReasoningBlock, { reasoning: event.reasoning }) : null,
|
|
12456
12666
|
!isPlanStateEmpty(event.planState) ? /* @__PURE__ */ React11.createElement(PlanStateBlock, { planState: event.planState }) : null,
|
|
12457
|
-
event.text ? /* @__PURE__ */ React11.createElement(Markdown, { text: event.text, projectRoot }) : /* @__PURE__ */ React11.createElement(Text8, { dimColor: true }, "(
|
|
12667
|
+
event.text ? /* @__PURE__ */ React11.createElement(Markdown, { text: event.text, projectRoot }) : /* @__PURE__ */ React11.createElement(Text8, { dimColor: true }, "(empty body \u2014 likely tool-call only)"),
|
|
12458
12668
|
event.stats ? /* @__PURE__ */ React11.createElement(StatsLine, { stats: event.stats }) : null,
|
|
12459
12669
|
event.repair ? /* @__PURE__ */ React11.createElement(Text8, { color: COLOR.accent }, event.repair) : null
|
|
12460
12670
|
));
|
|
@@ -14195,7 +14405,7 @@ function describeRepair(repair) {
|
|
|
14195
14405
|
}
|
|
14196
14406
|
|
|
14197
14407
|
// src/cli/ui/hash-memory.ts
|
|
14198
|
-
import { appendFileSync as appendFileSync3, existsSync as existsSync18, mkdirSync as mkdirSync12, readFileSync as readFileSync20, writeFileSync as
|
|
14408
|
+
import { appendFileSync as appendFileSync3, existsSync as existsSync18, mkdirSync as mkdirSync12, readFileSync as readFileSync20, writeFileSync as writeFileSync12 } from "fs";
|
|
14199
14409
|
import { homedir as homedir9 } from "os";
|
|
14200
14410
|
import { dirname as dirname15, join as join17 } from "path";
|
|
14201
14411
|
var PROJECT_HEADER = `# Reasonix project memory
|
|
@@ -14246,7 +14456,7 @@ function appendBulletToFile(path5, note, newFileHeader) {
|
|
|
14246
14456
|
`;
|
|
14247
14457
|
if (!existsSync18(path5)) {
|
|
14248
14458
|
mkdirSync12(dirname15(path5), { recursive: true });
|
|
14249
|
-
|
|
14459
|
+
writeFileSync12(path5, `${newFileHeader}${bullet}`, "utf8");
|
|
14250
14460
|
return { path: path5, created: true };
|
|
14251
14461
|
}
|
|
14252
14462
|
let prefix = "";
|
|
@@ -14692,6 +14902,11 @@ var SLASH_COMMANDS = [
|
|
|
14692
14902
|
},
|
|
14693
14903
|
{ cmd: "sessions", summary: "list saved sessions (current marked with \u25B8)" },
|
|
14694
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
|
+
},
|
|
14695
14910
|
{ cmd: "setup", summary: "reminds you to exit and run `reasonix setup`" },
|
|
14696
14911
|
{
|
|
14697
14912
|
cmd: "semantic",
|
|
@@ -16661,9 +16876,9 @@ async function indexFileExists(rootDir) {
|
|
|
16661
16876
|
async function readIndexMeta(rootDir) {
|
|
16662
16877
|
const dataPath = path4.join(rootDir, ".reasonix", "semantic", "index.jsonl");
|
|
16663
16878
|
try {
|
|
16664
|
-
const
|
|
16665
|
-
if (
|
|
16666
|
-
return { chunks: Math.round(
|
|
16879
|
+
const stat2 = await fs5.stat(dataPath);
|
|
16880
|
+
if (stat2.size > 10 * 1024 * 1024) {
|
|
16881
|
+
return { chunks: Math.round(stat2.size / 500), files: 0 };
|
|
16667
16882
|
}
|
|
16668
16883
|
const raw = await fs5.readFile(dataPath, "utf8");
|
|
16669
16884
|
const seenPaths = /* @__PURE__ */ new Set();
|
|
@@ -16687,6 +16902,7 @@ var handlers13 = {
|
|
|
16687
16902
|
};
|
|
16688
16903
|
|
|
16689
16904
|
// src/cli/ui/slash/handlers/sessions.ts
|
|
16905
|
+
var STALE_THRESHOLD_DAYS = 90;
|
|
16690
16906
|
var sessions = (_args, loop2) => {
|
|
16691
16907
|
const items = listSessions();
|
|
16692
16908
|
if (items.length === 0) {
|
|
@@ -16694,17 +16910,28 @@ var sessions = (_args, loop2) => {
|
|
|
16694
16910
|
info: "no saved sessions yet \u2014 chat normally and your messages will be saved automatically"
|
|
16695
16911
|
};
|
|
16696
16912
|
}
|
|
16913
|
+
const now = Date.now();
|
|
16697
16914
|
const lines = ["Saved sessions:"];
|
|
16915
|
+
let staleCount = 0;
|
|
16698
16916
|
for (const s of items) {
|
|
16699
16917
|
const sizeKb = (s.size / 1024).toFixed(1);
|
|
16700
16918
|
const when = s.mtime.toISOString().replace("T", " ").slice(0, 16);
|
|
16701
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++;
|
|
16702
16924
|
lines.push(
|
|
16703
|
-
` ${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}`
|
|
16704
16926
|
);
|
|
16705
16927
|
}
|
|
16706
16928
|
lines.push("");
|
|
16707
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
|
+
}
|
|
16708
16935
|
return { info: lines.join("\n") };
|
|
16709
16936
|
};
|
|
16710
16937
|
var forget = (_args, loop2) => {
|
|
@@ -16717,9 +16944,26 @@ var forget = (_args, loop2) => {
|
|
|
16717
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?)`
|
|
16718
16945
|
};
|
|
16719
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
|
+
};
|
|
16720
16963
|
var handlers14 = {
|
|
16721
16964
|
sessions,
|
|
16722
|
-
forget
|
|
16965
|
+
forget,
|
|
16966
|
+
"prune-sessions": pruneSessions
|
|
16723
16967
|
};
|
|
16724
16968
|
|
|
16725
16969
|
// src/cli/ui/slash/handlers/skill.ts
|
|
@@ -16845,13 +17089,21 @@ function useCompletionPickers({
|
|
|
16845
17089
|
});
|
|
16846
17090
|
}, [slashMatches]);
|
|
16847
17091
|
const [atSelected, setAtSelected] = useState6(0);
|
|
16848
|
-
const atFiles =
|
|
16849
|
-
|
|
16850
|
-
|
|
16851
|
-
|
|
16852
|
-
|
|
16853
|
-
return [];
|
|
17092
|
+
const [atFiles, setAtFiles] = useState6([]);
|
|
17093
|
+
useEffect3(() => {
|
|
17094
|
+
if (!codeMode) {
|
|
17095
|
+
setAtFiles([]);
|
|
17096
|
+
return;
|
|
16854
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
|
+
};
|
|
16855
17107
|
}, [codeMode, rootDir]);
|
|
16856
17108
|
const recentFilesRef = useRef3([]);
|
|
16857
17109
|
const recordRecentFile = useCallback((p) => {
|
|
@@ -20237,7 +20489,7 @@ async function codeCommand(opts = {}) {
|
|
|
20237
20489
|
}
|
|
20238
20490
|
|
|
20239
20491
|
// src/cli/commands/diff.ts
|
|
20240
|
-
import { writeFileSync as
|
|
20492
|
+
import { writeFileSync as writeFileSync13 } from "fs";
|
|
20241
20493
|
import { basename as basename3 } from "path";
|
|
20242
20494
|
import { render as render2 } from "ink";
|
|
20243
20495
|
import React30 from "react";
|
|
@@ -20384,7 +20636,7 @@ async function diffCommand(opts) {
|
|
|
20384
20636
|
if (wantMarkdown) {
|
|
20385
20637
|
console.log(renderSummaryTable(report));
|
|
20386
20638
|
const md = renderMarkdown(report);
|
|
20387
|
-
|
|
20639
|
+
writeFileSync13(opts.mdPath, md, "utf8");
|
|
20388
20640
|
console.log(`
|
|
20389
20641
|
markdown report written to ${opts.mdPath}`);
|
|
20390
20642
|
return;
|