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/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
|
-
|
|
543
|
-
|
|
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
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
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:
|
|
576
|
-
stderr:
|
|
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
|
|
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
|
|
1681
|
+
const stat2 = statSync(path);
|
|
1618
1682
|
const name = file.replace(/\.jsonl$/, "");
|
|
1619
1683
|
const messageCount = countLines(path);
|
|
1620
|
-
return { name, path, size:
|
|
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
|
|
4184
|
-
if (
|
|
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
|
|
4570
|
+
let stat2;
|
|
4454
4571
|
try {
|
|
4455
|
-
|
|
4572
|
+
stat2 = await fs.stat(full);
|
|
4456
4573
|
} catch {
|
|
4457
4574
|
continue;
|
|
4458
4575
|
}
|
|
4459
|
-
if (
|
|
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(
|
|
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
|
|
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:
|
|
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
|
|
7661
|
-
import { dirname as
|
|
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 =
|
|
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
|
-
|
|
7700
|
-
|
|
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 =
|
|
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
|
-
|
|
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 (!
|
|
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:
|
|
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 (
|
|
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
|
-
|
|
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
|
|
7786
|
-
import { join as
|
|
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 =
|
|
7993
|
-
if (!
|
|
8251
|
+
const gitignorePath = join10(rootDir, ".gitignore");
|
|
8252
|
+
if (!existsSync11(gitignorePath)) return withMemory;
|
|
7994
8253
|
let content;
|
|
7995
8254
|
try {
|
|
7996
|
-
content =
|
|
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
|
|
8017
|
-
import { homedir as
|
|
8018
|
-
import { dirname as
|
|
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
|
|
8279
|
+
return join11(homedir6(), ".reasonix", "config.json");
|
|
8021
8280
|
}
|
|
8022
8281
|
function readConfig(path = defaultConfigPath()) {
|
|
8023
8282
|
try {
|
|
8024
|
-
const raw =
|
|
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
|
-
|
|
8033
|
-
|
|
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/
|
|
8059
|
-
import {
|
|
8060
|
-
|
|
8061
|
-
|
|
8062
|
-
|
|
8063
|
-
|
|
8064
|
-
|
|
8065
|
-
|
|
8066
|
-
|
|
8067
|
-
|
|
8068
|
-
|
|
8069
|
-
|
|
8070
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
8099
|
-
|
|
8100
|
-
|
|
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
|
-
|
|
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
|
-
|
|
8109
|
-
const
|
|
8110
|
-
|
|
8111
|
-
|
|
8112
|
-
|
|
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
|
-
|
|
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
|
-
|
|
8122
|
-
|
|
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,
|