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/cli/index.js +606 -229
- package/dist/cli/index.js.map +1 -1
- package/dist/index.d.ts +94 -1
- package/dist/index.js +390 -145
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/cli/index.js
CHANGED
|
@@ -548,8 +548,8 @@ var defaultSelector = (samples) => {
|
|
|
548
548
|
})[0];
|
|
549
549
|
};
|
|
550
550
|
async function runBranches(client, request, opts = {}) {
|
|
551
|
-
const
|
|
552
|
-
const temperatures = resolveTemperatures(
|
|
551
|
+
const budget2 = Math.max(1, opts.budget ?? 1);
|
|
552
|
+
const temperatures = resolveTemperatures(budget2, opts.temperatures);
|
|
553
553
|
const selector = opts.selector ?? defaultSelector;
|
|
554
554
|
const samples = await Promise.all(
|
|
555
555
|
temperatures.map(async (temperature, index) => {
|
|
@@ -586,12 +586,12 @@ function aggregateBranchUsage(samples) {
|
|
|
586
586
|
promptCacheMissTokens
|
|
587
587
|
};
|
|
588
588
|
}
|
|
589
|
-
function resolveTemperatures(
|
|
590
|
-
if (custom && custom.length >=
|
|
591
|
-
if (
|
|
589
|
+
function resolveTemperatures(budget2, custom) {
|
|
590
|
+
if (custom && custom.length >= budget2) return [...custom.slice(0, budget2)];
|
|
591
|
+
if (budget2 === 1) return [0];
|
|
592
592
|
const out = [];
|
|
593
|
-
for (let i = 0; i <
|
|
594
|
-
out.push(Number((i / (
|
|
593
|
+
for (let i = 0; i < budget2; i++) {
|
|
594
|
+
out.push(Number((i / (budget2 - 1)).toFixed(2)));
|
|
595
595
|
}
|
|
596
596
|
return out;
|
|
597
597
|
}
|
|
@@ -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;
|
|
@@ -1253,29 +1276,29 @@ function truncateForModelByTokens(s, maxTokens) {
|
|
|
1253
1276
|
|
|
1254
1277
|
${tail}`;
|
|
1255
1278
|
}
|
|
1256
|
-
function sizePrefixToTokens(s,
|
|
1257
|
-
if (
|
|
1258
|
-
let size = Math.min(s.length,
|
|
1279
|
+
function sizePrefixToTokens(s, budget2) {
|
|
1280
|
+
if (budget2 <= 0 || s.length === 0) return "";
|
|
1281
|
+
let size = Math.min(s.length, budget2 * 4);
|
|
1259
1282
|
for (let iter = 0; iter < 6; iter++) {
|
|
1260
1283
|
if (size <= 0) return "";
|
|
1261
1284
|
const slice = s.slice(0, size);
|
|
1262
1285
|
const count = countTokens(slice);
|
|
1263
|
-
if (count <=
|
|
1264
|
-
const next = Math.floor(size * (
|
|
1286
|
+
if (count <= budget2) return slice;
|
|
1287
|
+
const next = Math.floor(size * (budget2 / count) * 0.95);
|
|
1265
1288
|
if (next >= size) return s.slice(0, Math.max(0, size - 1));
|
|
1266
1289
|
size = next;
|
|
1267
1290
|
}
|
|
1268
1291
|
return s.slice(0, Math.max(0, size));
|
|
1269
1292
|
}
|
|
1270
|
-
function sizeSuffixToTokens(s,
|
|
1271
|
-
if (
|
|
1272
|
-
let size = Math.min(s.length,
|
|
1293
|
+
function sizeSuffixToTokens(s, budget2) {
|
|
1294
|
+
if (budget2 <= 0 || s.length === 0) return "";
|
|
1295
|
+
let size = Math.min(s.length, budget2 * 4);
|
|
1273
1296
|
for (let iter = 0; iter < 6; iter++) {
|
|
1274
1297
|
if (size <= 0) return "";
|
|
1275
1298
|
const slice = s.slice(-size);
|
|
1276
1299
|
const count = countTokens(slice);
|
|
1277
|
-
if (count <=
|
|
1278
|
-
const next = Math.floor(size * (
|
|
1300
|
+
if (count <= budget2) return slice;
|
|
1301
|
+
const next = Math.floor(size * (budget2 / count) * 0.95);
|
|
1279
1302
|
if (next >= size) return s.slice(-Math.max(0, size - 1));
|
|
1280
1303
|
size = next;
|
|
1281
1304
|
}
|
|
@@ -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 {
|
|
@@ -1925,6 +1999,19 @@ var CacheFirstLoop = class {
|
|
|
1925
1999
|
* flip it live alongside `model`.
|
|
1926
2000
|
*/
|
|
1927
2001
|
autoEscalate = true;
|
|
2002
|
+
/**
|
|
2003
|
+
* Soft USD budget — see {@link CacheFirstLoopOptions.budgetUsd}.
|
|
2004
|
+
* Mutable so `/budget` slash can set / change / clear it mid-session.
|
|
2005
|
+
* `null` (the default) disables all budget checks.
|
|
2006
|
+
*/
|
|
2007
|
+
budgetUsd;
|
|
2008
|
+
/**
|
|
2009
|
+
* Set the first time a turn crosses 80% of the budget so the warning
|
|
2010
|
+
* doesn't repeat every turn afterwards. Cleared by `setBudget` (any
|
|
2011
|
+
* change re-arms the warning, including raising the cap above the
|
|
2012
|
+
* current spend).
|
|
2013
|
+
*/
|
|
2014
|
+
_budgetWarned = false;
|
|
1928
2015
|
sessionName;
|
|
1929
2016
|
/**
|
|
1930
2017
|
* Hook list, mutable so `/hooks reload` can swap it without
|
|
@@ -1990,6 +2077,7 @@ var CacheFirstLoop = class {
|
|
|
1990
2077
|
this.model = opts.model ?? "deepseek-v4-flash";
|
|
1991
2078
|
this.reasoningEffort = opts.reasoningEffort ?? "max";
|
|
1992
2079
|
if (opts.autoEscalate !== void 0) this.autoEscalate = opts.autoEscalate;
|
|
2080
|
+
this.budgetUsd = typeof opts.budgetUsd === "number" && opts.budgetUsd > 0 ? opts.budgetUsd : null;
|
|
1993
2081
|
this.maxToolIters = opts.maxToolIters ?? 64;
|
|
1994
2082
|
this.hooks = opts.hooks ?? [];
|
|
1995
2083
|
this.hookCwd = opts.hookCwd ?? process.cwd();
|
|
@@ -2229,6 +2317,16 @@ var CacheFirstLoop = class {
|
|
|
2229
2317
|
}
|
|
2230
2318
|
this.stream = this.branchEnabled ? false : this._streamPreference;
|
|
2231
2319
|
}
|
|
2320
|
+
/**
|
|
2321
|
+
* Set / change / clear the soft USD budget. `null` (or any non-
|
|
2322
|
+
* positive number) disables the cap entirely. Re-arms the 80%
|
|
2323
|
+
* warning so a user who bumps the cap mid-session sees a fresh
|
|
2324
|
+
* threshold message at the new boundary.
|
|
2325
|
+
*/
|
|
2326
|
+
setBudget(usd) {
|
|
2327
|
+
this.budgetUsd = typeof usd === "number" && usd > 0 ? usd : null;
|
|
2328
|
+
this._budgetWarned = false;
|
|
2329
|
+
}
|
|
2232
2330
|
/**
|
|
2233
2331
|
* Arm pro for the next turn (consumed at turn start). Called by
|
|
2234
2332
|
* `/pro`. Idempotent — repeated calls stay armed, `disarmPro()`
|
|
@@ -2390,6 +2488,26 @@ var CacheFirstLoop = class {
|
|
|
2390
2488
|
return userText;
|
|
2391
2489
|
}
|
|
2392
2490
|
async *step(userInput) {
|
|
2491
|
+
if (this.budgetUsd !== null) {
|
|
2492
|
+
const spent = this.stats.totalCost;
|
|
2493
|
+
if (spent >= this.budgetUsd) {
|
|
2494
|
+
yield {
|
|
2495
|
+
turn: this._turn,
|
|
2496
|
+
role: "error",
|
|
2497
|
+
content: "",
|
|
2498
|
+
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.`
|
|
2499
|
+
};
|
|
2500
|
+
return;
|
|
2501
|
+
}
|
|
2502
|
+
if (!this._budgetWarned && spent >= this.budgetUsd * 0.8) {
|
|
2503
|
+
this._budgetWarned = true;
|
|
2504
|
+
yield {
|
|
2505
|
+
turn: this._turn,
|
|
2506
|
+
role: "warning",
|
|
2507
|
+
content: `\u25B2 budget 80% used \u2014 $${spent.toFixed(4)} of $${this.budgetUsd.toFixed(2)}. Next turn or two likely trips the cap.`
|
|
2508
|
+
};
|
|
2509
|
+
}
|
|
2510
|
+
}
|
|
2393
2511
|
this._turn++;
|
|
2394
2512
|
this.scratch.reset();
|
|
2395
2513
|
this.repair.resetStorm();
|
|
@@ -2486,14 +2604,14 @@ var CacheFirstLoop = class {
|
|
|
2486
2604
|
let preHarvestedPlanState;
|
|
2487
2605
|
try {
|
|
2488
2606
|
if (this.branchEnabled) {
|
|
2489
|
-
const
|
|
2607
|
+
const budget2 = this.branchOptions.budget ?? 1;
|
|
2490
2608
|
yield {
|
|
2491
2609
|
turn: this._turn,
|
|
2492
2610
|
role: "branch_start",
|
|
2493
2611
|
content: "",
|
|
2494
2612
|
branchProgress: {
|
|
2495
2613
|
completed: 0,
|
|
2496
|
-
total:
|
|
2614
|
+
total: budget2,
|
|
2497
2615
|
latestIndex: -1,
|
|
2498
2616
|
latestTemperature: -1,
|
|
2499
2617
|
latestUncertainties: -1
|
|
@@ -2527,7 +2645,7 @@ var CacheFirstLoop = class {
|
|
|
2527
2645
|
onSampleDone
|
|
2528
2646
|
}
|
|
2529
2647
|
);
|
|
2530
|
-
for (let k = 0; k <
|
|
2648
|
+
for (let k = 0; k < budget2; k++) {
|
|
2531
2649
|
const sample = queue.shift() ?? await new Promise((resolve13) => {
|
|
2532
2650
|
waiter = resolve13;
|
|
2533
2651
|
});
|
|
@@ -2537,7 +2655,7 @@ var CacheFirstLoop = class {
|
|
|
2537
2655
|
content: "",
|
|
2538
2656
|
branchProgress: {
|
|
2539
2657
|
completed: k + 1,
|
|
2540
|
-
total:
|
|
2658
|
+
total: budget2,
|
|
2541
2659
|
latestIndex: sample.index,
|
|
2542
2660
|
latestTemperature: sample.temperature,
|
|
2543
2661
|
latestUncertainties: sample.planState.uncertainties.length
|
|
@@ -3252,6 +3370,7 @@ function extractDeepSeekErrorMessage(body) {
|
|
|
3252
3370
|
|
|
3253
3371
|
// src/at-mentions.ts
|
|
3254
3372
|
import { existsSync as existsSync4, readFileSync as readFileSync5, readdirSync as readdirSync2, statSync as statSync2 } from "fs";
|
|
3373
|
+
import { readdir, stat } from "fs/promises";
|
|
3255
3374
|
import { isAbsolute, join as join5, relative, resolve } from "path";
|
|
3256
3375
|
var DEFAULT_AT_MENTION_MAX_BYTES = 64 * 1024;
|
|
3257
3376
|
var DEFAULT_PICKER_IGNORE_DIRS = [
|
|
@@ -3306,6 +3425,58 @@ function listFilesWithStatsSync(root, opts = {}) {
|
|
|
3306
3425
|
walk3(rootAbs, "");
|
|
3307
3426
|
return out;
|
|
3308
3427
|
}
|
|
3428
|
+
async function listFilesWithStatsAsync(root, opts = {}) {
|
|
3429
|
+
const maxResults = Math.max(1, opts.maxResults ?? 500);
|
|
3430
|
+
const ignore = new Set(opts.ignoreDirs ?? DEFAULT_PICKER_IGNORE_DIRS);
|
|
3431
|
+
const rootAbs = resolve(root);
|
|
3432
|
+
const out = [];
|
|
3433
|
+
const walk3 = async (dirAbs, dirRel) => {
|
|
3434
|
+
if (out.length >= maxResults) return;
|
|
3435
|
+
let entries;
|
|
3436
|
+
try {
|
|
3437
|
+
entries = await readdir(dirAbs, { withFileTypes: true });
|
|
3438
|
+
} catch {
|
|
3439
|
+
return;
|
|
3440
|
+
}
|
|
3441
|
+
entries.sort((a, b) => a.name.localeCompare(b.name));
|
|
3442
|
+
const fileEnts = [];
|
|
3443
|
+
for (const ent of entries) {
|
|
3444
|
+
if (out.length >= maxResults) break;
|
|
3445
|
+
if (ent.isDirectory()) {
|
|
3446
|
+
if (ent.name.startsWith(".") || ignore.has(ent.name)) continue;
|
|
3447
|
+
if (fileEnts.length > 0) {
|
|
3448
|
+
await statBatch(fileEnts, dirAbs, dirRel, out, maxResults);
|
|
3449
|
+
fileEnts.length = 0;
|
|
3450
|
+
if (out.length >= maxResults) return;
|
|
3451
|
+
}
|
|
3452
|
+
await walk3(join5(dirAbs, ent.name), dirRel ? `${dirRel}/${ent.name}` : ent.name);
|
|
3453
|
+
} else if (ent.isFile()) {
|
|
3454
|
+
fileEnts.push(ent);
|
|
3455
|
+
}
|
|
3456
|
+
}
|
|
3457
|
+
if (fileEnts.length > 0 && out.length < maxResults) {
|
|
3458
|
+
await statBatch(fileEnts, dirAbs, dirRel, out, maxResults);
|
|
3459
|
+
}
|
|
3460
|
+
};
|
|
3461
|
+
await walk3(rootAbs, "");
|
|
3462
|
+
return out;
|
|
3463
|
+
}
|
|
3464
|
+
async function statBatch(ents, dirAbs, dirRel, out, maxResults) {
|
|
3465
|
+
const remaining = Math.max(0, maxResults - out.length);
|
|
3466
|
+
const batch = ents.slice(0, remaining);
|
|
3467
|
+
const stats2 = await Promise.all(
|
|
3468
|
+
batch.map(
|
|
3469
|
+
(e) => stat(join5(dirAbs, e.name)).then((s) => s.mtimeMs).catch(() => 0)
|
|
3470
|
+
)
|
|
3471
|
+
);
|
|
3472
|
+
for (let i = 0; i < batch.length; i++) {
|
|
3473
|
+
const ent = batch[i];
|
|
3474
|
+
out.push({
|
|
3475
|
+
path: dirRel ? `${dirRel}/${ent.name}` : ent.name,
|
|
3476
|
+
mtimeMs: stats2[i] ?? 0
|
|
3477
|
+
});
|
|
3478
|
+
}
|
|
3479
|
+
}
|
|
3309
3480
|
var AT_PICKER_PREFIX = /(?:^|\s)@([a-zA-Z0-9_./\\-]*)$/;
|
|
3310
3481
|
function detectAtPicker(input) {
|
|
3311
3482
|
const m = AT_PICKER_PREFIX.exec(input);
|
|
@@ -3666,8 +3837,8 @@ When none of these is given AND the file is longer than ${DEFAULT_AUTO_PREVIEW_L
|
|
|
3666
3837
|
},
|
|
3667
3838
|
fn: async (args) => {
|
|
3668
3839
|
const abs = safePath(args.path);
|
|
3669
|
-
const
|
|
3670
|
-
if (
|
|
3840
|
+
const stat2 = await fs.stat(abs);
|
|
3841
|
+
if (stat2.isDirectory()) {
|
|
3671
3842
|
throw new Error(`not a file: ${args.path} (it's a directory)`);
|
|
3672
3843
|
}
|
|
3673
3844
|
const raw = await fs.readFile(abs);
|
|
@@ -3936,13 +4107,13 @@ Prefer \`list_directory\` for a single-level view, \`search_files\` to find spec
|
|
|
3936
4107
|
if (nameFilter && !e.name.toLowerCase().includes(nameFilter)) continue;
|
|
3937
4108
|
if (isLikelyBinaryByName(e.name)) continue;
|
|
3938
4109
|
const full = pathMod.join(dir, e.name);
|
|
3939
|
-
let
|
|
4110
|
+
let stat2;
|
|
3940
4111
|
try {
|
|
3941
|
-
|
|
4112
|
+
stat2 = await fs.stat(full);
|
|
3942
4113
|
} catch {
|
|
3943
4114
|
continue;
|
|
3944
4115
|
}
|
|
3945
|
-
if (
|
|
4116
|
+
if (stat2.size > 2 * 1024 * 1024) continue;
|
|
3946
4117
|
let raw;
|
|
3947
4118
|
try {
|
|
3948
4119
|
raw = await fs.readFile(full);
|
|
@@ -4923,6 +5094,8 @@ var JobRegistry = class {
|
|
|
4923
5094
|
};
|
|
4924
5095
|
this.jobs.set(id, job);
|
|
4925
5096
|
let readyMatched = false;
|
|
5097
|
+
let recentForReady = "";
|
|
5098
|
+
const READY_WINDOW = 1024;
|
|
4926
5099
|
const onData = (chunk) => {
|
|
4927
5100
|
const s = chunk.toString();
|
|
4928
5101
|
job.totalBytesWritten += s.length;
|
|
@@ -4935,8 +5108,9 @@ var JobRegistry = class {
|
|
|
4935
5108
|
${job.output.slice(start)}`;
|
|
4936
5109
|
}
|
|
4937
5110
|
if (!readyMatched) {
|
|
5111
|
+
recentForReady = (recentForReady + s).slice(-READY_WINDOW);
|
|
4938
5112
|
for (const re of READY_SIGNALS) {
|
|
4939
|
-
if (re.test(
|
|
5113
|
+
if (re.test(recentForReady)) {
|
|
4940
5114
|
readyMatched = true;
|
|
4941
5115
|
job.signalReady();
|
|
4942
5116
|
break;
|
|
@@ -5620,6 +5794,7 @@ ${r.output}` : header2;
|
|
|
5620
5794
|
var DEFAULT_FETCH_MAX_CHARS = 32e3;
|
|
5621
5795
|
var DEFAULT_FETCH_TIMEOUT_MS = 15e3;
|
|
5622
5796
|
var DEFAULT_TOPK = 5;
|
|
5797
|
+
var FETCH_MAX_BYTES = 10 * 1024 * 1024;
|
|
5623
5798
|
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
5799
|
var MOJEEK_ENDPOINT = "https://www.mojeek.com/search";
|
|
5625
5800
|
async function webSearch(query, opts = {}) {
|
|
@@ -5699,7 +5874,13 @@ async function webFetch(url, opts = {}) {
|
|
|
5699
5874
|
}
|
|
5700
5875
|
if (!resp.ok) throw new Error(`web_fetch ${resp.status} for ${url}`);
|
|
5701
5876
|
const contentType = resp.headers.get("content-type") ?? "";
|
|
5702
|
-
const
|
|
5877
|
+
const declaredLen = Number(resp.headers.get("content-length") ?? "");
|
|
5878
|
+
if (Number.isFinite(declaredLen) && declaredLen > FETCH_MAX_BYTES) {
|
|
5879
|
+
throw new Error(
|
|
5880
|
+
`web_fetch refused: content-length ${declaredLen} bytes exceeds ${FETCH_MAX_BYTES}-byte cap (${url})`
|
|
5881
|
+
);
|
|
5882
|
+
}
|
|
5883
|
+
const raw = await readBodyCapped(resp, FETCH_MAX_BYTES);
|
|
5703
5884
|
const title = extractTitle(raw);
|
|
5704
5885
|
const text = contentType.includes("text/html") ? htmlToText(raw) : raw;
|
|
5705
5886
|
const truncated = text.length > maxChars;
|
|
@@ -5708,6 +5889,37 @@ async function webFetch(url, opts = {}) {
|
|
|
5708
5889
|
[\u2026 truncated ${text.length - maxChars} chars \u2026]` : text;
|
|
5709
5890
|
return { url, title, text: finalText, truncated };
|
|
5710
5891
|
}
|
|
5892
|
+
async function readBodyCapped(resp, maxBytes) {
|
|
5893
|
+
if (!resp.body) return await resp.text();
|
|
5894
|
+
const reader = resp.body.getReader();
|
|
5895
|
+
const decoder = new TextDecoder("utf-8");
|
|
5896
|
+
let total = 0;
|
|
5897
|
+
let out = "";
|
|
5898
|
+
try {
|
|
5899
|
+
while (true) {
|
|
5900
|
+
const { value, done } = await reader.read();
|
|
5901
|
+
if (done) break;
|
|
5902
|
+
total += value.byteLength;
|
|
5903
|
+
if (total > maxBytes) {
|
|
5904
|
+
try {
|
|
5905
|
+
await reader.cancel();
|
|
5906
|
+
} catch {
|
|
5907
|
+
}
|
|
5908
|
+
throw new Error(
|
|
5909
|
+
`web_fetch refused: response body exceeded ${maxBytes}-byte cap (${total} bytes seen)`
|
|
5910
|
+
);
|
|
5911
|
+
}
|
|
5912
|
+
out += decoder.decode(value, { stream: true });
|
|
5913
|
+
}
|
|
5914
|
+
out += decoder.decode();
|
|
5915
|
+
} finally {
|
|
5916
|
+
try {
|
|
5917
|
+
reader.releaseLock();
|
|
5918
|
+
} catch {
|
|
5919
|
+
}
|
|
5920
|
+
}
|
|
5921
|
+
return out;
|
|
5922
|
+
}
|
|
5711
5923
|
function htmlToText(html) {
|
|
5712
5924
|
let s = html;
|
|
5713
5925
|
s = s.replace(/<script[\s\S]*?<\/script>/gi, "");
|
|
@@ -6352,6 +6564,107 @@ function truncate(s, n) {
|
|
|
6352
6564
|
return s.length > n ? `${s.slice(0, n)}\u2026` : s;
|
|
6353
6565
|
}
|
|
6354
6566
|
|
|
6567
|
+
// src/version.ts
|
|
6568
|
+
import { existsSync as existsSync6, mkdirSync as mkdirSync3, readFileSync as readFileSync8, writeFileSync as writeFileSync3 } from "fs";
|
|
6569
|
+
import { homedir as homedir4 } from "os";
|
|
6570
|
+
import { dirname as dirname5, join as join7 } from "path";
|
|
6571
|
+
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
6572
|
+
var REGISTRY_URL = "https://registry.npmjs.org/reasonix/latest";
|
|
6573
|
+
var LATEST_CACHE_TTL_MS = 24 * 60 * 60 * 1e3;
|
|
6574
|
+
var LATEST_FETCH_TIMEOUT_MS = 2e3;
|
|
6575
|
+
function readPackageVersion() {
|
|
6576
|
+
try {
|
|
6577
|
+
let dir = dirname5(fileURLToPath2(import.meta.url));
|
|
6578
|
+
for (let i = 0; i < 6; i++) {
|
|
6579
|
+
const p = join7(dir, "package.json");
|
|
6580
|
+
if (existsSync6(p)) {
|
|
6581
|
+
const pkg = JSON.parse(readFileSync8(p, "utf8"));
|
|
6582
|
+
if (pkg?.name === "reasonix" && typeof pkg.version === "string") {
|
|
6583
|
+
return pkg.version;
|
|
6584
|
+
}
|
|
6585
|
+
}
|
|
6586
|
+
const parent = dirname5(dir);
|
|
6587
|
+
if (parent === dir) break;
|
|
6588
|
+
dir = parent;
|
|
6589
|
+
}
|
|
6590
|
+
} catch {
|
|
6591
|
+
}
|
|
6592
|
+
return "0.0.0-dev";
|
|
6593
|
+
}
|
|
6594
|
+
var VERSION = readPackageVersion();
|
|
6595
|
+
function cachePath(homeDirOverride) {
|
|
6596
|
+
return join7(homeDirOverride ?? homedir4(), ".reasonix", "version-cache.json");
|
|
6597
|
+
}
|
|
6598
|
+
function readCache(homeDirOverride) {
|
|
6599
|
+
try {
|
|
6600
|
+
const raw = readFileSync8(cachePath(homeDirOverride), "utf8");
|
|
6601
|
+
const parsed = JSON.parse(raw);
|
|
6602
|
+
if (parsed && typeof parsed.version === "string" && typeof parsed.checkedAt === "number") {
|
|
6603
|
+
return parsed;
|
|
6604
|
+
}
|
|
6605
|
+
} catch {
|
|
6606
|
+
}
|
|
6607
|
+
return null;
|
|
6608
|
+
}
|
|
6609
|
+
function writeCache(entry, homeDirOverride) {
|
|
6610
|
+
try {
|
|
6611
|
+
const p = cachePath(homeDirOverride);
|
|
6612
|
+
mkdirSync3(dirname5(p), { recursive: true });
|
|
6613
|
+
writeFileSync3(p, JSON.stringify(entry), "utf8");
|
|
6614
|
+
} catch {
|
|
6615
|
+
}
|
|
6616
|
+
}
|
|
6617
|
+
async function getLatestVersion(opts = {}) {
|
|
6618
|
+
const ttl = opts.ttlMs ?? LATEST_CACHE_TTL_MS;
|
|
6619
|
+
if (!opts.force) {
|
|
6620
|
+
const cached2 = readCache(opts.homeDir);
|
|
6621
|
+
if (cached2 && Date.now() - cached2.checkedAt < ttl) return cached2.version;
|
|
6622
|
+
}
|
|
6623
|
+
const fetchImpl = opts.fetchImpl ?? globalThis.fetch;
|
|
6624
|
+
if (!fetchImpl) return null;
|
|
6625
|
+
const url = opts.registryUrl ?? REGISTRY_URL;
|
|
6626
|
+
const timeout = opts.timeoutMs ?? LATEST_FETCH_TIMEOUT_MS;
|
|
6627
|
+
const controller = new AbortController();
|
|
6628
|
+
const timer = setTimeout(() => controller.abort(), timeout);
|
|
6629
|
+
try {
|
|
6630
|
+
const res = await fetchImpl(url, {
|
|
6631
|
+
signal: controller.signal,
|
|
6632
|
+
headers: { accept: "application/json" }
|
|
6633
|
+
});
|
|
6634
|
+
if (!res.ok) return null;
|
|
6635
|
+
const body = await res.json();
|
|
6636
|
+
if (typeof body.version !== "string") return null;
|
|
6637
|
+
writeCache({ version: body.version, checkedAt: Date.now() }, opts.homeDir);
|
|
6638
|
+
return body.version;
|
|
6639
|
+
} catch {
|
|
6640
|
+
return null;
|
|
6641
|
+
} finally {
|
|
6642
|
+
clearTimeout(timer);
|
|
6643
|
+
}
|
|
6644
|
+
}
|
|
6645
|
+
function compareVersions(a, b) {
|
|
6646
|
+
const [aCore = "0", aPre = ""] = a.split("-", 2);
|
|
6647
|
+
const [bCore = "0", bPre = ""] = b.split("-", 2);
|
|
6648
|
+
const aParts = aCore.split(".").map((p) => Number.parseInt(p, 10) || 0);
|
|
6649
|
+
const bParts = bCore.split(".").map((p) => Number.parseInt(p, 10) || 0);
|
|
6650
|
+
for (let i = 0; i < 3; i++) {
|
|
6651
|
+
const diff = (aParts[i] ?? 0) - (bParts[i] ?? 0);
|
|
6652
|
+
if (diff !== 0) return diff;
|
|
6653
|
+
}
|
|
6654
|
+
if (!aPre && !bPre) return 0;
|
|
6655
|
+
if (!aPre) return 1;
|
|
6656
|
+
if (!bPre) return -1;
|
|
6657
|
+
return aPre < bPre ? -1 : aPre > bPre ? 1 : 0;
|
|
6658
|
+
}
|
|
6659
|
+
function isNpxInstall() {
|
|
6660
|
+
const bin = process.argv[1] ?? "";
|
|
6661
|
+
if (/[/\\]_npx[/\\]/.test(bin)) return true;
|
|
6662
|
+
if (/[/\\]\.pnpm[/\\]/.test(bin) && /dlx/i.test(bin)) return true;
|
|
6663
|
+
const ua = process.env.npm_config_user_agent ?? "";
|
|
6664
|
+
if (ua.includes("npx/")) return true;
|
|
6665
|
+
return false;
|
|
6666
|
+
}
|
|
6667
|
+
|
|
6355
6668
|
// src/mcp/types.ts
|
|
6356
6669
|
var MCP_PROTOCOL_VERSION = "2024-11-05";
|
|
6357
6670
|
function isJsonRpcError(msg) {
|
|
@@ -6380,7 +6693,7 @@ var McpClient = class {
|
|
|
6380
6693
|
nextProgressToken = 1;
|
|
6381
6694
|
constructor(opts) {
|
|
6382
6695
|
this.transport = opts.transport;
|
|
6383
|
-
this.clientInfo = opts.clientInfo ?? { name: "reasonix", version:
|
|
6696
|
+
this.clientInfo = opts.clientInfo ?? { name: "reasonix", version: VERSION };
|
|
6384
6697
|
this.requestTimeoutMs = opts.requestTimeoutMs ?? 6e4;
|
|
6385
6698
|
}
|
|
6386
6699
|
/** Server's advertised capabilities, available after initialize(). */
|
|
@@ -7120,8 +7433,8 @@ async function trySection(load) {
|
|
|
7120
7433
|
}
|
|
7121
7434
|
|
|
7122
7435
|
// src/code/edit-blocks.ts
|
|
7123
|
-
import { existsSync as
|
|
7124
|
-
import { dirname as
|
|
7436
|
+
import { existsSync as existsSync7, mkdirSync as mkdirSync4, readFileSync as readFileSync9, unlinkSync as unlinkSync2, writeFileSync as writeFileSync4 } from "fs";
|
|
7437
|
+
import { dirname as dirname6, resolve as resolve6 } from "path";
|
|
7125
7438
|
var BLOCK_RE = /^(\S[^\n]*)\n<{7} SEARCH\n([\s\S]*?)\n?={7}\n([\s\S]*?)\n?>{7} REPLACE/gm;
|
|
7126
7439
|
function parseEditBlocks(text) {
|
|
7127
7440
|
const out = [];
|
|
@@ -7149,7 +7462,7 @@ function applyEditBlock(block, rootDir) {
|
|
|
7149
7462
|
};
|
|
7150
7463
|
}
|
|
7151
7464
|
const searchEmpty = block.search.length === 0;
|
|
7152
|
-
const exists =
|
|
7465
|
+
const exists = existsSync7(absTarget);
|
|
7153
7466
|
try {
|
|
7154
7467
|
if (!exists) {
|
|
7155
7468
|
if (!searchEmpty) {
|
|
@@ -7159,11 +7472,11 @@ function applyEditBlock(block, rootDir) {
|
|
|
7159
7472
|
message: "file does not exist; to create it, use an empty SEARCH block"
|
|
7160
7473
|
};
|
|
7161
7474
|
}
|
|
7162
|
-
|
|
7163
|
-
|
|
7475
|
+
mkdirSync4(dirname6(absTarget), { recursive: true });
|
|
7476
|
+
writeFileSync4(absTarget, block.replace, "utf8");
|
|
7164
7477
|
return { path: block.path, status: "created" };
|
|
7165
7478
|
}
|
|
7166
|
-
const content =
|
|
7479
|
+
const content = readFileSync9(absTarget, "utf8");
|
|
7167
7480
|
if (searchEmpty) {
|
|
7168
7481
|
return {
|
|
7169
7482
|
path: block.path,
|
|
@@ -7180,7 +7493,7 @@ function applyEditBlock(block, rootDir) {
|
|
|
7180
7493
|
};
|
|
7181
7494
|
}
|
|
7182
7495
|
const replaced = `${content.slice(0, idx)}${block.replace}${content.slice(idx + block.search.length)}`;
|
|
7183
|
-
|
|
7496
|
+
writeFileSync4(absTarget, replaced, "utf8");
|
|
7184
7497
|
return { path: block.path, status: "applied" };
|
|
7185
7498
|
} catch (err) {
|
|
7186
7499
|
return { path: block.path, status: "error", message: err.message };
|
|
@@ -7192,9 +7505,9 @@ function applyEditBlocks(blocks, rootDir) {
|
|
|
7192
7505
|
function toWholeFileEditBlock(path5, content, rootDir) {
|
|
7193
7506
|
const abs = resolve6(rootDir, path5);
|
|
7194
7507
|
let search = "";
|
|
7195
|
-
if (
|
|
7508
|
+
if (existsSync7(abs)) {
|
|
7196
7509
|
try {
|
|
7197
|
-
search =
|
|
7510
|
+
search = readFileSync9(abs, "utf8");
|
|
7198
7511
|
} catch {
|
|
7199
7512
|
search = "";
|
|
7200
7513
|
}
|
|
@@ -7209,12 +7522,12 @@ function snapshotBeforeEdits(blocks, rootDir) {
|
|
|
7209
7522
|
if (seen.has(b.path)) continue;
|
|
7210
7523
|
seen.add(b.path);
|
|
7211
7524
|
const abs = resolve6(absRoot, b.path);
|
|
7212
|
-
if (!
|
|
7525
|
+
if (!existsSync7(abs)) {
|
|
7213
7526
|
snapshots.push({ path: b.path, prevContent: null });
|
|
7214
7527
|
continue;
|
|
7215
7528
|
}
|
|
7216
7529
|
try {
|
|
7217
|
-
snapshots.push({ path: b.path, prevContent:
|
|
7530
|
+
snapshots.push({ path: b.path, prevContent: readFileSync9(abs, "utf8") });
|
|
7218
7531
|
} catch {
|
|
7219
7532
|
snapshots.push({ path: b.path, prevContent: null });
|
|
7220
7533
|
}
|
|
@@ -7234,14 +7547,14 @@ function restoreSnapshots(snapshots, rootDir) {
|
|
|
7234
7547
|
}
|
|
7235
7548
|
try {
|
|
7236
7549
|
if (snap.prevContent === null) {
|
|
7237
|
-
if (
|
|
7550
|
+
if (existsSync7(abs)) unlinkSync2(abs);
|
|
7238
7551
|
return {
|
|
7239
7552
|
path: snap.path,
|
|
7240
7553
|
status: "applied",
|
|
7241
7554
|
message: "removed (the edit had created it)"
|
|
7242
7555
|
};
|
|
7243
7556
|
}
|
|
7244
|
-
|
|
7557
|
+
writeFileSync4(abs, snap.prevContent, "utf8");
|
|
7245
7558
|
return {
|
|
7246
7559
|
path: snap.path,
|
|
7247
7560
|
status: "applied",
|
|
@@ -7256,114 +7569,54 @@ function sep() {
|
|
|
7256
7569
|
return process.platform === "win32" ? "\\" : "/";
|
|
7257
7570
|
}
|
|
7258
7571
|
|
|
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");
|
|
7572
|
+
// src/usage.ts
|
|
7573
|
+
import {
|
|
7574
|
+
appendFileSync as appendFileSync2,
|
|
7575
|
+
existsSync as existsSync8,
|
|
7576
|
+
mkdirSync as mkdirSync5,
|
|
7577
|
+
readFileSync as readFileSync10,
|
|
7578
|
+
statSync as statSync4,
|
|
7579
|
+
writeFileSync as writeFileSync5
|
|
7580
|
+
} from "fs";
|
|
7581
|
+
import { homedir as homedir5 } from "os";
|
|
7582
|
+
import { dirname as dirname7, join as join8 } from "path";
|
|
7583
|
+
function defaultUsageLogPath(homeDirOverride) {
|
|
7584
|
+
return join8(homeDirOverride ?? homedir5(), ".reasonix", "usage.jsonl");
|
|
7289
7585
|
}
|
|
7290
|
-
|
|
7586
|
+
var USAGE_COMPACTION_THRESHOLD_BYTES = 5 * 1024 * 1024;
|
|
7587
|
+
var USAGE_RETENTION_DAYS = 365;
|
|
7588
|
+
function compactUsageLogIfLarge(path5, now) {
|
|
7589
|
+
let size;
|
|
7291
7590
|
try {
|
|
7292
|
-
|
|
7293
|
-
const parsed = JSON.parse(raw);
|
|
7294
|
-
if (parsed && typeof parsed.version === "string" && typeof parsed.checkedAt === "number") {
|
|
7295
|
-
return parsed;
|
|
7296
|
-
}
|
|
7591
|
+
size = statSync4(path5).size;
|
|
7297
7592
|
} catch {
|
|
7593
|
+
return;
|
|
7298
7594
|
}
|
|
7299
|
-
return
|
|
7300
|
-
|
|
7301
|
-
|
|
7595
|
+
if (size < USAGE_COMPACTION_THRESHOLD_BYTES) return;
|
|
7596
|
+
const cutoff = now - USAGE_RETENTION_DAYS * 24 * 60 * 60 * 1e3;
|
|
7597
|
+
let raw;
|
|
7302
7598
|
try {
|
|
7303
|
-
|
|
7304
|
-
mkdirSync4(dirname6(p), { recursive: true });
|
|
7305
|
-
writeFileSync4(p, JSON.stringify(entry), "utf8");
|
|
7599
|
+
raw = readFileSync10(path5, "utf8");
|
|
7306
7600
|
} catch {
|
|
7601
|
+
return;
|
|
7307
7602
|
}
|
|
7308
|
-
|
|
7309
|
-
|
|
7310
|
-
const
|
|
7311
|
-
|
|
7312
|
-
|
|
7313
|
-
|
|
7603
|
+
const lines = raw.split(/\r?\n/);
|
|
7604
|
+
const kept = [];
|
|
7605
|
+
for (const line of lines) {
|
|
7606
|
+
if (!line.trim()) continue;
|
|
7607
|
+
try {
|
|
7608
|
+
const rec = JSON.parse(line);
|
|
7609
|
+
if (isValidRecord(rec) && rec.ts >= cutoff) kept.push(line);
|
|
7610
|
+
} catch {
|
|
7611
|
+
}
|
|
7314
7612
|
}
|
|
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);
|
|
7613
|
+
if (kept.length === lines.filter((l) => l.trim()).length) return;
|
|
7321
7614
|
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;
|
|
7615
|
+
writeFileSync5(path5, kept.length > 0 ? `${kept.join("\n")}
|
|
7616
|
+
` : "", "utf8");
|
|
7331
7617
|
} catch {
|
|
7332
|
-
return null;
|
|
7333
|
-
} finally {
|
|
7334
|
-
clearTimeout(timer);
|
|
7335
7618
|
}
|
|
7336
7619
|
}
|
|
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
7620
|
function appendUsage(input) {
|
|
7368
7621
|
const record = {
|
|
7369
7622
|
ts: input.now ?? Date.now(),
|
|
@@ -7383,6 +7636,7 @@ function appendUsage(input) {
|
|
|
7383
7636
|
mkdirSync5(dirname7(path5), { recursive: true });
|
|
7384
7637
|
appendFileSync2(path5, `${JSON.stringify(record)}
|
|
7385
7638
|
`, "utf8");
|
|
7639
|
+
compactUsageLogIfLarge(path5, record.ts);
|
|
7386
7640
|
} catch {
|
|
7387
7641
|
}
|
|
7388
7642
|
return record;
|
|
@@ -7521,7 +7775,7 @@ import { Box as Box22, Static, Text as Text20, useStdout as useStdout8 } from "i
|
|
|
7521
7775
|
import React24, { useCallback as useCallback4, useEffect as useEffect6, useMemo as useMemo3, useRef as useRef6, useState as useState10 } from "react";
|
|
7522
7776
|
|
|
7523
7777
|
// src/code/pending-edits.ts
|
|
7524
|
-
import { existsSync as existsSync9, mkdirSync as mkdirSync6, readFileSync as readFileSync11, unlinkSync as unlinkSync3, writeFileSync as
|
|
7778
|
+
import { existsSync as existsSync9, mkdirSync as mkdirSync6, readFileSync as readFileSync11, unlinkSync as unlinkSync3, writeFileSync as writeFileSync6 } from "fs";
|
|
7525
7779
|
import { dirname as dirname8, join as join9 } from "path";
|
|
7526
7780
|
function pendingEditsPath(sessionName) {
|
|
7527
7781
|
return join9(sessionsDir(), `${sanitizeName(sessionName)}.pending.json`);
|
|
@@ -7535,7 +7789,7 @@ function savePendingEdits(sessionName, blocks) {
|
|
|
7535
7789
|
return;
|
|
7536
7790
|
}
|
|
7537
7791
|
mkdirSync6(dirname8(path5), { recursive: true });
|
|
7538
|
-
|
|
7792
|
+
writeFileSync6(path5, JSON.stringify(blocks, null, 2), "utf8");
|
|
7539
7793
|
} catch {
|
|
7540
7794
|
}
|
|
7541
7795
|
}
|
|
@@ -7581,7 +7835,7 @@ import {
|
|
|
7581
7835
|
renameSync,
|
|
7582
7836
|
statSync as statSync5,
|
|
7583
7837
|
unlinkSync as unlinkSync4,
|
|
7584
|
-
writeFileSync as
|
|
7838
|
+
writeFileSync as writeFileSync7
|
|
7585
7839
|
} from "fs";
|
|
7586
7840
|
import { dirname as dirname9, join as join10 } from "path";
|
|
7587
7841
|
function planStatePath(sessionName) {
|
|
@@ -7638,7 +7892,7 @@ function savePlanState(sessionName, steps, completedStepIds, extras) {
|
|
|
7638
7892
|
};
|
|
7639
7893
|
if (extras?.body) state.body = extras.body;
|
|
7640
7894
|
if (extras?.summary) state.summary = extras.summary;
|
|
7641
|
-
|
|
7895
|
+
writeFileSync7(path5, `${JSON.stringify(state, null, 2)}
|
|
7642
7896
|
`, "utf8");
|
|
7643
7897
|
} catch (err) {
|
|
7644
7898
|
process.stderr.write(
|
|
@@ -7906,7 +8160,7 @@ import {
|
|
|
7906
8160
|
readFileSync as readFileSync14,
|
|
7907
8161
|
readSync,
|
|
7908
8162
|
statSync as statSync6,
|
|
7909
|
-
writeFileSync as
|
|
8163
|
+
writeFileSync as writeFileSync8
|
|
7910
8164
|
} from "fs";
|
|
7911
8165
|
import { dirname as dirname11, isAbsolute as isAbsolute4, resolve as resolve7, sep as sep2 } from "path";
|
|
7912
8166
|
var MAX_BYTES = 4 * 1024 * 1024;
|
|
@@ -7986,14 +8240,14 @@ async function handleFile(method, rest, body, ctx) {
|
|
|
7986
8240
|
if (!existsSync11(target)) {
|
|
7987
8241
|
return { status: 404, body: { error: "file not found" } };
|
|
7988
8242
|
}
|
|
7989
|
-
const
|
|
7990
|
-
if (
|
|
8243
|
+
const stat2 = statSync6(target);
|
|
8244
|
+
if (stat2.isDirectory()) {
|
|
7991
8245
|
return { status: 400, body: { error: "path is a directory" } };
|
|
7992
8246
|
}
|
|
7993
|
-
if (
|
|
8247
|
+
if (stat2.size > MAX_BYTES) {
|
|
7994
8248
|
return {
|
|
7995
8249
|
status: 413,
|
|
7996
|
-
body: { error: `file too large (${
|
|
8250
|
+
body: { error: `file too large (${stat2.size} bytes; cap ${MAX_BYTES})` }
|
|
7997
8251
|
};
|
|
7998
8252
|
}
|
|
7999
8253
|
if (looksBinary(target)) {
|
|
@@ -8008,8 +8262,8 @@ async function handleFile(method, rest, body, ctx) {
|
|
|
8008
8262
|
body: {
|
|
8009
8263
|
path: requested,
|
|
8010
8264
|
absolute: target,
|
|
8011
|
-
size:
|
|
8012
|
-
mtime:
|
|
8265
|
+
size: stat2.size,
|
|
8266
|
+
mtime: stat2.mtime.getTime(),
|
|
8013
8267
|
content
|
|
8014
8268
|
}
|
|
8015
8269
|
};
|
|
@@ -8029,20 +8283,20 @@ async function handleFile(method, rest, body, ctx) {
|
|
|
8029
8283
|
if (!existsSync11(parent)) {
|
|
8030
8284
|
mkdirSync8(parent, { recursive: true });
|
|
8031
8285
|
}
|
|
8032
|
-
|
|
8286
|
+
writeFileSync8(target, content, "utf8");
|
|
8033
8287
|
ctx.audit?.({
|
|
8034
8288
|
ts: Date.now(),
|
|
8035
8289
|
action: "save-file",
|
|
8036
8290
|
payload: { path: requested, bytes: Buffer.byteLength(content, "utf8") }
|
|
8037
8291
|
});
|
|
8038
|
-
const
|
|
8292
|
+
const stat2 = statSync6(target);
|
|
8039
8293
|
return {
|
|
8040
8294
|
status: 200,
|
|
8041
8295
|
body: {
|
|
8042
8296
|
saved: true,
|
|
8043
8297
|
path: requested,
|
|
8044
|
-
size:
|
|
8045
|
-
mtime:
|
|
8298
|
+
size: stat2.size,
|
|
8299
|
+
mtime: stat2.mtime.getTime()
|
|
8046
8300
|
}
|
|
8047
8301
|
};
|
|
8048
8302
|
}
|
|
@@ -8139,7 +8393,7 @@ async function handleHealth(method, _rest, _body, ctx) {
|
|
|
8139
8393
|
}
|
|
8140
8394
|
|
|
8141
8395
|
// src/server/api/hooks.ts
|
|
8142
|
-
import { existsSync as existsSync13, mkdirSync as mkdirSync9, readFileSync as readFileSync15, writeFileSync as
|
|
8396
|
+
import { existsSync as existsSync13, mkdirSync as mkdirSync9, readFileSync as readFileSync15, writeFileSync as writeFileSync9 } from "fs";
|
|
8143
8397
|
import { dirname as dirname12 } from "path";
|
|
8144
8398
|
function parseBody3(raw) {
|
|
8145
8399
|
if (!raw) return {};
|
|
@@ -8164,7 +8418,7 @@ function writeSettingsFile(path5, hooksBlock) {
|
|
|
8164
8418
|
const existing = readSettingsFile2(path5);
|
|
8165
8419
|
existing.hooks = hooksBlock;
|
|
8166
8420
|
mkdirSync9(dirname12(path5), { recursive: true });
|
|
8167
|
-
|
|
8421
|
+
writeFileSync9(path5, `${JSON.stringify(existing, null, 2)}
|
|
8168
8422
|
`, "utf8");
|
|
8169
8423
|
}
|
|
8170
8424
|
async function handleHooks(method, rest, body, ctx) {
|
|
@@ -8345,7 +8599,7 @@ import {
|
|
|
8345
8599
|
readdirSync as readdirSync5,
|
|
8346
8600
|
statSync as statSync8,
|
|
8347
8601
|
unlinkSync as unlinkSync5,
|
|
8348
|
-
writeFileSync as
|
|
8602
|
+
writeFileSync as writeFileSync10
|
|
8349
8603
|
} from "fs";
|
|
8350
8604
|
import { homedir as homedir7 } from "os";
|
|
8351
8605
|
import { dirname as dirname13, join as join13, resolve as resolvePath } from "path";
|
|
@@ -8372,11 +8626,11 @@ function listMemoryFiles(dir) {
|
|
|
8372
8626
|
if (!existsSync14(dir)) return [];
|
|
8373
8627
|
try {
|
|
8374
8628
|
return readdirSync5(dir).filter((f) => f.endsWith(".md")).map((f) => {
|
|
8375
|
-
const
|
|
8629
|
+
const stat2 = statSync8(join13(dir, f));
|
|
8376
8630
|
return {
|
|
8377
8631
|
name: f.replace(/\.md$/, ""),
|
|
8378
|
-
size:
|
|
8379
|
-
mtime:
|
|
8632
|
+
size: stat2.size,
|
|
8633
|
+
mtime: stat2.mtime.getTime()
|
|
8380
8634
|
};
|
|
8381
8635
|
}).sort((a, b) => b.mtime - a.mtime);
|
|
8382
8636
|
} catch {
|
|
@@ -8436,7 +8690,7 @@ async function handleMemory(method, rest, body, ctx) {
|
|
|
8436
8690
|
if (!cwd2) return { status: 503, body: { error: "no active project" } };
|
|
8437
8691
|
const path5 = join13(cwd2, PROJECT_MEMORY_FILE);
|
|
8438
8692
|
mkdirSync10(dirname13(path5), { recursive: true });
|
|
8439
|
-
|
|
8693
|
+
writeFileSync10(path5, contents, "utf8");
|
|
8440
8694
|
ctx.audit?.({ ts: Date.now(), action: "save-memory", payload: { scope, path: path5 } });
|
|
8441
8695
|
return { status: 200, body: { saved: true, path: path5 } };
|
|
8442
8696
|
}
|
|
@@ -8445,7 +8699,7 @@ async function handleMemory(method, rest, body, ctx) {
|
|
|
8445
8699
|
if (!dir) return { status: 503, body: { error: "no project root for project-mem" } };
|
|
8446
8700
|
mkdirSync10(dir, { recursive: true });
|
|
8447
8701
|
const path5 = join13(dir, `${name}.md`);
|
|
8448
|
-
|
|
8702
|
+
writeFileSync10(path5, contents, "utf8");
|
|
8449
8703
|
ctx.audit?.({ ts: Date.now(), action: "save-memory", payload: { scope, name, path: path5 } });
|
|
8450
8704
|
return { status: 200, body: { saved: true, path: path5 } };
|
|
8451
8705
|
}
|
|
@@ -8783,13 +9037,13 @@ async function* walkChunks(root, opts = {}) {
|
|
|
8783
9037
|
const ext = path.extname(name).toLowerCase();
|
|
8784
9038
|
if (BINARY_EXTS.has(ext)) continue;
|
|
8785
9039
|
const abs = path.join(dir, name);
|
|
8786
|
-
let
|
|
9040
|
+
let stat2;
|
|
8787
9041
|
try {
|
|
8788
|
-
|
|
9042
|
+
stat2 = await fs2.stat(abs);
|
|
8789
9043
|
} catch {
|
|
8790
9044
|
continue;
|
|
8791
9045
|
}
|
|
8792
|
-
if (
|
|
9046
|
+
if (stat2.size > maxFileBytes) continue;
|
|
8793
9047
|
let text;
|
|
8794
9048
|
try {
|
|
8795
9049
|
text = await fs2.readFile(abs, "utf8");
|
|
@@ -9165,8 +9419,8 @@ async function buildIndex(root, opts = {}) {
|
|
|
9165
9419
|
const abs = path3.join(root, chunk.path);
|
|
9166
9420
|
let mtimeMs = 0;
|
|
9167
9421
|
try {
|
|
9168
|
-
const
|
|
9169
|
-
mtimeMs =
|
|
9422
|
+
const stat2 = await fs4.stat(abs);
|
|
9423
|
+
mtimeMs = stat2.mtimeMs;
|
|
9170
9424
|
} catch {
|
|
9171
9425
|
continue;
|
|
9172
9426
|
}
|
|
@@ -9969,7 +10223,7 @@ import {
|
|
|
9969
10223
|
readdirSync as readdirSync6,
|
|
9970
10224
|
rmSync,
|
|
9971
10225
|
statSync as statSync9,
|
|
9972
|
-
writeFileSync as
|
|
10226
|
+
writeFileSync as writeFileSync11
|
|
9973
10227
|
} from "fs";
|
|
9974
10228
|
import { homedir as homedir8 } from "os";
|
|
9975
10229
|
import { dirname as dirname14, join as join14 } from "path";
|
|
@@ -10008,14 +10262,14 @@ function listSkills(dir, scope) {
|
|
|
10008
10262
|
const skillPath = join14(dir, entry, SKILL_FILE);
|
|
10009
10263
|
if (!existsSync16(skillPath)) continue;
|
|
10010
10264
|
try {
|
|
10011
|
-
const
|
|
10265
|
+
const stat2 = statSync9(skillPath);
|
|
10012
10266
|
const raw = readFileSync18(skillPath, "utf8");
|
|
10013
10267
|
const item = {
|
|
10014
10268
|
name: entry,
|
|
10015
10269
|
scope,
|
|
10016
10270
|
path: skillPath,
|
|
10017
|
-
size:
|
|
10018
|
-
mtime:
|
|
10271
|
+
size: stat2.size,
|
|
10272
|
+
mtime: stat2.mtime.getTime()
|
|
10019
10273
|
};
|
|
10020
10274
|
const desc = parseFrontmatterDescription(raw);
|
|
10021
10275
|
if (desc) item.description = desc;
|
|
@@ -10084,7 +10338,7 @@ async function handleSkills(method, rest, body, ctx) {
|
|
|
10084
10338
|
return { status: 400, body: { error: "body (string) required" } };
|
|
10085
10339
|
}
|
|
10086
10340
|
mkdirSync11(dirname14(skillPath), { recursive: true });
|
|
10087
|
-
|
|
10341
|
+
writeFileSync11(skillPath, contents, "utf8");
|
|
10088
10342
|
ctx.audit?.({
|
|
10089
10343
|
ts: Date.now(),
|
|
10090
10344
|
action: "save-skill",
|
|
@@ -11191,13 +11445,13 @@ var MIN_DIFF_ROWS = 8;
|
|
|
11191
11445
|
function EditConfirm({ block, onChoose }) {
|
|
11192
11446
|
const { stdout: stdout3 } = useStdout2();
|
|
11193
11447
|
const rows = stdout3?.rows ?? 40;
|
|
11194
|
-
const
|
|
11448
|
+
const budget2 = Math.max(MIN_DIFF_ROWS, rows - MODAL_OVERHEAD_ROWS);
|
|
11195
11449
|
const allLines = useMemo(
|
|
11196
11450
|
() => formatEditBlockDiff(block, { contextLines: 2, maxLines: 1e5, indent: " " }),
|
|
11197
11451
|
[block]
|
|
11198
11452
|
);
|
|
11199
11453
|
const [scroll, setScroll] = useState2(0);
|
|
11200
|
-
const maxScroll = Math.max(0, allLines.length -
|
|
11454
|
+
const maxScroll = Math.max(0, allLines.length - budget2);
|
|
11201
11455
|
const effectiveScroll = Math.min(scroll, maxScroll);
|
|
11202
11456
|
useKeystroke((ev) => {
|
|
11203
11457
|
if (ev.paste) return;
|
|
@@ -11228,11 +11482,11 @@ function EditConfirm({ block, onChoose }) {
|
|
|
11228
11482
|
return;
|
|
11229
11483
|
}
|
|
11230
11484
|
if (key.pageDown || input === " " || input === "f") {
|
|
11231
|
-
setScroll((s) => Math.min(maxScroll, s + Math.max(1,
|
|
11485
|
+
setScroll((s) => Math.min(maxScroll, s + Math.max(1, budget2 - 2)));
|
|
11232
11486
|
return;
|
|
11233
11487
|
}
|
|
11234
11488
|
if (key.pageUp || input === "b") {
|
|
11235
|
-
setScroll((s) => Math.max(0, s - Math.max(1,
|
|
11489
|
+
setScroll((s) => Math.max(0, s - Math.max(1, budget2 - 2)));
|
|
11236
11490
|
return;
|
|
11237
11491
|
}
|
|
11238
11492
|
if (input === "g") {
|
|
@@ -11248,9 +11502,9 @@ function EditConfirm({ block, onChoose }) {
|
|
|
11248
11502
|
const removed = isNew ? 0 : (block.search.match(/\n/g)?.length ?? 0) + 1;
|
|
11249
11503
|
const added = block.replace === "" ? 0 : (block.replace.match(/\n/g)?.length ?? 0) + 1;
|
|
11250
11504
|
const tag = isNew ? "NEW" : "EDIT";
|
|
11251
|
-
const visibleLines = allLines.slice(effectiveScroll, effectiveScroll +
|
|
11505
|
+
const visibleLines = allLines.slice(effectiveScroll, effectiveScroll + budget2);
|
|
11252
11506
|
const hiddenAbove = effectiveScroll;
|
|
11253
|
-
const hiddenBelow = Math.max(0, allLines.length - effectiveScroll -
|
|
11507
|
+
const hiddenBelow = Math.max(0, allLines.length - effectiveScroll - budget2);
|
|
11254
11508
|
const totalLines = allLines.length;
|
|
11255
11509
|
const showScrollHud = hiddenAbove + hiddenBelow > 0;
|
|
11256
11510
|
const subtitleParts = [`-${removed} +${added} lines`];
|
|
@@ -11640,26 +11894,26 @@ function validateCitation(url, projectRoot) {
|
|
|
11640
11894
|
...siblings.map((ext) => baseFullPath.replace(/\.[^./\\]+$/, ext))
|
|
11641
11895
|
];
|
|
11642
11896
|
let fullPath = baseFullPath;
|
|
11643
|
-
let
|
|
11897
|
+
let stat2 = null;
|
|
11644
11898
|
for (const candidate of candidates) {
|
|
11645
11899
|
try {
|
|
11646
|
-
|
|
11900
|
+
stat2 = statSync11(candidate);
|
|
11647
11901
|
fullPath = candidate;
|
|
11648
11902
|
break;
|
|
11649
11903
|
} catch {
|
|
11650
11904
|
}
|
|
11651
11905
|
}
|
|
11652
|
-
if (!
|
|
11653
|
-
if (!
|
|
11906
|
+
if (!stat2) return { ok: false, reason: "file not found" };
|
|
11907
|
+
if (!stat2.isFile()) return { ok: false, reason: "not a file" };
|
|
11654
11908
|
if (parts.startLine === void 0) return { ok: true };
|
|
11655
|
-
let lineCount = getCachedLineCount(fullPath,
|
|
11909
|
+
let lineCount = getCachedLineCount(fullPath, stat2.mtimeMs);
|
|
11656
11910
|
if (lineCount === null) {
|
|
11657
11911
|
try {
|
|
11658
11912
|
lineCount = readFileSync19(fullPath, "utf8").split("\n").length;
|
|
11659
11913
|
} catch {
|
|
11660
11914
|
return { ok: false, reason: "unreadable" };
|
|
11661
11915
|
}
|
|
11662
|
-
setCachedLineCount(fullPath,
|
|
11916
|
+
setCachedLineCount(fullPath, stat2.mtimeMs, lineCount);
|
|
11663
11917
|
}
|
|
11664
11918
|
if (parts.startLine < 1 || parts.startLine > lineCount) {
|
|
11665
11919
|
return { ok: false, reason: `line ${parts.startLine} > ${lineCount}` };
|
|
@@ -12454,7 +12708,7 @@ var EventRow = React11.memo(function EventRow2({
|
|
|
12454
12708
|
event.branch ? /* @__PURE__ */ React11.createElement(BranchBlock, { branch: event.branch }) : null,
|
|
12455
12709
|
event.reasoning ? /* @__PURE__ */ React11.createElement(ReasoningBlock, { reasoning: event.reasoning }) : null,
|
|
12456
12710
|
!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 }, "(
|
|
12711
|
+
event.text ? /* @__PURE__ */ React11.createElement(Markdown, { text: event.text, projectRoot }) : /* @__PURE__ */ React11.createElement(Text8, { dimColor: true }, "(empty body \u2014 likely tool-call only)"),
|
|
12458
12712
|
event.stats ? /* @__PURE__ */ React11.createElement(StatsLine, { stats: event.stats }) : null,
|
|
12459
12713
|
event.repair ? /* @__PURE__ */ React11.createElement(Text8, { color: COLOR.accent }, event.repair) : null
|
|
12460
12714
|
));
|
|
@@ -13337,13 +13591,13 @@ function buildViewport(line, cursorCol, visibleCells, pastes) {
|
|
|
13337
13591
|
return clipAroundCursor(line, cursorCol, visibleCells, pastes);
|
|
13338
13592
|
}
|
|
13339
13593
|
function clipFromLeft(line, visibleCells, pastes) {
|
|
13340
|
-
const
|
|
13594
|
+
const budget2 = Math.max(1, visibleCells - 1);
|
|
13341
13595
|
let used = 0;
|
|
13342
13596
|
let end = 0;
|
|
13343
13597
|
while (end < line.length) {
|
|
13344
13598
|
const ch = line[end];
|
|
13345
13599
|
const cw = charCellsAt(line, end, pastes);
|
|
13346
|
-
if (used + cw >
|
|
13600
|
+
if (used + cw > budget2) break;
|
|
13347
13601
|
used += cw;
|
|
13348
13602
|
end++;
|
|
13349
13603
|
}
|
|
@@ -13351,10 +13605,10 @@ function clipFromLeft(line, visibleCells, pastes) {
|
|
|
13351
13605
|
return { segments, cursorCell: null, hiddenLeft: false, hiddenRight: end < line.length };
|
|
13352
13606
|
}
|
|
13353
13607
|
function clipAroundCursor(line, cursorCol, visibleCells, pastes) {
|
|
13354
|
-
let
|
|
13608
|
+
let budget2 = visibleCells;
|
|
13355
13609
|
const reservedForMarkers = 2;
|
|
13356
|
-
|
|
13357
|
-
const halfBudget = Math.floor(
|
|
13610
|
+
budget2 = Math.max(1, budget2 - reservedForMarkers);
|
|
13611
|
+
const halfBudget = Math.floor(budget2 / 2);
|
|
13358
13612
|
let start = cursorCol;
|
|
13359
13613
|
let leftCells = 0;
|
|
13360
13614
|
while (start > 0 && leftCells < halfBudget) {
|
|
@@ -13363,7 +13617,7 @@ function clipAroundCursor(line, cursorCol, visibleCells, pastes) {
|
|
|
13363
13617
|
start--;
|
|
13364
13618
|
leftCells += cw;
|
|
13365
13619
|
}
|
|
13366
|
-
const rightBudget =
|
|
13620
|
+
const rightBudget = budget2 - leftCells;
|
|
13367
13621
|
let end = cursorCol;
|
|
13368
13622
|
let rightCells = 0;
|
|
13369
13623
|
const cursorChar = cursorCol < line.length ? charCellsAt(line, cursorCol, pastes) : 1;
|
|
@@ -13870,7 +14124,8 @@ function StatsPanel({
|
|
|
13870
14124
|
busy,
|
|
13871
14125
|
proArmed,
|
|
13872
14126
|
escalated,
|
|
13873
|
-
dashboardUrl
|
|
14127
|
+
dashboardUrl,
|
|
14128
|
+
budgetUsd
|
|
13874
14129
|
}) {
|
|
13875
14130
|
const branchOn = (branchBudget ?? 1) > 1;
|
|
13876
14131
|
const ctxMax = DEEPSEEK_CONTEXT_TOKENS[model2] ?? DEFAULT_CONTEXT_TOKENS;
|
|
@@ -13916,7 +14171,12 @@ function StatsPanel({
|
|
|
13916
14171
|
balance,
|
|
13917
14172
|
coldStart
|
|
13918
14173
|
}
|
|
13919
|
-
));
|
|
14174
|
+
), budgetUsd !== null && budgetUsd !== void 0 ? /* @__PURE__ */ React21.createElement(BudgetRow, { spent: summary.totalCostUsd, cap: budgetUsd }) : null);
|
|
14175
|
+
}
|
|
14176
|
+
function BudgetRow({ spent, cap }) {
|
|
14177
|
+
const pct2 = Math.max(0, spent / cap * 100);
|
|
14178
|
+
const color = pct2 >= 100 ? "#f87171" : pct2 >= 80 ? "#fbbf24" : "#94a3b8";
|
|
14179
|
+
return /* @__PURE__ */ React21.createElement(Box19, null, /* @__PURE__ */ React21.createElement(Text17, { dimColor: true }, " budget "), /* @__PURE__ */ React21.createElement(Text17, { color }, `$${spent.toFixed(4)} / $${cap.toFixed(2)}`, /* @__PURE__ */ React21.createElement(Text17, { dimColor: true }, ` (${pct2.toFixed(0)}%)`)));
|
|
13920
14180
|
}
|
|
13921
14181
|
function Header({
|
|
13922
14182
|
model: model2,
|
|
@@ -14195,7 +14455,7 @@ function describeRepair(repair) {
|
|
|
14195
14455
|
}
|
|
14196
14456
|
|
|
14197
14457
|
// src/cli/ui/hash-memory.ts
|
|
14198
|
-
import { appendFileSync as appendFileSync3, existsSync as existsSync18, mkdirSync as mkdirSync12, readFileSync as readFileSync20, writeFileSync as
|
|
14458
|
+
import { appendFileSync as appendFileSync3, existsSync as existsSync18, mkdirSync as mkdirSync12, readFileSync as readFileSync20, writeFileSync as writeFileSync12 } from "fs";
|
|
14199
14459
|
import { homedir as homedir9 } from "os";
|
|
14200
14460
|
import { dirname as dirname15, join as join17 } from "path";
|
|
14201
14461
|
var PROJECT_HEADER = `# Reasonix project memory
|
|
@@ -14246,7 +14506,7 @@ function appendBulletToFile(path5, note, newFileHeader) {
|
|
|
14246
14506
|
`;
|
|
14247
14507
|
if (!existsSync18(path5)) {
|
|
14248
14508
|
mkdirSync12(dirname15(path5), { recursive: true });
|
|
14249
|
-
|
|
14509
|
+
writeFileSync12(path5, `${newFileHeader}${bullet}`, "utf8");
|
|
14250
14510
|
return { path: path5, created: true };
|
|
14251
14511
|
}
|
|
14252
14512
|
let prefix = "";
|
|
@@ -14618,6 +14878,12 @@ var SLASH_COMMANDS = [
|
|
|
14618
14878
|
summary: "arm v4-pro for the NEXT turn only (one-shot \xB7 auto-disarms after turn)",
|
|
14619
14879
|
argCompleter: ["off"]
|
|
14620
14880
|
},
|
|
14881
|
+
{
|
|
14882
|
+
cmd: "budget",
|
|
14883
|
+
argsHint: "[usd|off]",
|
|
14884
|
+
summary: "session USD cap \u2014 warns at 80%, refuses next turn at 100%. Off by default. /budget alone shows status",
|
|
14885
|
+
argCompleter: ["off", "1", "5", "10", "20", "50"]
|
|
14886
|
+
},
|
|
14621
14887
|
{ cmd: "mcp", summary: "list MCP servers + tools attached to this session" },
|
|
14622
14888
|
{
|
|
14623
14889
|
cmd: "resource",
|
|
@@ -14692,6 +14958,11 @@ var SLASH_COMMANDS = [
|
|
|
14692
14958
|
},
|
|
14693
14959
|
{ cmd: "sessions", summary: "list saved sessions (current marked with \u25B8)" },
|
|
14694
14960
|
{ cmd: "forget", summary: "delete the current session from disk" },
|
|
14961
|
+
{
|
|
14962
|
+
cmd: "prune-sessions",
|
|
14963
|
+
summary: "delete sessions idle \u2265N days (default 90) \u2014 frees disk on long-time installs",
|
|
14964
|
+
argsHint: "[days]"
|
|
14965
|
+
},
|
|
14695
14966
|
{ cmd: "setup", summary: "reminds you to exit and run `reasonix setup`" },
|
|
14696
14967
|
{
|
|
14697
14968
|
cmd: "semantic",
|
|
@@ -16102,6 +16373,42 @@ var pro = (args, loop2, ctx) => {
|
|
|
16102
16373
|
};
|
|
16103
16374
|
};
|
|
16104
16375
|
var ESCALATION_MODEL_ID = "deepseek-v4-pro";
|
|
16376
|
+
var budget = (args, loop2) => {
|
|
16377
|
+
const arg = args[0]?.trim() ?? "";
|
|
16378
|
+
if (arg === "") {
|
|
16379
|
+
if (loop2.budgetUsd === null) {
|
|
16380
|
+
return {
|
|
16381
|
+
info: "no session budget set \u2014 Reasonix will keep going until you stop it. Set one with: /budget <usd> (e.g. /budget 5)"
|
|
16382
|
+
};
|
|
16383
|
+
}
|
|
16384
|
+
const spent2 = loop2.stats.totalCost;
|
|
16385
|
+
const pct2 = spent2 / loop2.budgetUsd * 100;
|
|
16386
|
+
return {
|
|
16387
|
+
info: `budget: $${spent2.toFixed(4)} of $${loop2.budgetUsd.toFixed(2)} (${pct2.toFixed(1)}%) \xB7 /budget off to clear, /budget <usd> to change`
|
|
16388
|
+
};
|
|
16389
|
+
}
|
|
16390
|
+
if (arg === "off" || arg === "none" || arg === "0") {
|
|
16391
|
+
loop2.setBudget(null);
|
|
16392
|
+
return { info: "budget \u2192 off (no cap)" };
|
|
16393
|
+
}
|
|
16394
|
+
const cleaned = arg.replace(/^\$/, "");
|
|
16395
|
+
const usd = Number(cleaned);
|
|
16396
|
+
if (!Number.isFinite(usd) || usd <= 0) {
|
|
16397
|
+
return {
|
|
16398
|
+
info: `usage: /budget <usd> (got "${arg}" \u2014 must be a positive number, e.g. /budget 5 or /budget 12.50)`
|
|
16399
|
+
};
|
|
16400
|
+
}
|
|
16401
|
+
loop2.setBudget(usd);
|
|
16402
|
+
const spent = loop2.stats.totalCost;
|
|
16403
|
+
if (spent >= usd) {
|
|
16404
|
+
return {
|
|
16405
|
+
info: `\u25B2 budget \u2192 $${usd.toFixed(2)} but already spent $${spent.toFixed(4)}. Next turn will be refused \u2014 bump the cap higher to keep going, or end the session.`
|
|
16406
|
+
};
|
|
16407
|
+
}
|
|
16408
|
+
return {
|
|
16409
|
+
info: `budget \u2192 $${usd.toFixed(2)} (so far: $${spent.toFixed(4)} \xB7 warns at 80%, refuses next turn at 100% \xB7 /budget off to clear)`
|
|
16410
|
+
};
|
|
16411
|
+
};
|
|
16105
16412
|
var handlers9 = {
|
|
16106
16413
|
model,
|
|
16107
16414
|
models,
|
|
@@ -16109,7 +16416,8 @@ var handlers9 = {
|
|
|
16109
16416
|
preset,
|
|
16110
16417
|
branch,
|
|
16111
16418
|
effort,
|
|
16112
|
-
pro
|
|
16419
|
+
pro,
|
|
16420
|
+
budget
|
|
16113
16421
|
};
|
|
16114
16422
|
|
|
16115
16423
|
// src/cli/ui/slash/handlers/observability.ts
|
|
@@ -16661,9 +16969,9 @@ async function indexFileExists(rootDir) {
|
|
|
16661
16969
|
async function readIndexMeta(rootDir) {
|
|
16662
16970
|
const dataPath = path4.join(rootDir, ".reasonix", "semantic", "index.jsonl");
|
|
16663
16971
|
try {
|
|
16664
|
-
const
|
|
16665
|
-
if (
|
|
16666
|
-
return { chunks: Math.round(
|
|
16972
|
+
const stat2 = await fs5.stat(dataPath);
|
|
16973
|
+
if (stat2.size > 10 * 1024 * 1024) {
|
|
16974
|
+
return { chunks: Math.round(stat2.size / 500), files: 0 };
|
|
16667
16975
|
}
|
|
16668
16976
|
const raw = await fs5.readFile(dataPath, "utf8");
|
|
16669
16977
|
const seenPaths = /* @__PURE__ */ new Set();
|
|
@@ -16687,6 +16995,7 @@ var handlers13 = {
|
|
|
16687
16995
|
};
|
|
16688
16996
|
|
|
16689
16997
|
// src/cli/ui/slash/handlers/sessions.ts
|
|
16998
|
+
var STALE_THRESHOLD_DAYS = 90;
|
|
16690
16999
|
var sessions = (_args, loop2) => {
|
|
16691
17000
|
const items = listSessions();
|
|
16692
17001
|
if (items.length === 0) {
|
|
@@ -16694,17 +17003,28 @@ var sessions = (_args, loop2) => {
|
|
|
16694
17003
|
info: "no saved sessions yet \u2014 chat normally and your messages will be saved automatically"
|
|
16695
17004
|
};
|
|
16696
17005
|
}
|
|
17006
|
+
const now = Date.now();
|
|
16697
17007
|
const lines = ["Saved sessions:"];
|
|
17008
|
+
let staleCount = 0;
|
|
16698
17009
|
for (const s of items) {
|
|
16699
17010
|
const sizeKb = (s.size / 1024).toFixed(1);
|
|
16700
17011
|
const when = s.mtime.toISOString().replace("T", " ").slice(0, 16);
|
|
16701
17012
|
const marker = s.name === loop2.sessionName ? "\u25B8" : " ";
|
|
17013
|
+
const ageDays = Math.floor((now - s.mtime.getTime()) / (24 * 60 * 60 * 1e3));
|
|
17014
|
+
const isStale = ageDays >= STALE_THRESHOLD_DAYS;
|
|
17015
|
+
const ageTag = isStale ? ` (${ageDays}d \u2014 stale)` : "";
|
|
17016
|
+
if (isStale) staleCount++;
|
|
16702
17017
|
lines.push(
|
|
16703
|
-
` ${marker} ${s.name.padEnd(22)} ${String(s.messageCount).padStart(5)} msgs ${sizeKb.padStart(7)} KB ${when}`
|
|
17018
|
+
` ${marker} ${s.name.padEnd(22)} ${String(s.messageCount).padStart(5)} msgs ${sizeKb.padStart(7)} KB ${when}${ageTag}`
|
|
16704
17019
|
);
|
|
16705
17020
|
}
|
|
16706
17021
|
lines.push("");
|
|
16707
17022
|
lines.push("Resume with: reasonix chat --session <name>");
|
|
17023
|
+
if (staleCount > 0) {
|
|
17024
|
+
lines.push(
|
|
17025
|
+
`${staleCount} session${staleCount === 1 ? "" : "s"} idle \u2265${STALE_THRESHOLD_DAYS} days \u2014 /prune-sessions to remove`
|
|
17026
|
+
);
|
|
17027
|
+
}
|
|
16708
17028
|
return { info: lines.join("\n") };
|
|
16709
17029
|
};
|
|
16710
17030
|
var forget = (_args, loop2) => {
|
|
@@ -16717,9 +17037,26 @@ var forget = (_args, loop2) => {
|
|
|
16717
17037
|
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
17038
|
};
|
|
16719
17039
|
};
|
|
17040
|
+
var pruneSessions = (args) => {
|
|
17041
|
+
const raw = args?.[0];
|
|
17042
|
+
const days = raw ? Number.parseInt(raw, 10) : STALE_THRESHOLD_DAYS;
|
|
17043
|
+
if (!Number.isFinite(days) || days < 1) {
|
|
17044
|
+
return {
|
|
17045
|
+
info: `\u25B8 usage: /prune-sessions [days] \u2014 defaults to ${STALE_THRESHOLD_DAYS}, must be \u22651`
|
|
17046
|
+
};
|
|
17047
|
+
}
|
|
17048
|
+
const removed = pruneStaleSessions(days);
|
|
17049
|
+
if (removed.length === 0) {
|
|
17050
|
+
return { info: `\u25B8 nothing to prune \u2014 no sessions idle \u2265${days} days` };
|
|
17051
|
+
}
|
|
17052
|
+
return {
|
|
17053
|
+
info: `\u25B8 pruned ${removed.length} session${removed.length === 1 ? "" : "s"} idle \u2265${days} days: ${removed.join(", ")}`
|
|
17054
|
+
};
|
|
17055
|
+
};
|
|
16720
17056
|
var handlers14 = {
|
|
16721
17057
|
sessions,
|
|
16722
|
-
forget
|
|
17058
|
+
forget,
|
|
17059
|
+
"prune-sessions": pruneSessions
|
|
16723
17060
|
};
|
|
16724
17061
|
|
|
16725
17062
|
// src/cli/ui/slash/handlers/skill.ts
|
|
@@ -16845,13 +17182,21 @@ function useCompletionPickers({
|
|
|
16845
17182
|
});
|
|
16846
17183
|
}, [slashMatches]);
|
|
16847
17184
|
const [atSelected, setAtSelected] = useState6(0);
|
|
16848
|
-
const atFiles =
|
|
16849
|
-
|
|
16850
|
-
|
|
16851
|
-
|
|
16852
|
-
|
|
16853
|
-
return [];
|
|
17185
|
+
const [atFiles, setAtFiles] = useState6([]);
|
|
17186
|
+
useEffect3(() => {
|
|
17187
|
+
if (!codeMode) {
|
|
17188
|
+
setAtFiles([]);
|
|
17189
|
+
return;
|
|
16854
17190
|
}
|
|
17191
|
+
let cancelled = false;
|
|
17192
|
+
listFilesWithStatsAsync(rootDir, { maxResults: 500 }).then((files) => {
|
|
17193
|
+
if (!cancelled) setAtFiles(files);
|
|
17194
|
+
}).catch(() => {
|
|
17195
|
+
if (!cancelled) setAtFiles([]);
|
|
17196
|
+
});
|
|
17197
|
+
return () => {
|
|
17198
|
+
cancelled = true;
|
|
17199
|
+
};
|
|
16855
17200
|
}, [codeMode, rootDir]);
|
|
16856
17201
|
const recentFilesRef = useRef3([]);
|
|
16857
17202
|
const recordRecentFile = useCallback((p) => {
|
|
@@ -17325,6 +17670,7 @@ function App({
|
|
|
17325
17670
|
transcript,
|
|
17326
17671
|
harvest: harvest3,
|
|
17327
17672
|
branch: branch2,
|
|
17673
|
+
budgetUsd,
|
|
17328
17674
|
session,
|
|
17329
17675
|
tools,
|
|
17330
17676
|
mcpSpecs,
|
|
@@ -17538,6 +17884,7 @@ function App({
|
|
|
17538
17884
|
model: model2,
|
|
17539
17885
|
harvest: harvest3,
|
|
17540
17886
|
branch: branch2,
|
|
17887
|
+
budgetUsd,
|
|
17541
17888
|
session,
|
|
17542
17889
|
hooks: hookList,
|
|
17543
17890
|
hookCwd: currentRootDir,
|
|
@@ -17548,7 +17895,7 @@ function App({
|
|
|
17548
17895
|
});
|
|
17549
17896
|
loopRef.current = l;
|
|
17550
17897
|
return l;
|
|
17551
|
-
}, [model2, system, harvest3, branch2, session, tools, codeMode]);
|
|
17898
|
+
}, [model2, system, harvest3, branch2, budgetUsd, session, tools, codeMode]);
|
|
17552
17899
|
useEffect6(() => {
|
|
17553
17900
|
loop2.hooks = hookList;
|
|
17554
17901
|
}, [loop2, hookList]);
|
|
@@ -19793,7 +20140,8 @@ Continue executing from the next pending step. Call mark_step_complete after eac
|
|
|
19793
20140
|
updateAvailable,
|
|
19794
20141
|
proArmed,
|
|
19795
20142
|
escalated: turnOnPro,
|
|
19796
|
-
dashboardUrl
|
|
20143
|
+
dashboardUrl,
|
|
20144
|
+
budgetUsd: loop2.budgetUsd
|
|
19797
20145
|
}
|
|
19798
20146
|
), /* @__PURE__ */ React24.createElement(Static, { items: historical }, (item) => /* @__PURE__ */ React24.createElement(EventRow, { key: item.id, event: item, projectRoot: currentRootDir })), !historical.some((e) => e.role === "user" || e.role === "assistant") && !busy && !streaming ? /* @__PURE__ */ React24.createElement(WelcomeBanner, { inCodeMode: !!codeMode }) : null, !PLAIN_UI && !pendingShell && !pendingWorkspace && !pendingPlan && !stagedInput && !pendingEditReview && !pendingCheckpoint && !stagedCheckpointRevise && streaming ? /* @__PURE__ */ React24.createElement(Box22, { marginY: 1 }, /* @__PURE__ */ React24.createElement(EventRow, { event: streaming, projectRoot: currentRootDir })) : null, !PLAIN_UI && !pendingShell && !pendingWorkspace && !pendingPlan && !stagedInput && !pendingEditReview && !pendingCheckpoint && !stagedCheckpointRevise && ongoingTool ? /* @__PURE__ */ React24.createElement(OngoingToolRow, { tool: ongoingTool, progress: toolProgress }) : null, !PLAIN_UI && !pendingShell && !pendingWorkspace && !pendingPlan && !stagedInput && !pendingEditReview && !pendingCheckpoint && !stagedCheckpointRevise && subagentActivity ? /* @__PURE__ */ React24.createElement(SubagentRow, { activity: subagentActivity }) : null, !PLAIN_UI && !pendingShell && !pendingWorkspace && !pendingPlan && !stagedInput && !pendingEditReview && !pendingCheckpoint && !stagedCheckpointRevise && !ongoingTool && statusLine ? /* @__PURE__ */ React24.createElement(StatusRow, { text: statusLine }) : null, !PLAIN_UI && undoBanner && !pendingShell && !pendingWorkspace && !pendingPlan && !stagedInput && !pendingEditReview && !pendingCheckpoint && !stagedCheckpointRevise && !pendingChoice && !stagedChoiceCustom && !pendingRevision ? /* @__PURE__ */ React24.createElement(UndoBanner, { banner: undoBanner }) : null, !PLAIN_UI && !pendingShell && !pendingWorkspace && !pendingPlan && !stagedInput && !pendingEditReview && !pendingCheckpoint && !stagedCheckpointRevise && busy && !streaming && !ongoingTool && !statusLine ? /* @__PURE__ */ React24.createElement(StatusRow, { text: "processing\u2026" }) : null, stagedInput ? /* @__PURE__ */ React24.createElement(
|
|
19799
20147
|
PlanRefineInput,
|
|
@@ -20064,6 +20412,7 @@ function Root({
|
|
|
20064
20412
|
transcript: appProps.transcript,
|
|
20065
20413
|
harvest: appProps.harvest,
|
|
20066
20414
|
branch: appProps.branch,
|
|
20415
|
+
budgetUsd: appProps.budgetUsd,
|
|
20067
20416
|
session: appProps.session,
|
|
20068
20417
|
tools,
|
|
20069
20418
|
mcpSpecs,
|
|
@@ -20225,6 +20574,7 @@ async function codeCommand(opts = {}) {
|
|
|
20225
20574
|
await chatCommand({
|
|
20226
20575
|
model: opts.model ?? "deepseek-v4-flash",
|
|
20227
20576
|
harvest: opts.harvest ?? false,
|
|
20577
|
+
budgetUsd: opts.budgetUsd,
|
|
20228
20578
|
system: codeSystemPrompt2(rootDir, { hasSemanticSearch: semantic2.enabled }),
|
|
20229
20579
|
transcript: opts.transcript,
|
|
20230
20580
|
session,
|
|
@@ -20237,7 +20587,7 @@ async function codeCommand(opts = {}) {
|
|
|
20237
20587
|
}
|
|
20238
20588
|
|
|
20239
20589
|
// src/cli/commands/diff.ts
|
|
20240
|
-
import { writeFileSync as
|
|
20590
|
+
import { writeFileSync as writeFileSync13 } from "fs";
|
|
20241
20591
|
import { basename as basename3 } from "path";
|
|
20242
20592
|
import { render as render2 } from "ink";
|
|
20243
20593
|
import React30 from "react";
|
|
@@ -20384,7 +20734,7 @@ async function diffCommand(opts) {
|
|
|
20384
20734
|
if (wantMarkdown) {
|
|
20385
20735
|
console.log(renderSummaryTable(report));
|
|
20386
20736
|
const md = renderMarkdown(report);
|
|
20387
|
-
|
|
20737
|
+
writeFileSync13(opts.mdPath, md, "utf8");
|
|
20388
20738
|
console.log(`
|
|
20389
20739
|
markdown report written to ${opts.mdPath}`);
|
|
20390
20740
|
return;
|
|
@@ -20961,7 +21311,8 @@ async function runCommand2(opts) {
|
|
|
20961
21311
|
tools,
|
|
20962
21312
|
model: opts.model,
|
|
20963
21313
|
harvest: opts.harvest,
|
|
20964
|
-
branch: opts.branch
|
|
21314
|
+
branch: opts.branch,
|
|
21315
|
+
budgetUsd: opts.budgetUsd
|
|
20965
21316
|
});
|
|
20966
21317
|
const prefixHash = prefix.fingerprint;
|
|
20967
21318
|
let transcriptStream = null;
|
|
@@ -21517,6 +21868,17 @@ Your training data has a cutoff. When an answer's correctness depends on somethi
|
|
|
21517
21868
|
The signal isn't a topic list \u2014 it's: "if I'm wrong about this, is it because reality moved on?". If yes, ground the answer in fresh evidence; if no (definitions, mechanisms, well-established APIs), answer from memory.
|
|
21518
21869
|
|
|
21519
21870
|
${ESCALATION_CONTRACT}`;
|
|
21871
|
+
function parseBudgetFlag(raw) {
|
|
21872
|
+
if (raw === void 0) return void 0;
|
|
21873
|
+
if (!Number.isFinite(raw) || raw <= 0) {
|
|
21874
|
+
process.stderr.write(
|
|
21875
|
+
`\u25B2 ignoring --budget=${raw} (must be a positive number) \u2014 running with no cap
|
|
21876
|
+
`
|
|
21877
|
+
);
|
|
21878
|
+
return void 0;
|
|
21879
|
+
}
|
|
21880
|
+
return raw;
|
|
21881
|
+
}
|
|
21520
21882
|
var program = new Command();
|
|
21521
21883
|
program.name("reasonix").description("DeepSeek-native agent framework \u2014 built for cache hits and cheap tokens.").version(VERSION).option(
|
|
21522
21884
|
"-c, --continue",
|
|
@@ -21554,6 +21916,10 @@ program.command("code [dir]").description(
|
|
|
21554
21916
|
).option("-m, --model <id>", "Override default model (v4-flash)").option("--no-session", "Disable session persistence for this run").option("-r, --resume", "Skip the session picker \u2014 always continue prior messages").option("-n, --new", "Skip the session picker \u2014 always wipe prior messages and start fresh").option("--transcript <path>", "Write a JSONL transcript to this path").option(
|
|
21555
21917
|
"--harvest",
|
|
21556
21918
|
"Opt-in Pillar-2 plan-state extraction. Adds one flash call per turn; off by default (no preset enables it)."
|
|
21919
|
+
).option(
|
|
21920
|
+
"--budget <usd>",
|
|
21921
|
+
"Soft USD cap on session spend. Off by default. Warns at 80%, refuses next turn at 100%. Mid-session: /budget <usd> or /budget off.",
|
|
21922
|
+
(v) => Number.parseFloat(v)
|
|
21557
21923
|
).option(
|
|
21558
21924
|
"--no-dashboard",
|
|
21559
21925
|
"Suppress the auto-launched embedded web dashboard. Default behavior boots it on TUI mount and shows the URL in the status bar (clickable in OSC-8-aware terminals)."
|
|
@@ -21566,6 +21932,7 @@ program.command("code [dir]").description(
|
|
|
21566
21932
|
forceResume: !!opts.resume,
|
|
21567
21933
|
forceNew: !!opts.new,
|
|
21568
21934
|
harvest: !!opts.harvest,
|
|
21935
|
+
budgetUsd: parseBudgetFlag(opts.budget),
|
|
21569
21936
|
noDashboard: opts.dashboard === false
|
|
21570
21937
|
});
|
|
21571
21938
|
});
|
|
@@ -21579,6 +21946,10 @@ program.command("chat").description("Interactive Ink TUI with live cache/cost pa
|
|
|
21579
21946
|
"--branch <n>",
|
|
21580
21947
|
"Self-consistency: run N parallel samples per turn (N\xD7 cost). Manual only \u2014 never auto-enabled.",
|
|
21581
21948
|
(v) => Number.parseInt(v, 10)
|
|
21949
|
+
).option(
|
|
21950
|
+
"--budget <usd>",
|
|
21951
|
+
"Soft USD cap on session spend. Off by default. Warns at 80%, refuses next turn at 100%. Mid-session: /budget <usd> or /budget off.",
|
|
21952
|
+
(v) => Number.parseFloat(v)
|
|
21582
21953
|
).option("--session <name>", "Use a named session (default: from config, usually 'default').").option("--no-session", "Disable session persistence for this run (ephemeral chat)").option("-r, --resume", "Skip the session picker \u2014 always continue prior messages").option(
|
|
21583
21954
|
"-c, --continue",
|
|
21584
21955
|
"Resume the most-recently-used session (any name) without showing the picker."
|
|
@@ -21616,6 +21987,7 @@ program.command("chat").description("Interactive Ink TUI with live cache/cost pa
|
|
|
21616
21987
|
transcript: opts.transcript,
|
|
21617
21988
|
harvest: defaults.harvest,
|
|
21618
21989
|
branch: defaults.branch,
|
|
21990
|
+
budgetUsd: parseBudgetFlag(opts.budget),
|
|
21619
21991
|
session: continueOpts.session,
|
|
21620
21992
|
mcp: defaults.mcp,
|
|
21621
21993
|
mcpPrefix: opts.mcpPrefix,
|
|
@@ -21628,6 +22000,10 @@ program.command("run <task>").description("Run a single task non-interactively,
|
|
|
21628
22000
|
"--branch <n>",
|
|
21629
22001
|
"Self-consistency: run N parallel samples per turn and pick the most confident",
|
|
21630
22002
|
(v) => Number.parseInt(v, 10)
|
|
22003
|
+
).option(
|
|
22004
|
+
"--budget <usd>",
|
|
22005
|
+
"Soft USD cap on session spend. Off by default. Refuses to start a new turn once cumulative cost \u2265 cap.",
|
|
22006
|
+
(v) => Number.parseFloat(v)
|
|
21631
22007
|
).option("--transcript <path>", "Write a JSONL transcript to this path for replay/diff").option(
|
|
21632
22008
|
"--mcp <spec>",
|
|
21633
22009
|
'MCP server spec; repeatable. "name=cmd args...", "cmd args...", or a URL (http/https \u2192 SSE).',
|
|
@@ -21651,6 +22027,7 @@ program.command("run <task>").description("Run a single task non-interactively,
|
|
|
21651
22027
|
system: applyMemoryStack(opts.system, process.cwd()),
|
|
21652
22028
|
harvest: defaults.harvest,
|
|
21653
22029
|
branch: defaults.branch,
|
|
22030
|
+
budgetUsd: parseBudgetFlag(opts.budget),
|
|
21654
22031
|
transcript: opts.transcript,
|
|
21655
22032
|
mcp: defaults.mcp,
|
|
21656
22033
|
mcpPrefix: opts.mcpPrefix
|