reasonix 0.25.0 → 0.26.0
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 +2245 -1667
- package/dist/cli/index.js.map +1 -1
- package/dist/index.d.ts +124 -9
- package/dist/index.js +1067 -490
- package/dist/index.js.map +1 -1
- package/package.json +6 -1
package/dist/index.js
CHANGED
|
@@ -47,8 +47,8 @@ function computeWait(attempt, initial, cap, retryAfter) {
|
|
|
47
47
|
}
|
|
48
48
|
function sleep(ms, signal) {
|
|
49
49
|
if (ms <= 0) return Promise.resolve();
|
|
50
|
-
return new Promise((
|
|
51
|
-
const timer = setTimeout(
|
|
50
|
+
return new Promise((resolve10, reject) => {
|
|
51
|
+
const timer = setTimeout(resolve10, ms);
|
|
52
52
|
if (signal) {
|
|
53
53
|
const onAbort = () => {
|
|
54
54
|
clearTimeout(timer);
|
|
@@ -451,6 +451,54 @@ function resolveTemperatures(budget, custom) {
|
|
|
451
451
|
return out;
|
|
452
452
|
}
|
|
453
453
|
|
|
454
|
+
// src/core/pause-gate.ts
|
|
455
|
+
var PauseGate = class {
|
|
456
|
+
_nextId = 0;
|
|
457
|
+
_pending = /* @__PURE__ */ new Map();
|
|
458
|
+
_listeners = /* @__PURE__ */ new Set();
|
|
459
|
+
/** Block until the user responds. Takes a named options object so the
|
|
460
|
+
* kind and payload fields don't get confused at the call site. */
|
|
461
|
+
ask(opts) {
|
|
462
|
+
const { kind, payload } = opts;
|
|
463
|
+
if (this._listeners.size === 0) {
|
|
464
|
+
throw new Error(
|
|
465
|
+
`${kind}: no confirmation listener registered \u2014 cannot prompt the user. This tool can only be used inside an interactive Reasonix session.`
|
|
466
|
+
);
|
|
467
|
+
}
|
|
468
|
+
return new Promise((resolve10) => {
|
|
469
|
+
const id = this._nextId++;
|
|
470
|
+
const request = { id, kind, payload };
|
|
471
|
+
this._pending.set(id, { resolve: resolve10, request });
|
|
472
|
+
for (const fn of this._listeners) {
|
|
473
|
+
try {
|
|
474
|
+
fn(request);
|
|
475
|
+
} catch {
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
});
|
|
479
|
+
}
|
|
480
|
+
/** Resolve a pending request. Called by the App's modal callback. */
|
|
481
|
+
resolve(id, data) {
|
|
482
|
+
const p = this._pending.get(id);
|
|
483
|
+
if (!p) return;
|
|
484
|
+
this._pending.delete(id);
|
|
485
|
+
p.resolve(data);
|
|
486
|
+
}
|
|
487
|
+
/** Subscribe to new pause requests. Returns an unsubscribe function. */
|
|
488
|
+
on(fn) {
|
|
489
|
+
this._listeners.add(fn);
|
|
490
|
+
return () => {
|
|
491
|
+
this._listeners.delete(fn);
|
|
492
|
+
};
|
|
493
|
+
}
|
|
494
|
+
/** Current pending request, if any (polling fallback). */
|
|
495
|
+
get current() {
|
|
496
|
+
for (const [, p] of this._pending) return p.request;
|
|
497
|
+
return null;
|
|
498
|
+
}
|
|
499
|
+
};
|
|
500
|
+
var pauseGate = new PauseGate();
|
|
501
|
+
|
|
454
502
|
// src/hooks.ts
|
|
455
503
|
import { spawn } from "child_process";
|
|
456
504
|
import { existsSync, readFileSync } from "fs";
|
|
@@ -523,7 +571,7 @@ function matchesTool(hook, toolName) {
|
|
|
523
571
|
}
|
|
524
572
|
var HOOK_OUTPUT_CAP_BYTES = 256 * 1024;
|
|
525
573
|
function defaultSpawner(input) {
|
|
526
|
-
return new Promise((
|
|
574
|
+
return new Promise((resolve10) => {
|
|
527
575
|
const child = spawn(input.command, {
|
|
528
576
|
cwd: input.cwd,
|
|
529
577
|
shell: true,
|
|
@@ -568,7 +616,7 @@ function defaultSpawner(input) {
|
|
|
568
616
|
child.stderr.on("data", (chunk) => onChunk("stderr", chunk));
|
|
569
617
|
child.once("error", (err) => {
|
|
570
618
|
clearTimeout(timer);
|
|
571
|
-
|
|
619
|
+
resolve10({
|
|
572
620
|
exitCode: null,
|
|
573
621
|
stdout: Buffer.concat(stdoutChunks).toString("utf8"),
|
|
574
622
|
stderr: Buffer.concat(stderrChunks).toString("utf8"),
|
|
@@ -579,7 +627,7 @@ function defaultSpawner(input) {
|
|
|
579
627
|
});
|
|
580
628
|
child.once("close", (code) => {
|
|
581
629
|
clearTimeout(timer);
|
|
582
|
-
|
|
630
|
+
resolve10({
|
|
583
631
|
exitCode: code,
|
|
584
632
|
stdout: Buffer.concat(stdoutChunks).toString("utf8").trim(),
|
|
585
633
|
stderr: Buffer.concat(stderrChunks).toString("utf8").trim(),
|
|
@@ -989,7 +1037,10 @@ var ToolRegistry = class {
|
|
|
989
1037
|
}
|
|
990
1038
|
}
|
|
991
1039
|
try {
|
|
992
|
-
const result = await tool.fn(args, {
|
|
1040
|
+
const result = await tool.fn(args, {
|
|
1041
|
+
signal: opts.signal,
|
|
1042
|
+
confirmationGate: opts.confirmationGate
|
|
1043
|
+
});
|
|
993
1044
|
const str = typeof result === "string" ? result : JSON.stringify(result);
|
|
994
1045
|
let clipped = str;
|
|
995
1046
|
if (opts.maxResultTokens !== void 0) {
|
|
@@ -1193,98 +1244,6 @@ function blockToString(block) {
|
|
|
1193
1244
|
return `[unknown block: ${JSON.stringify(block)}]`;
|
|
1194
1245
|
}
|
|
1195
1246
|
|
|
1196
|
-
// src/memory/runtime.ts
|
|
1197
|
-
import { createHash } from "crypto";
|
|
1198
|
-
var ImmutablePrefix = class {
|
|
1199
|
-
system;
|
|
1200
|
-
/** Each `addTool` costs one cache-miss turn — DeepSeek's prefix cache is keyed by full tool list. */
|
|
1201
|
-
_toolSpecs;
|
|
1202
|
-
fewShots;
|
|
1203
|
-
/** Invalidated only via `addTool`; bypassing it leaves cache stale → fingerprint diverges from sent prefix. */
|
|
1204
|
-
_fingerprintCache = null;
|
|
1205
|
-
constructor(opts) {
|
|
1206
|
-
this.system = opts.system;
|
|
1207
|
-
this._toolSpecs = [...opts.toolSpecs ?? []];
|
|
1208
|
-
this.fewShots = Object.freeze([...opts.fewShots ?? []]);
|
|
1209
|
-
}
|
|
1210
|
-
get toolSpecs() {
|
|
1211
|
-
return this._toolSpecs;
|
|
1212
|
-
}
|
|
1213
|
-
toMessages() {
|
|
1214
|
-
return [{ role: "system", content: this.system }, ...this.fewShots.map((m) => ({ ...m }))];
|
|
1215
|
-
}
|
|
1216
|
-
tools() {
|
|
1217
|
-
return this._toolSpecs.map((t) => structuredClone(t));
|
|
1218
|
-
}
|
|
1219
|
-
addTool(spec) {
|
|
1220
|
-
const name = spec.function?.name;
|
|
1221
|
-
if (!name) return false;
|
|
1222
|
-
if (this._toolSpecs.some((t) => t.function?.name === name)) return false;
|
|
1223
|
-
this._toolSpecs.push(spec);
|
|
1224
|
-
this._fingerprintCache = null;
|
|
1225
|
-
return true;
|
|
1226
|
-
}
|
|
1227
|
-
get fingerprint() {
|
|
1228
|
-
if (this._fingerprintCache !== null) return this._fingerprintCache;
|
|
1229
|
-
this._fingerprintCache = this.computeFingerprint();
|
|
1230
|
-
return this._fingerprintCache;
|
|
1231
|
-
}
|
|
1232
|
-
/** Dev/test only — throws on cache drift, which always means a non-`addTool` mutation slipped in. */
|
|
1233
|
-
verifyFingerprint() {
|
|
1234
|
-
const fresh = this.computeFingerprint();
|
|
1235
|
-
if (this._fingerprintCache !== null && this._fingerprintCache !== fresh) {
|
|
1236
|
-
throw new Error(
|
|
1237
|
-
`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.`
|
|
1238
|
-
);
|
|
1239
|
-
}
|
|
1240
|
-
this._fingerprintCache = fresh;
|
|
1241
|
-
return fresh;
|
|
1242
|
-
}
|
|
1243
|
-
computeFingerprint() {
|
|
1244
|
-
const blob = JSON.stringify({
|
|
1245
|
-
system: this.system,
|
|
1246
|
-
tools: this._toolSpecs,
|
|
1247
|
-
shots: this.fewShots
|
|
1248
|
-
});
|
|
1249
|
-
return createHash("sha256").update(blob).digest("hex").slice(0, 16);
|
|
1250
|
-
}
|
|
1251
|
-
};
|
|
1252
|
-
var AppendOnlyLog = class {
|
|
1253
|
-
_entries = [];
|
|
1254
|
-
append(message) {
|
|
1255
|
-
if (!message || typeof message !== "object" || !("role" in message)) {
|
|
1256
|
-
throw new Error(`invalid log entry: ${JSON.stringify(message)}`);
|
|
1257
|
-
}
|
|
1258
|
-
this._entries.push(message);
|
|
1259
|
-
}
|
|
1260
|
-
extend(messages) {
|
|
1261
|
-
for (const m of messages) this.append(m);
|
|
1262
|
-
}
|
|
1263
|
-
/** The one append-only-breaking path — reserved for `/compact` + recovery. Use `append()` otherwise. */
|
|
1264
|
-
compactInPlace(replacement) {
|
|
1265
|
-
this._entries = [...replacement];
|
|
1266
|
-
}
|
|
1267
|
-
get entries() {
|
|
1268
|
-
return this._entries;
|
|
1269
|
-
}
|
|
1270
|
-
toMessages() {
|
|
1271
|
-
return this._entries.map((e) => ({ ...e }));
|
|
1272
|
-
}
|
|
1273
|
-
get length() {
|
|
1274
|
-
return this._entries.length;
|
|
1275
|
-
}
|
|
1276
|
-
};
|
|
1277
|
-
var VolatileScratch = class {
|
|
1278
|
-
reasoning = null;
|
|
1279
|
-
planState = null;
|
|
1280
|
-
notes = [];
|
|
1281
|
-
reset() {
|
|
1282
|
-
this.reasoning = null;
|
|
1283
|
-
this.planState = null;
|
|
1284
|
-
this.notes = [];
|
|
1285
|
-
}
|
|
1286
|
-
};
|
|
1287
|
-
|
|
1288
1247
|
// src/memory/session.ts
|
|
1289
1248
|
import { execFileSync } from "child_process";
|
|
1290
1249
|
import {
|
|
@@ -1415,6 +1374,338 @@ function countLines(path2) {
|
|
|
1415
1374
|
}
|
|
1416
1375
|
}
|
|
1417
1376
|
|
|
1377
|
+
// src/telemetry/stats.ts
|
|
1378
|
+
var DEEPSEEK_PRICING = {
|
|
1379
|
+
"deepseek-v4-flash": { inputCacheHit: 0.028, inputCacheMiss: 0.139, output: 0.278 },
|
|
1380
|
+
"deepseek-v4-pro": { inputCacheHit: 0.139, inputCacheMiss: 1.667, output: 3.333 },
|
|
1381
|
+
// Compat aliases — priced as v4-flash per the deprecation notice.
|
|
1382
|
+
"deepseek-chat": { inputCacheHit: 0.028, inputCacheMiss: 0.139, output: 0.278 },
|
|
1383
|
+
"deepseek-reasoner": { inputCacheHit: 0.028, inputCacheMiss: 0.139, output: 0.278 }
|
|
1384
|
+
};
|
|
1385
|
+
var CLAUDE_SONNET_PRICING = { input: 3, output: 15 };
|
|
1386
|
+
var DEEPSEEK_CONTEXT_TOKENS = {
|
|
1387
|
+
"deepseek-v4-flash": 1e6,
|
|
1388
|
+
"deepseek-v4-pro": 1e6,
|
|
1389
|
+
"deepseek-chat": 1e6,
|
|
1390
|
+
"deepseek-reasoner": 1e6
|
|
1391
|
+
};
|
|
1392
|
+
var DEFAULT_CONTEXT_TOKENS = 131072;
|
|
1393
|
+
function costUsd(model, usage) {
|
|
1394
|
+
const p = DEEPSEEK_PRICING[model];
|
|
1395
|
+
if (!p) return 0;
|
|
1396
|
+
return (usage.promptCacheHitTokens * p.inputCacheHit + usage.promptCacheMissTokens * p.inputCacheMiss + usage.completionTokens * p.output) / 1e6;
|
|
1397
|
+
}
|
|
1398
|
+
function inputCostUsd(model, usage) {
|
|
1399
|
+
const p = DEEPSEEK_PRICING[model];
|
|
1400
|
+
if (!p) return 0;
|
|
1401
|
+
return (usage.promptCacheHitTokens * p.inputCacheHit + usage.promptCacheMissTokens * p.inputCacheMiss) / 1e6;
|
|
1402
|
+
}
|
|
1403
|
+
function outputCostUsd(model, usage) {
|
|
1404
|
+
const p = DEEPSEEK_PRICING[model];
|
|
1405
|
+
if (!p) return 0;
|
|
1406
|
+
return usage.completionTokens * p.output / 1e6;
|
|
1407
|
+
}
|
|
1408
|
+
function cacheSavingsUsd(model, hitTokens) {
|
|
1409
|
+
if (hitTokens <= 0) return 0;
|
|
1410
|
+
const p = DEEPSEEK_PRICING[model];
|
|
1411
|
+
if (!p) return 0;
|
|
1412
|
+
return hitTokens * (p.inputCacheMiss - p.inputCacheHit) / 1e6;
|
|
1413
|
+
}
|
|
1414
|
+
function claudeEquivalentCost(usage) {
|
|
1415
|
+
return (usage.promptTokens * CLAUDE_SONNET_PRICING.input + usage.completionTokens * CLAUDE_SONNET_PRICING.output) / 1e6;
|
|
1416
|
+
}
|
|
1417
|
+
var SessionStats = class {
|
|
1418
|
+
turns = [];
|
|
1419
|
+
record(turn, model, usage) {
|
|
1420
|
+
const cost = costUsd(model, usage);
|
|
1421
|
+
const stats = {
|
|
1422
|
+
turn,
|
|
1423
|
+
model,
|
|
1424
|
+
usage,
|
|
1425
|
+
cost,
|
|
1426
|
+
cacheHitRatio: usage.cacheHitRatio
|
|
1427
|
+
};
|
|
1428
|
+
this.turns.push(stats);
|
|
1429
|
+
return stats;
|
|
1430
|
+
}
|
|
1431
|
+
get totalCost() {
|
|
1432
|
+
return this.turns.reduce((sum, t) => sum + t.cost, 0);
|
|
1433
|
+
}
|
|
1434
|
+
get totalClaudeEquivalent() {
|
|
1435
|
+
return this.turns.reduce((sum, t) => sum + claudeEquivalentCost(t.usage), 0);
|
|
1436
|
+
}
|
|
1437
|
+
get savingsVsClaude() {
|
|
1438
|
+
const c = this.totalClaudeEquivalent;
|
|
1439
|
+
return c > 0 ? 1 - this.totalCost / c : 0;
|
|
1440
|
+
}
|
|
1441
|
+
get totalInputCost() {
|
|
1442
|
+
return this.turns.reduce((sum, t) => sum + inputCostUsd(t.model, t.usage), 0);
|
|
1443
|
+
}
|
|
1444
|
+
get totalOutputCost() {
|
|
1445
|
+
return this.turns.reduce((sum, t) => sum + outputCostUsd(t.model, t.usage), 0);
|
|
1446
|
+
}
|
|
1447
|
+
get aggregateCacheHitRatio() {
|
|
1448
|
+
let hit = 0;
|
|
1449
|
+
let miss = 0;
|
|
1450
|
+
for (const t of this.turns) {
|
|
1451
|
+
hit += t.usage.promptCacheHitTokens;
|
|
1452
|
+
miss += t.usage.promptCacheMissTokens;
|
|
1453
|
+
}
|
|
1454
|
+
const denom = hit + miss;
|
|
1455
|
+
return denom > 0 ? hit / denom : 0;
|
|
1456
|
+
}
|
|
1457
|
+
summary() {
|
|
1458
|
+
const last = this.turns[this.turns.length - 1];
|
|
1459
|
+
return {
|
|
1460
|
+
turns: this.turns.length,
|
|
1461
|
+
totalCostUsd: round(this.totalCost, 6),
|
|
1462
|
+
totalInputCostUsd: round(this.totalInputCost, 6),
|
|
1463
|
+
totalOutputCostUsd: round(this.totalOutputCost, 6),
|
|
1464
|
+
claudeEquivalentUsd: round(this.totalClaudeEquivalent, 6),
|
|
1465
|
+
savingsVsClaudePct: round(this.savingsVsClaude * 100, 2),
|
|
1466
|
+
cacheHitRatio: round(this.aggregateCacheHitRatio, 4),
|
|
1467
|
+
lastPromptTokens: last?.usage.promptTokens ?? 0,
|
|
1468
|
+
lastTurnCostUsd: round(last?.cost ?? 0, 6)
|
|
1469
|
+
};
|
|
1470
|
+
}
|
|
1471
|
+
};
|
|
1472
|
+
function round(n, digits) {
|
|
1473
|
+
const f = 10 ** digits;
|
|
1474
|
+
return Math.round(n * f) / f;
|
|
1475
|
+
}
|
|
1476
|
+
|
|
1477
|
+
// src/context-manager.ts
|
|
1478
|
+
var HISTORY_FOLD_THRESHOLD = 0.5;
|
|
1479
|
+
var HISTORY_FOLD_TAIL_FRACTION = 0.2;
|
|
1480
|
+
var HISTORY_FOLD_AGGRESSIVE_THRESHOLD = 0.7;
|
|
1481
|
+
var HISTORY_FOLD_AGGRESSIVE_TAIL_FRACTION = 0.1;
|
|
1482
|
+
var HISTORY_FOLD_MIN_SAVINGS_FRACTION = 0.3;
|
|
1483
|
+
var FORCE_SUMMARY_THRESHOLD = 0.8;
|
|
1484
|
+
var PREFLIGHT_EMERGENCY_THRESHOLD = 0.95;
|
|
1485
|
+
var HISTORY_FOLD_MARKER = "[CONVERSATION HISTORY SUMMARY \u2014 earlier turns folded for context efficiency]\n\n";
|
|
1486
|
+
var ContextManager = class {
|
|
1487
|
+
constructor(deps) {
|
|
1488
|
+
this.deps = deps;
|
|
1489
|
+
}
|
|
1490
|
+
deps;
|
|
1491
|
+
/** Decision after a turn's response — fold, exit with summary, or carry on. */
|
|
1492
|
+
decideAfterUsage(usage, model, alreadyFoldedThisTurn) {
|
|
1493
|
+
const ctxMax = DEEPSEEK_CONTEXT_TOKENS[model] ?? DEFAULT_CONTEXT_TOKENS;
|
|
1494
|
+
if (!usage) return { kind: "none", promptTokens: 0, ctxMax, ratio: 0 };
|
|
1495
|
+
const ratio = usage.promptTokens / ctxMax;
|
|
1496
|
+
const base = { promptTokens: usage.promptTokens, ctxMax, ratio };
|
|
1497
|
+
if (ratio > FORCE_SUMMARY_THRESHOLD) {
|
|
1498
|
+
return { kind: "exit-with-summary", ...base };
|
|
1499
|
+
}
|
|
1500
|
+
if (alreadyFoldedThisTurn) return { kind: "none", ...base };
|
|
1501
|
+
if (ratio > HISTORY_FOLD_AGGRESSIVE_THRESHOLD) {
|
|
1502
|
+
return {
|
|
1503
|
+
kind: "fold",
|
|
1504
|
+
...base,
|
|
1505
|
+
tailBudget: Math.floor(ctxMax * HISTORY_FOLD_AGGRESSIVE_TAIL_FRACTION),
|
|
1506
|
+
aggressive: true
|
|
1507
|
+
};
|
|
1508
|
+
}
|
|
1509
|
+
if (ratio > HISTORY_FOLD_THRESHOLD) {
|
|
1510
|
+
return {
|
|
1511
|
+
kind: "fold",
|
|
1512
|
+
...base,
|
|
1513
|
+
tailBudget: Math.floor(ctxMax * HISTORY_FOLD_TAIL_FRACTION),
|
|
1514
|
+
aggressive: false
|
|
1515
|
+
};
|
|
1516
|
+
}
|
|
1517
|
+
return { kind: "none", ...base };
|
|
1518
|
+
}
|
|
1519
|
+
/** Local-side preflight before sending a request — catches oversized payloads early. */
|
|
1520
|
+
decidePreflight(messages, toolSpecs, model) {
|
|
1521
|
+
const ctxMax = DEEPSEEK_CONTEXT_TOKENS[model] ?? DEFAULT_CONTEXT_TOKENS;
|
|
1522
|
+
const estimate = estimateRequestTokens(messages, toolSpecs ?? null);
|
|
1523
|
+
return {
|
|
1524
|
+
needsAction: estimate / ctxMax > PREFLIGHT_EMERGENCY_THRESHOLD,
|
|
1525
|
+
estimateTokens: estimate,
|
|
1526
|
+
ctxMax
|
|
1527
|
+
};
|
|
1528
|
+
}
|
|
1529
|
+
/** Replace older turns with one summary message; keep tail within keepRecentTokens budget. */
|
|
1530
|
+
async fold(model, opts) {
|
|
1531
|
+
const ctxMax = DEEPSEEK_CONTEXT_TOKENS[model] ?? DEFAULT_CONTEXT_TOKENS;
|
|
1532
|
+
const tailBudget = opts?.keepRecentTokens ?? Math.floor(ctxMax * HISTORY_FOLD_TAIL_FRACTION);
|
|
1533
|
+
const all = this.deps.log.toMessages();
|
|
1534
|
+
const noop = {
|
|
1535
|
+
folded: false,
|
|
1536
|
+
beforeMessages: all.length,
|
|
1537
|
+
afterMessages: all.length,
|
|
1538
|
+
summaryChars: 0
|
|
1539
|
+
};
|
|
1540
|
+
if (all.length === 0) return noop;
|
|
1541
|
+
const tokenCounts = all.map((m) => estimateConversationTokens([m]));
|
|
1542
|
+
const totalTokens = tokenCounts.reduce((a, b) => a + b, 0);
|
|
1543
|
+
let cumTokens = 0;
|
|
1544
|
+
let boundary = all.length;
|
|
1545
|
+
for (let i = all.length - 1; i >= 0; i--) {
|
|
1546
|
+
if (cumTokens + tokenCounts[i] > tailBudget) break;
|
|
1547
|
+
cumTokens += tokenCounts[i];
|
|
1548
|
+
if (all[i].role === "user") boundary = i;
|
|
1549
|
+
}
|
|
1550
|
+
if (boundary <= 0) return noop;
|
|
1551
|
+
const head = all.slice(0, boundary);
|
|
1552
|
+
const tail = all.slice(boundary);
|
|
1553
|
+
const headTokens = totalTokens - cumTokens;
|
|
1554
|
+
if (headTokens < totalTokens * HISTORY_FOLD_MIN_SAVINGS_FRACTION) return noop;
|
|
1555
|
+
const summary = await this.summarizeForFold(head);
|
|
1556
|
+
if (!summary) return noop;
|
|
1557
|
+
const summaryMsg = {
|
|
1558
|
+
role: "assistant",
|
|
1559
|
+
content: HISTORY_FOLD_MARKER + summary
|
|
1560
|
+
};
|
|
1561
|
+
const replacement = [summaryMsg, ...tail];
|
|
1562
|
+
this.deps.log.compactInPlace(replacement);
|
|
1563
|
+
this.persistRewrite(replacement);
|
|
1564
|
+
return {
|
|
1565
|
+
folded: true,
|
|
1566
|
+
beforeMessages: all.length,
|
|
1567
|
+
afterMessages: replacement.length,
|
|
1568
|
+
summaryChars: summary.length
|
|
1569
|
+
};
|
|
1570
|
+
}
|
|
1571
|
+
/** Drop a trailing in-flight assistant-with-tool_calls before a forced summary. Tail-only mutation; prefix cache safe. */
|
|
1572
|
+
trimTrailingToolCalls() {
|
|
1573
|
+
const tail = this.deps.log.entries[this.deps.log.entries.length - 1];
|
|
1574
|
+
if (!tail || tail.role !== "assistant" || !Array.isArray(tail.tool_calls) || tail.tool_calls.length === 0) {
|
|
1575
|
+
return false;
|
|
1576
|
+
}
|
|
1577
|
+
const kept = this.deps.log.entries.slice(0, -1);
|
|
1578
|
+
this.deps.log.compactInPlace([...kept]);
|
|
1579
|
+
this.persistRewrite([...kept]);
|
|
1580
|
+
return true;
|
|
1581
|
+
}
|
|
1582
|
+
async summarizeForFold(messagesToSummarize) {
|
|
1583
|
+
const summaryModel = "deepseek-v4-flash";
|
|
1584
|
+
const systemPrompt = "You compress conversation history for a coding agent. Output one prose recap that preserves: the user's overall goal, decisions and conclusions reached, files inspected or modified, important tool results still relevant to ongoing work, and any open todos. Skip turn-by-turn play-by-play. No tool calls, no markdown headings, no SEARCH/REPLACE blocks \u2014 plain prose only.";
|
|
1585
|
+
const healed = healLoadedMessages(messagesToSummarize, DEFAULT_MAX_RESULT_CHARS).messages;
|
|
1586
|
+
const messages = [
|
|
1587
|
+
{ role: "system", content: systemPrompt },
|
|
1588
|
+
...healed,
|
|
1589
|
+
{
|
|
1590
|
+
role: "user",
|
|
1591
|
+
content: "Summarize the conversation above as plain prose. This summary replaces the original turns to free context \u2014 make it self-contained."
|
|
1592
|
+
}
|
|
1593
|
+
];
|
|
1594
|
+
try {
|
|
1595
|
+
const resp = await this.deps.client.chat({
|
|
1596
|
+
model: summaryModel,
|
|
1597
|
+
messages,
|
|
1598
|
+
signal: this.deps.getAbortSignal(),
|
|
1599
|
+
thinking: thinkingModeForModel(summaryModel),
|
|
1600
|
+
reasoningEffort: "high"
|
|
1601
|
+
});
|
|
1602
|
+
this.deps.stats.record(this.deps.getCurrentTurn(), summaryModel, resp.usage ?? new Usage());
|
|
1603
|
+
return stripHallucinatedToolMarkup((resp.content ?? "").trim());
|
|
1604
|
+
} catch {
|
|
1605
|
+
return "";
|
|
1606
|
+
}
|
|
1607
|
+
}
|
|
1608
|
+
persistRewrite(messages) {
|
|
1609
|
+
if (!this.deps.sessionName) return;
|
|
1610
|
+
try {
|
|
1611
|
+
rewriteSession(this.deps.sessionName, messages);
|
|
1612
|
+
} catch {
|
|
1613
|
+
}
|
|
1614
|
+
}
|
|
1615
|
+
};
|
|
1616
|
+
|
|
1617
|
+
// src/memory/runtime.ts
|
|
1618
|
+
import { createHash } from "crypto";
|
|
1619
|
+
var ImmutablePrefix = class {
|
|
1620
|
+
system;
|
|
1621
|
+
/** Each `addTool` costs one cache-miss turn — DeepSeek's prefix cache is keyed by full tool list. */
|
|
1622
|
+
_toolSpecs;
|
|
1623
|
+
fewShots;
|
|
1624
|
+
/** Invalidated only via `addTool`; bypassing it leaves cache stale → fingerprint diverges from sent prefix. */
|
|
1625
|
+
_fingerprintCache = null;
|
|
1626
|
+
constructor(opts) {
|
|
1627
|
+
this.system = opts.system;
|
|
1628
|
+
this._toolSpecs = [...opts.toolSpecs ?? []];
|
|
1629
|
+
this.fewShots = Object.freeze([...opts.fewShots ?? []]);
|
|
1630
|
+
}
|
|
1631
|
+
get toolSpecs() {
|
|
1632
|
+
return this._toolSpecs;
|
|
1633
|
+
}
|
|
1634
|
+
toMessages() {
|
|
1635
|
+
return [{ role: "system", content: this.system }, ...this.fewShots.map((m) => ({ ...m }))];
|
|
1636
|
+
}
|
|
1637
|
+
tools() {
|
|
1638
|
+
return this._toolSpecs.map((t) => structuredClone(t));
|
|
1639
|
+
}
|
|
1640
|
+
addTool(spec) {
|
|
1641
|
+
const name = spec.function?.name;
|
|
1642
|
+
if (!name) return false;
|
|
1643
|
+
if (this._toolSpecs.some((t) => t.function?.name === name)) return false;
|
|
1644
|
+
this._toolSpecs.push(spec);
|
|
1645
|
+
this._fingerprintCache = null;
|
|
1646
|
+
return true;
|
|
1647
|
+
}
|
|
1648
|
+
get fingerprint() {
|
|
1649
|
+
if (this._fingerprintCache !== null) return this._fingerprintCache;
|
|
1650
|
+
this._fingerprintCache = this.computeFingerprint();
|
|
1651
|
+
return this._fingerprintCache;
|
|
1652
|
+
}
|
|
1653
|
+
/** Dev/test only — throws on cache drift, which always means a non-`addTool` mutation slipped in. */
|
|
1654
|
+
verifyFingerprint() {
|
|
1655
|
+
const fresh = this.computeFingerprint();
|
|
1656
|
+
if (this._fingerprintCache !== null && this._fingerprintCache !== fresh) {
|
|
1657
|
+
throw new Error(
|
|
1658
|
+
`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.`
|
|
1659
|
+
);
|
|
1660
|
+
}
|
|
1661
|
+
this._fingerprintCache = fresh;
|
|
1662
|
+
return fresh;
|
|
1663
|
+
}
|
|
1664
|
+
computeFingerprint() {
|
|
1665
|
+
const blob = JSON.stringify({
|
|
1666
|
+
system: this.system,
|
|
1667
|
+
tools: this._toolSpecs,
|
|
1668
|
+
shots: this.fewShots
|
|
1669
|
+
});
|
|
1670
|
+
return createHash("sha256").update(blob).digest("hex").slice(0, 16);
|
|
1671
|
+
}
|
|
1672
|
+
};
|
|
1673
|
+
var AppendOnlyLog = class {
|
|
1674
|
+
_entries = [];
|
|
1675
|
+
append(message) {
|
|
1676
|
+
if (!message || typeof message !== "object" || !("role" in message)) {
|
|
1677
|
+
throw new Error(`invalid log entry: ${JSON.stringify(message)}`);
|
|
1678
|
+
}
|
|
1679
|
+
this._entries.push(message);
|
|
1680
|
+
}
|
|
1681
|
+
extend(messages) {
|
|
1682
|
+
for (const m of messages) this.append(m);
|
|
1683
|
+
}
|
|
1684
|
+
/** The one append-only-breaking path — reserved for `/compact` + recovery. Use `append()` otherwise. */
|
|
1685
|
+
compactInPlace(replacement) {
|
|
1686
|
+
this._entries = [...replacement];
|
|
1687
|
+
}
|
|
1688
|
+
get entries() {
|
|
1689
|
+
return this._entries;
|
|
1690
|
+
}
|
|
1691
|
+
toMessages() {
|
|
1692
|
+
return this._entries.map((e) => ({ ...e }));
|
|
1693
|
+
}
|
|
1694
|
+
get length() {
|
|
1695
|
+
return this._entries.length;
|
|
1696
|
+
}
|
|
1697
|
+
};
|
|
1698
|
+
var VolatileScratch = class {
|
|
1699
|
+
reasoning = null;
|
|
1700
|
+
planState = null;
|
|
1701
|
+
notes = [];
|
|
1702
|
+
reset() {
|
|
1703
|
+
this.reasoning = null;
|
|
1704
|
+
this.planState = null;
|
|
1705
|
+
this.notes = [];
|
|
1706
|
+
}
|
|
1707
|
+
};
|
|
1708
|
+
|
|
1418
1709
|
// src/repair/scavenge.ts
|
|
1419
1710
|
function scavengeToolCalls(reasoningContent, opts) {
|
|
1420
1711
|
if (!reasoningContent) return { calls: [], notes: [] };
|
|
@@ -1716,109 +2007,7 @@ function signature(call) {
|
|
|
1716
2007
|
return `${call.function?.name ?? ""}::${call.function?.arguments ?? ""}`;
|
|
1717
2008
|
}
|
|
1718
2009
|
|
|
1719
|
-
// src/telemetry/stats.ts
|
|
1720
|
-
var DEEPSEEK_PRICING = {
|
|
1721
|
-
"deepseek-v4-flash": { inputCacheHit: 0.028, inputCacheMiss: 0.139, output: 0.278 },
|
|
1722
|
-
"deepseek-v4-pro": { inputCacheHit: 0.139, inputCacheMiss: 1.667, output: 3.333 },
|
|
1723
|
-
// Compat aliases — priced as v4-flash per the deprecation notice.
|
|
1724
|
-
"deepseek-chat": { inputCacheHit: 0.028, inputCacheMiss: 0.139, output: 0.278 },
|
|
1725
|
-
"deepseek-reasoner": { inputCacheHit: 0.028, inputCacheMiss: 0.139, output: 0.278 }
|
|
1726
|
-
};
|
|
1727
|
-
var CLAUDE_SONNET_PRICING = { input: 3, output: 15 };
|
|
1728
|
-
var DEEPSEEK_CONTEXT_TOKENS = {
|
|
1729
|
-
"deepseek-v4-flash": 1e6,
|
|
1730
|
-
"deepseek-v4-pro": 1e6,
|
|
1731
|
-
"deepseek-chat": 1e6,
|
|
1732
|
-
"deepseek-reasoner": 1e6
|
|
1733
|
-
};
|
|
1734
|
-
var DEFAULT_CONTEXT_TOKENS = 131072;
|
|
1735
|
-
function costUsd(model, usage) {
|
|
1736
|
-
const p = DEEPSEEK_PRICING[model];
|
|
1737
|
-
if (!p) return 0;
|
|
1738
|
-
return (usage.promptCacheHitTokens * p.inputCacheHit + usage.promptCacheMissTokens * p.inputCacheMiss + usage.completionTokens * p.output) / 1e6;
|
|
1739
|
-
}
|
|
1740
|
-
function inputCostUsd(model, usage) {
|
|
1741
|
-
const p = DEEPSEEK_PRICING[model];
|
|
1742
|
-
if (!p) return 0;
|
|
1743
|
-
return (usage.promptCacheHitTokens * p.inputCacheHit + usage.promptCacheMissTokens * p.inputCacheMiss) / 1e6;
|
|
1744
|
-
}
|
|
1745
|
-
function outputCostUsd(model, usage) {
|
|
1746
|
-
const p = DEEPSEEK_PRICING[model];
|
|
1747
|
-
if (!p) return 0;
|
|
1748
|
-
return usage.completionTokens * p.output / 1e6;
|
|
1749
|
-
}
|
|
1750
|
-
function cacheSavingsUsd(model, hitTokens) {
|
|
1751
|
-
if (hitTokens <= 0) return 0;
|
|
1752
|
-
const p = DEEPSEEK_PRICING[model];
|
|
1753
|
-
if (!p) return 0;
|
|
1754
|
-
return hitTokens * (p.inputCacheMiss - p.inputCacheHit) / 1e6;
|
|
1755
|
-
}
|
|
1756
|
-
function claudeEquivalentCost(usage) {
|
|
1757
|
-
return (usage.promptTokens * CLAUDE_SONNET_PRICING.input + usage.completionTokens * CLAUDE_SONNET_PRICING.output) / 1e6;
|
|
1758
|
-
}
|
|
1759
|
-
var SessionStats = class {
|
|
1760
|
-
turns = [];
|
|
1761
|
-
record(turn, model, usage) {
|
|
1762
|
-
const cost = costUsd(model, usage);
|
|
1763
|
-
const stats = {
|
|
1764
|
-
turn,
|
|
1765
|
-
model,
|
|
1766
|
-
usage,
|
|
1767
|
-
cost,
|
|
1768
|
-
cacheHitRatio: usage.cacheHitRatio
|
|
1769
|
-
};
|
|
1770
|
-
this.turns.push(stats);
|
|
1771
|
-
return stats;
|
|
1772
|
-
}
|
|
1773
|
-
get totalCost() {
|
|
1774
|
-
return this.turns.reduce((sum, t) => sum + t.cost, 0);
|
|
1775
|
-
}
|
|
1776
|
-
get totalClaudeEquivalent() {
|
|
1777
|
-
return this.turns.reduce((sum, t) => sum + claudeEquivalentCost(t.usage), 0);
|
|
1778
|
-
}
|
|
1779
|
-
get savingsVsClaude() {
|
|
1780
|
-
const c = this.totalClaudeEquivalent;
|
|
1781
|
-
return c > 0 ? 1 - this.totalCost / c : 0;
|
|
1782
|
-
}
|
|
1783
|
-
get totalInputCost() {
|
|
1784
|
-
return this.turns.reduce((sum, t) => sum + inputCostUsd(t.model, t.usage), 0);
|
|
1785
|
-
}
|
|
1786
|
-
get totalOutputCost() {
|
|
1787
|
-
return this.turns.reduce((sum, t) => sum + outputCostUsd(t.model, t.usage), 0);
|
|
1788
|
-
}
|
|
1789
|
-
get aggregateCacheHitRatio() {
|
|
1790
|
-
let hit = 0;
|
|
1791
|
-
let miss = 0;
|
|
1792
|
-
for (const t of this.turns) {
|
|
1793
|
-
hit += t.usage.promptCacheHitTokens;
|
|
1794
|
-
miss += t.usage.promptCacheMissTokens;
|
|
1795
|
-
}
|
|
1796
|
-
const denom = hit + miss;
|
|
1797
|
-
return denom > 0 ? hit / denom : 0;
|
|
1798
|
-
}
|
|
1799
|
-
summary() {
|
|
1800
|
-
const last = this.turns[this.turns.length - 1];
|
|
1801
|
-
return {
|
|
1802
|
-
turns: this.turns.length,
|
|
1803
|
-
totalCostUsd: round(this.totalCost, 6),
|
|
1804
|
-
totalInputCostUsd: round(this.totalInputCost, 6),
|
|
1805
|
-
totalOutputCostUsd: round(this.totalOutputCost, 6),
|
|
1806
|
-
claudeEquivalentUsd: round(this.totalClaudeEquivalent, 6),
|
|
1807
|
-
savingsVsClaudePct: round(this.savingsVsClaude * 100, 2),
|
|
1808
|
-
cacheHitRatio: round(this.aggregateCacheHitRatio, 4),
|
|
1809
|
-
lastPromptTokens: last?.usage.promptTokens ?? 0,
|
|
1810
|
-
lastTurnCostUsd: round(last?.cost ?? 0, 6)
|
|
1811
|
-
};
|
|
1812
|
-
}
|
|
1813
|
-
};
|
|
1814
|
-
function round(n, digits) {
|
|
1815
|
-
const f = 10 ** digits;
|
|
1816
|
-
return Math.round(n * f) / f;
|
|
1817
|
-
}
|
|
1818
|
-
|
|
1819
2010
|
// src/loop.ts
|
|
1820
|
-
var ARGS_COMPACT_THRESHOLD_TOKENS = 800;
|
|
1821
|
-
var TURN_END_RESULT_CAP_TOKENS = 3e3;
|
|
1822
2011
|
var FAILURE_ESCALATION_THRESHOLD = 3;
|
|
1823
2012
|
var ESCALATION_MODEL = "deepseek-v4-pro";
|
|
1824
2013
|
var NEEDS_PRO_MARKER_PREFIX = "<<<NEEDS_PRO";
|
|
@@ -1849,6 +2038,8 @@ var CacheFirstLoop = class {
|
|
|
1849
2038
|
sessionName;
|
|
1850
2039
|
hooks;
|
|
1851
2040
|
hookCwd;
|
|
2041
|
+
/** PauseGate bridge — defaults to singleton, injectable for tests. */
|
|
2042
|
+
confirmationGate;
|
|
1852
2043
|
/** Number of messages that were pre-loaded from the session file. */
|
|
1853
2044
|
resumedMessageCount;
|
|
1854
2045
|
_turn = 0;
|
|
@@ -1860,6 +2051,8 @@ var CacheFirstLoop = class {
|
|
|
1860
2051
|
_turnFailureCount = 0;
|
|
1861
2052
|
_turnFailureTypes = {};
|
|
1862
2053
|
_turnSelfCorrected = false;
|
|
2054
|
+
_foldedThisTurn = false;
|
|
2055
|
+
context;
|
|
1863
2056
|
constructor(opts) {
|
|
1864
2057
|
this.client = opts.client;
|
|
1865
2058
|
this.prefix = opts.prefix;
|
|
@@ -1871,6 +2064,7 @@ var CacheFirstLoop = class {
|
|
|
1871
2064
|
this.maxToolIters = opts.maxToolIters ?? 64;
|
|
1872
2065
|
this.hooks = opts.hooks ?? [];
|
|
1873
2066
|
this.hookCwd = opts.hookCwd ?? process.cwd();
|
|
2067
|
+
this.confirmationGate = opts.confirmationGate ?? pauseGate;
|
|
1874
2068
|
if (typeof opts.branch === "number") {
|
|
1875
2069
|
this.branchOptions = { budget: opts.branch };
|
|
1876
2070
|
} else if (opts.branch && typeof opts.branch === "object") {
|
|
@@ -1933,54 +2127,18 @@ var CacheFirstLoop = class {
|
|
|
1933
2127
|
} else {
|
|
1934
2128
|
this.resumedMessageCount = 0;
|
|
1935
2129
|
}
|
|
2130
|
+
this.context = new ContextManager({
|
|
2131
|
+
client: this.client,
|
|
2132
|
+
log: this.log,
|
|
2133
|
+
stats: this.stats,
|
|
2134
|
+
sessionName: this.sessionName,
|
|
2135
|
+
getAbortSignal: () => this._turnAbort.signal,
|
|
2136
|
+
getCurrentTurn: () => this._turn
|
|
2137
|
+
});
|
|
1936
2138
|
}
|
|
1937
|
-
/**
|
|
1938
|
-
|
|
1939
|
-
|
|
1940
|
-
const { messages, healedCount } = shrinkOversizedToolCallArgsByTokens(
|
|
1941
|
-
before,
|
|
1942
|
-
ARGS_COMPACT_THRESHOLD_TOKENS
|
|
1943
|
-
);
|
|
1944
|
-
if (healedCount === 0) return;
|
|
1945
|
-
this.log.compactInPlace(messages);
|
|
1946
|
-
if (this.sessionName) {
|
|
1947
|
-
try {
|
|
1948
|
-
rewriteSession(this.sessionName, messages);
|
|
1949
|
-
} catch {
|
|
1950
|
-
}
|
|
1951
|
-
}
|
|
1952
|
-
}
|
|
1953
|
-
/** Preventive end-of-turn shrink — trim big results before they ride into the next prompt. */
|
|
1954
|
-
autoCompactToolResultsOnTurnEnd() {
|
|
1955
|
-
const before = this.log.toMessages();
|
|
1956
|
-
const shrunk = shrinkOversizedToolResultsByTokens(before, TURN_END_RESULT_CAP_TOKENS);
|
|
1957
|
-
if (shrunk.healedCount === 0) return;
|
|
1958
|
-
this.log.compactInPlace(shrunk.messages);
|
|
1959
|
-
if (this.sessionName) {
|
|
1960
|
-
try {
|
|
1961
|
-
rewriteSession(this.sessionName, shrunk.messages);
|
|
1962
|
-
} catch {
|
|
1963
|
-
}
|
|
1964
|
-
}
|
|
1965
|
-
}
|
|
1966
|
-
compact(maxTokens = 4e3) {
|
|
1967
|
-
const before = this.log.toMessages();
|
|
1968
|
-
const resultsPass = shrinkOversizedToolResultsByTokens(before, maxTokens);
|
|
1969
|
-
const argsPass = shrinkOversizedToolCallArgsByTokens(resultsPass.messages, maxTokens);
|
|
1970
|
-
const messages = argsPass.messages;
|
|
1971
|
-
const healedCount = resultsPass.healedCount + argsPass.healedCount;
|
|
1972
|
-
const tokensSaved = resultsPass.tokensSaved + argsPass.tokensSaved;
|
|
1973
|
-
const charsSaved = resultsPass.charsSaved + argsPass.charsSaved;
|
|
1974
|
-
if (healedCount > 0) {
|
|
1975
|
-
this.log.compactInPlace(messages);
|
|
1976
|
-
if (this.sessionName) {
|
|
1977
|
-
try {
|
|
1978
|
-
rewriteSession(this.sessionName, messages);
|
|
1979
|
-
} catch {
|
|
1980
|
-
}
|
|
1981
|
-
}
|
|
1982
|
-
}
|
|
1983
|
-
return { healedCount, tokensSaved, charsSaved };
|
|
2139
|
+
/** Replace older turns with one summary message; keep tail within keepRecentTokens budget. */
|
|
2140
|
+
async compactHistory(opts) {
|
|
2141
|
+
return this.context.fold(this.model, opts);
|
|
1984
2142
|
}
|
|
1985
2143
|
appendAndPersist(message) {
|
|
1986
2144
|
this.log.append(message);
|
|
@@ -2178,6 +2336,7 @@ var CacheFirstLoop = class {
|
|
|
2178
2336
|
this._turnFailureTypes = {};
|
|
2179
2337
|
this._turnSelfCorrected = false;
|
|
2180
2338
|
this._escalateThisTurn = false;
|
|
2339
|
+
this._foldedThisTurn = false;
|
|
2181
2340
|
let armedConsumed = false;
|
|
2182
2341
|
if (this._proArmedForNextTurn) {
|
|
2183
2342
|
this._escalateThisTurn = true;
|
|
@@ -2214,7 +2373,6 @@ var CacheFirstLoop = class {
|
|
|
2214
2373
|
content: stoppedMsg,
|
|
2215
2374
|
forcedSummary: true
|
|
2216
2375
|
};
|
|
2217
|
-
this.autoCompactToolResultsOnTurnEnd();
|
|
2218
2376
|
yield { turn: this._turn, role: "done", content: stoppedMsg };
|
|
2219
2377
|
this._turnAbort = new AbortController();
|
|
2220
2378
|
return;
|
|
@@ -2236,26 +2394,31 @@ var CacheFirstLoop = class {
|
|
|
2236
2394
|
}
|
|
2237
2395
|
let messages = this.buildMessages(pendingUser);
|
|
2238
2396
|
{
|
|
2239
|
-
const
|
|
2240
|
-
|
|
2241
|
-
|
|
2242
|
-
|
|
2243
|
-
|
|
2397
|
+
const decision2 = this.context.decidePreflight(messages, this.prefix.toolSpecs, this.model);
|
|
2398
|
+
if (decision2.needsAction) {
|
|
2399
|
+
const { estimateTokens: estimate, ctxMax } = decision2;
|
|
2400
|
+
yield {
|
|
2401
|
+
turn: this._turn,
|
|
2402
|
+
role: "status",
|
|
2403
|
+
content: "preflight: context near full, attempting fold\u2026"
|
|
2404
|
+
};
|
|
2405
|
+
const result = await this.context.fold(this.model);
|
|
2406
|
+
if (result.folded) {
|
|
2244
2407
|
yield {
|
|
2245
2408
|
turn: this._turn,
|
|
2246
2409
|
role: "warning",
|
|
2247
|
-
content: `preflight: request ~${estimate.toLocaleString()}/${
|
|
2248
|
-
estimate /
|
|
2249
|
-
)}%) \u2014
|
|
2410
|
+
content: `preflight: request ~${estimate.toLocaleString()}/${ctxMax.toLocaleString()} tokens (${Math.round(
|
|
2411
|
+
estimate / ctxMax * 100
|
|
2412
|
+
)}%) \u2014 folded ${result.beforeMessages} messages \u2192 ${result.afterMessages} (summary ${result.summaryChars} chars). Sending.`
|
|
2250
2413
|
};
|
|
2251
2414
|
messages = this.buildMessages(pendingUser);
|
|
2252
2415
|
} else {
|
|
2253
2416
|
yield {
|
|
2254
2417
|
turn: this._turn,
|
|
2255
2418
|
role: "warning",
|
|
2256
|
-
content: `preflight: request ~${estimate.toLocaleString()}/${
|
|
2257
|
-
estimate /
|
|
2258
|
-
)}%) and nothing to
|
|
2419
|
+
content: `preflight: request ~${estimate.toLocaleString()}/${ctxMax.toLocaleString()} tokens (${Math.round(
|
|
2420
|
+
estimate / ctxMax * 100
|
|
2421
|
+
)}%) and nothing left to fold \u2014 DeepSeek will likely 400. Run /clear or /new to start fresh.`
|
|
2259
2422
|
};
|
|
2260
2423
|
}
|
|
2261
2424
|
}
|
|
@@ -2310,8 +2473,8 @@ var CacheFirstLoop = class {
|
|
|
2310
2473
|
}
|
|
2311
2474
|
);
|
|
2312
2475
|
for (let k = 0; k < budget; k++) {
|
|
2313
|
-
const sample = queue.shift() ?? await new Promise((
|
|
2314
|
-
waiter =
|
|
2476
|
+
const sample = queue.shift() ?? await new Promise((resolve10) => {
|
|
2477
|
+
waiter = resolve10;
|
|
2315
2478
|
});
|
|
2316
2479
|
yield {
|
|
2317
2480
|
turn: this._turn,
|
|
@@ -2450,7 +2613,6 @@ var CacheFirstLoop = class {
|
|
|
2450
2613
|
}
|
|
2451
2614
|
} catch (err) {
|
|
2452
2615
|
if (signal.aborted) {
|
|
2453
|
-
this.autoCompactToolResultsOnTurnEnd();
|
|
2454
2616
|
yield { turn: this._turn, role: "done", content: "" };
|
|
2455
2617
|
this._turnAbort = new AbortController();
|
|
2456
2618
|
return;
|
|
@@ -2568,60 +2730,43 @@ var CacheFirstLoop = class {
|
|
|
2568
2730
|
yield* this.forceSummaryAfterIterLimit({ reason: "stuck" });
|
|
2569
2731
|
return;
|
|
2570
2732
|
}
|
|
2571
|
-
this.autoCompactToolResultsOnTurnEnd();
|
|
2572
2733
|
yield { turn: this._turn, role: "done", content: assistantContent };
|
|
2573
2734
|
return;
|
|
2574
2735
|
}
|
|
2575
|
-
const
|
|
2576
|
-
if (
|
|
2577
|
-
|
|
2578
|
-
|
|
2579
|
-
|
|
2580
|
-
|
|
2581
|
-
|
|
2582
|
-
|
|
2583
|
-
|
|
2584
|
-
|
|
2585
|
-
|
|
2586
|
-
|
|
2587
|
-
|
|
2588
|
-
)}%) \u2014 proactively compacted ${soft.healedCount} tool result(s) to 4k tokens, saved ${soft.tokensSaved.toLocaleString()} tokens (now ~${after.toLocaleString()}). Staying ahead of the 80% guard.`
|
|
2589
|
-
};
|
|
2590
|
-
}
|
|
2591
|
-
}
|
|
2592
|
-
}
|
|
2593
|
-
if (usage && usage.promptTokens / ctxMax > 0.8) {
|
|
2594
|
-
const before = usage.promptTokens;
|
|
2595
|
-
const compactResult = this.compact(1e3);
|
|
2596
|
-
if (compactResult.healedCount > 0) {
|
|
2597
|
-
const after = Math.max(0, before - compactResult.tokensSaved);
|
|
2598
|
-
yield {
|
|
2599
|
-
turn: this._turn,
|
|
2600
|
-
role: "warning",
|
|
2601
|
-
content: `context ${before.toLocaleString()}/${ctxMax.toLocaleString()} \u2014 auto-compacted ${compactResult.healedCount} oversized tool result(s), saved ${compactResult.tokensSaved.toLocaleString()} tokens (now ~${after.toLocaleString()}). Continuing.`
|
|
2602
|
-
};
|
|
2603
|
-
} else {
|
|
2736
|
+
const decision = this.context.decideAfterUsage(usage, this.model, this._foldedThisTurn);
|
|
2737
|
+
if (decision.kind === "fold") {
|
|
2738
|
+
this._foldedThisTurn = true;
|
|
2739
|
+
const before = decision.promptTokens;
|
|
2740
|
+
const ctxMax = decision.ctxMax;
|
|
2741
|
+
const aggressiveTag = decision.aggressive ? " (aggressive)" : "";
|
|
2742
|
+
yield {
|
|
2743
|
+
turn: this._turn,
|
|
2744
|
+
role: "status",
|
|
2745
|
+
content: `compacting history${aggressiveTag}\u2026`
|
|
2746
|
+
};
|
|
2747
|
+
const result = await this.compactHistory({ keepRecentTokens: decision.tailBudget });
|
|
2748
|
+
if (result.folded) {
|
|
2604
2749
|
yield {
|
|
2605
2750
|
turn: this._turn,
|
|
2606
2751
|
role: "warning",
|
|
2607
|
-
content: `context ${before.toLocaleString()}/${ctxMax.toLocaleString()} (${Math.round(
|
|
2608
|
-
before / ctxMax * 100
|
|
2609
|
-
)}%) \u2014
|
|
2610
|
-
};
|
|
2611
|
-
const tail = this.log.entries[this.log.entries.length - 1];
|
|
2612
|
-
if (tail && tail.role === "assistant" && Array.isArray(tail.tool_calls) && tail.tool_calls.length > 0) {
|
|
2613
|
-
const kept = this.log.entries.slice(0, -1);
|
|
2614
|
-
this.log.compactInPlace([...kept]);
|
|
2615
|
-
if (this.sessionName) {
|
|
2616
|
-
try {
|
|
2617
|
-
rewriteSession(this.sessionName, kept);
|
|
2618
|
-
} catch {
|
|
2619
|
-
}
|
|
2620
|
-
}
|
|
2621
|
-
}
|
|
2622
|
-
yield* this.forceSummaryAfterIterLimit({ reason: "context-guard" });
|
|
2623
|
-
return;
|
|
2752
|
+
content: `context ${before.toLocaleString()}/${ctxMax.toLocaleString()} (${Math.round(
|
|
2753
|
+
before / ctxMax * 100
|
|
2754
|
+
)}%) \u2014 ${decision.aggressive ? "aggressively folded" : "folded"} ${result.beforeMessages} messages \u2192 ${result.afterMessages} (summary ${result.summaryChars} chars). Continuing.`
|
|
2755
|
+
};
|
|
2624
2756
|
}
|
|
2757
|
+
} else if (decision.kind === "exit-with-summary") {
|
|
2758
|
+
const before = decision.promptTokens;
|
|
2759
|
+
const ctxMax = decision.ctxMax;
|
|
2760
|
+
yield {
|
|
2761
|
+
turn: this._turn,
|
|
2762
|
+
role: "warning",
|
|
2763
|
+
content: `context ${before.toLocaleString()}/${ctxMax.toLocaleString()} (${Math.round(
|
|
2764
|
+
before / ctxMax * 100
|
|
2765
|
+
)}%) \u2014 forcing summary from what was gathered. Run /compact, /clear, or /new to reset.`
|
|
2766
|
+
};
|
|
2767
|
+
this.context.trimTrailingToolCalls();
|
|
2768
|
+
yield* this.forceSummaryAfterIterLimit({ reason: "context-guard" });
|
|
2769
|
+
return;
|
|
2625
2770
|
}
|
|
2626
2771
|
for (const call of repairedCalls) {
|
|
2627
2772
|
const name = call.function?.name ?? "";
|
|
@@ -2653,7 +2798,8 @@ ${reason}`;
|
|
|
2653
2798
|
} else {
|
|
2654
2799
|
result = await this.tools.dispatch(name, args, {
|
|
2655
2800
|
signal,
|
|
2656
|
-
maxResultTokens: DEFAULT_MAX_RESULT_TOKENS
|
|
2801
|
+
maxResultTokens: DEFAULT_MAX_RESULT_TOKENS,
|
|
2802
|
+
confirmationGate: this.confirmationGate
|
|
2657
2803
|
});
|
|
2658
2804
|
const postReport = await runHooks({
|
|
2659
2805
|
hooks: this.hooks,
|
|
@@ -2673,7 +2819,6 @@ ${reason}`;
|
|
|
2673
2819
|
name,
|
|
2674
2820
|
content: result
|
|
2675
2821
|
});
|
|
2676
|
-
this.compactToolCallArgsAfterResponse();
|
|
2677
2822
|
if (this.noteToolFailureSignal(result)) {
|
|
2678
2823
|
yield {
|
|
2679
2824
|
turn: this._turn,
|
|
@@ -2732,7 +2877,6 @@ ${summary}`;
|
|
|
2732
2877
|
stats: summaryStats,
|
|
2733
2878
|
forcedSummary: true
|
|
2734
2879
|
};
|
|
2735
|
-
this.autoCompactToolResultsOnTurnEnd();
|
|
2736
2880
|
yield { turn: this._turn, role: "done", content: summary };
|
|
2737
2881
|
} catch (err) {
|
|
2738
2882
|
const label = errorLabelFor(opts.reason, this.maxToolIters);
|
|
@@ -2742,7 +2886,6 @@ ${summary}`;
|
|
|
2742
2886
|
content: "",
|
|
2743
2887
|
error: `${label} and the fallback summary call failed: ${err.message}. Run /clear and retry with a narrower question, or raise --max-tool-iters.`
|
|
2744
2888
|
};
|
|
2745
|
-
this.autoCompactToolResultsOnTurnEnd();
|
|
2746
2889
|
yield { turn: this._turn, role: "done", content: "" };
|
|
2747
2890
|
}
|
|
2748
2891
|
}
|
|
@@ -2871,56 +3014,6 @@ function shrinkOversizedToolResultsByTokens(messages, maxTokens) {
|
|
|
2871
3014
|
});
|
|
2872
3015
|
return { messages: out, healedCount, tokensSaved, charsSaved };
|
|
2873
3016
|
}
|
|
2874
|
-
function shrinkOversizedToolCallArgsByTokens(messages, maxTokens) {
|
|
2875
|
-
let healedCount = 0;
|
|
2876
|
-
let tokensSaved = 0;
|
|
2877
|
-
let charsSaved = 0;
|
|
2878
|
-
const out = messages.map((msg) => {
|
|
2879
|
-
if (msg.role !== "assistant" || !Array.isArray(msg.tool_calls)) return msg;
|
|
2880
|
-
let changed = false;
|
|
2881
|
-
const newCalls = msg.tool_calls.map((call) => {
|
|
2882
|
-
const args = call.function?.arguments;
|
|
2883
|
-
if (typeof args !== "string" || args.length <= maxTokens) return call;
|
|
2884
|
-
const beforeTokens = countTokens(args);
|
|
2885
|
-
if (beforeTokens <= maxTokens) return call;
|
|
2886
|
-
const shrunk = shrinkJsonLongStrings(args);
|
|
2887
|
-
const afterTokens = countTokens(shrunk);
|
|
2888
|
-
if (afterTokens >= beforeTokens) return call;
|
|
2889
|
-
changed = true;
|
|
2890
|
-
healedCount += 1;
|
|
2891
|
-
tokensSaved += beforeTokens - afterTokens;
|
|
2892
|
-
charsSaved += args.length - shrunk.length;
|
|
2893
|
-
return { ...call, function: { ...call.function, arguments: shrunk } };
|
|
2894
|
-
});
|
|
2895
|
-
if (!changed) return msg;
|
|
2896
|
-
return { ...msg, tool_calls: newCalls };
|
|
2897
|
-
});
|
|
2898
|
-
return { messages: out, healedCount, tokensSaved, charsSaved };
|
|
2899
|
-
}
|
|
2900
|
-
function shrinkJsonLongStrings(jsonStr) {
|
|
2901
|
-
let parsed;
|
|
2902
|
-
try {
|
|
2903
|
-
parsed = JSON.parse(jsonStr);
|
|
2904
|
-
} catch {
|
|
2905
|
-
const head = jsonStr.slice(0, 200);
|
|
2906
|
-
return `${head}\u2026[shrunk: ${jsonStr.length} chars, unparsed]`;
|
|
2907
|
-
}
|
|
2908
|
-
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
2909
|
-
return jsonStr;
|
|
2910
|
-
}
|
|
2911
|
-
const LONG_THRESHOLD = 300;
|
|
2912
|
-
const input = parsed;
|
|
2913
|
-
const output = {};
|
|
2914
|
-
for (const [k, v] of Object.entries(input)) {
|
|
2915
|
-
if (typeof v === "string" && v.length > LONG_THRESHOLD) {
|
|
2916
|
-
const newlines = v.match(/\n/g)?.length ?? 0;
|
|
2917
|
-
output[k] = `[\u2026shrunk: ${v.length} chars, ${newlines} lines \u2014 tool already responded, see result]`;
|
|
2918
|
-
} else {
|
|
2919
|
-
output[k] = v;
|
|
2920
|
-
}
|
|
2921
|
-
}
|
|
2922
|
-
return JSON.stringify(output);
|
|
2923
|
-
}
|
|
2924
3017
|
function fixToolCallPairing(messages) {
|
|
2925
3018
|
const out = [];
|
|
2926
3019
|
let droppedAssistantCalls = 0;
|
|
@@ -4876,7 +4969,7 @@ function registerChoiceTool(registry, opts = {}) {
|
|
|
4876
4969
|
},
|
|
4877
4970
|
required: ["question", "options"]
|
|
4878
4971
|
},
|
|
4879
|
-
fn: async (args) => {
|
|
4972
|
+
fn: async (args, ctx) => {
|
|
4880
4973
|
const question = (args?.question ?? "").trim();
|
|
4881
4974
|
if (!question) {
|
|
4882
4975
|
throw new Error(
|
|
@@ -4896,7 +4989,13 @@ function registerChoiceTool(registry, opts = {}) {
|
|
|
4896
4989
|
}
|
|
4897
4990
|
const allowCustom = args?.allowCustom === true;
|
|
4898
4991
|
opts.onChoiceRequested?.(question, options);
|
|
4899
|
-
|
|
4992
|
+
const verdict = await (ctx?.confirmationGate ?? pauseGate).ask({
|
|
4993
|
+
kind: "choice",
|
|
4994
|
+
payload: { question, options, allowCustom }
|
|
4995
|
+
});
|
|
4996
|
+
if (verdict.type === "pick") return `user picked: ${verdict.optionId}`;
|
|
4997
|
+
if (verdict.type === "text") return `user answered: ${verdict.text}`;
|
|
4998
|
+
return "user cancelled the choice";
|
|
4900
4999
|
}
|
|
4901
5000
|
});
|
|
4902
5001
|
return registry;
|
|
@@ -5013,7 +5112,7 @@ function registerSubmitPlan(registry, opts) {
|
|
|
5013
5112
|
},
|
|
5014
5113
|
required: ["plan"]
|
|
5015
5114
|
},
|
|
5016
|
-
fn: async (args) => {
|
|
5115
|
+
fn: async (args, ctx) => {
|
|
5017
5116
|
const plan = (args?.plan ?? "").trim();
|
|
5018
5117
|
if (!plan) {
|
|
5019
5118
|
throw new Error("submit_plan: empty plan \u2014 write a markdown plan and try again.");
|
|
@@ -5021,7 +5120,13 @@ function registerSubmitPlan(registry, opts) {
|
|
|
5021
5120
|
const steps = sanitizeSteps(args?.steps);
|
|
5022
5121
|
const summary = typeof args?.summary === "string" ? args.summary.trim() || void 0 : void 0;
|
|
5023
5122
|
opts.onPlanSubmitted?.(plan, steps);
|
|
5024
|
-
|
|
5123
|
+
const verdict = await (ctx?.confirmationGate ?? pauseGate).ask({
|
|
5124
|
+
kind: "plan_proposed",
|
|
5125
|
+
payload: { plan, steps, summary }
|
|
5126
|
+
});
|
|
5127
|
+
if (verdict.type === "approve") return "plan approved";
|
|
5128
|
+
if (verdict.type === "refine") throw new Error("user requested refinement");
|
|
5129
|
+
throw new Error("plan cancelled");
|
|
5025
5130
|
}
|
|
5026
5131
|
});
|
|
5027
5132
|
}
|
|
@@ -5052,7 +5157,7 @@ function registerMarkStepComplete(registry, opts) {
|
|
|
5052
5157
|
},
|
|
5053
5158
|
required: ["stepId", "result"]
|
|
5054
5159
|
},
|
|
5055
|
-
fn: async (args) => {
|
|
5160
|
+
fn: async (args, ctx) => {
|
|
5056
5161
|
const stepId = (args?.stepId ?? "").trim();
|
|
5057
5162
|
const result = (args?.result ?? "").trim();
|
|
5058
5163
|
if (!stepId) {
|
|
@@ -5069,7 +5174,16 @@ function registerMarkStepComplete(registry, opts) {
|
|
|
5069
5174
|
if (title) update.title = title;
|
|
5070
5175
|
if (notes) update.notes = notes;
|
|
5071
5176
|
opts.onStepCompleted?.(update);
|
|
5072
|
-
|
|
5177
|
+
const verdict = await (ctx?.confirmationGate ?? pauseGate).ask({
|
|
5178
|
+
kind: "plan_checkpoint",
|
|
5179
|
+
payload: { stepId, title, result, notes }
|
|
5180
|
+
});
|
|
5181
|
+
if (verdict.type === "continue") return JSON.stringify(update);
|
|
5182
|
+
if (verdict.type === "revise") {
|
|
5183
|
+
if (verdict.feedback) return `revision requested: ${verdict.feedback}`;
|
|
5184
|
+
throw new Error("user requested revision at checkpoint");
|
|
5185
|
+
}
|
|
5186
|
+
throw new Error("user stopped at checkpoint");
|
|
5073
5187
|
}
|
|
5074
5188
|
});
|
|
5075
5189
|
}
|
|
@@ -5097,7 +5211,7 @@ function registerRevisePlan(registry, opts) {
|
|
|
5097
5211
|
},
|
|
5098
5212
|
required: ["reason", "remainingSteps"]
|
|
5099
5213
|
},
|
|
5100
|
-
fn: async (args) => {
|
|
5214
|
+
fn: async (args, ctx) => {
|
|
5101
5215
|
const reason = (args?.reason ?? "").trim();
|
|
5102
5216
|
if (!reason) {
|
|
5103
5217
|
throw new Error(
|
|
@@ -5112,7 +5226,13 @@ function registerRevisePlan(registry, opts) {
|
|
|
5112
5226
|
}
|
|
5113
5227
|
const summary = typeof args?.summary === "string" ? args.summary.trim() || void 0 : void 0;
|
|
5114
5228
|
opts.onPlanRevisionProposed?.(reason, remainingSteps, summary);
|
|
5115
|
-
|
|
5229
|
+
const verdict = await (ctx?.confirmationGate ?? pauseGate).ask({
|
|
5230
|
+
kind: "plan_revision",
|
|
5231
|
+
payload: { reason, remainingSteps, summary }
|
|
5232
|
+
});
|
|
5233
|
+
if (verdict.type === "accepted") return "revision accepted";
|
|
5234
|
+
if (verdict.type === "rejected") throw new Error("revision rejected");
|
|
5235
|
+
throw new Error("revision cancelled");
|
|
5116
5236
|
}
|
|
5117
5237
|
});
|
|
5118
5238
|
}
|
|
@@ -5367,9 +5487,63 @@ function forkRegistryExcluding(parent, exclude) {
|
|
|
5367
5487
|
}
|
|
5368
5488
|
|
|
5369
5489
|
// src/tools/shell.ts
|
|
5370
|
-
import { spawn as
|
|
5490
|
+
import { spawn as spawn4, spawnSync } from "child_process";
|
|
5371
5491
|
import { existsSync as existsSync8, statSync as statSync4 } from "fs";
|
|
5372
|
-
import * as
|
|
5492
|
+
import * as pathMod4 from "path";
|
|
5493
|
+
|
|
5494
|
+
// src/config.ts
|
|
5495
|
+
import { chmodSync as chmodSync2, mkdirSync as mkdirSync3, readFileSync as readFileSync9, writeFileSync as writeFileSync3 } from "fs";
|
|
5496
|
+
import { homedir as homedir5 } from "os";
|
|
5497
|
+
import { dirname as dirname4, join as join9 } from "path";
|
|
5498
|
+
function defaultConfigPath() {
|
|
5499
|
+
return join9(homedir5(), ".reasonix", "config.json");
|
|
5500
|
+
}
|
|
5501
|
+
function readConfig(path2 = defaultConfigPath()) {
|
|
5502
|
+
try {
|
|
5503
|
+
const raw = readFileSync9(path2, "utf8");
|
|
5504
|
+
const parsed = JSON.parse(raw);
|
|
5505
|
+
if (parsed && typeof parsed === "object") return parsed;
|
|
5506
|
+
} catch {
|
|
5507
|
+
}
|
|
5508
|
+
return {};
|
|
5509
|
+
}
|
|
5510
|
+
function writeConfig(cfg, path2 = defaultConfigPath()) {
|
|
5511
|
+
mkdirSync3(dirname4(path2), { recursive: true });
|
|
5512
|
+
writeFileSync3(path2, JSON.stringify(cfg, null, 2), "utf8");
|
|
5513
|
+
try {
|
|
5514
|
+
chmodSync2(path2, 384);
|
|
5515
|
+
} catch {
|
|
5516
|
+
}
|
|
5517
|
+
}
|
|
5518
|
+
function loadApiKey(path2 = defaultConfigPath()) {
|
|
5519
|
+
if (process.env.DEEPSEEK_API_KEY) return process.env.DEEPSEEK_API_KEY;
|
|
5520
|
+
return readConfig(path2).apiKey;
|
|
5521
|
+
}
|
|
5522
|
+
function saveApiKey(key, path2 = defaultConfigPath()) {
|
|
5523
|
+
const cfg = readConfig(path2);
|
|
5524
|
+
cfg.apiKey = key.trim();
|
|
5525
|
+
writeConfig(cfg, path2);
|
|
5526
|
+
}
|
|
5527
|
+
function addProjectShellAllowed(rootDir, prefix, path2 = defaultConfigPath()) {
|
|
5528
|
+
const trimmed = prefix.trim();
|
|
5529
|
+
if (!trimmed) return;
|
|
5530
|
+
const cfg = readConfig(path2);
|
|
5531
|
+
if (!cfg.projects) cfg.projects = {};
|
|
5532
|
+
if (!cfg.projects[rootDir]) cfg.projects[rootDir] = {};
|
|
5533
|
+
const existing = cfg.projects[rootDir].shellAllowed ?? [];
|
|
5534
|
+
if (existing.includes(trimmed)) return;
|
|
5535
|
+
cfg.projects[rootDir].shellAllowed = [...existing, trimmed];
|
|
5536
|
+
writeConfig(cfg, path2);
|
|
5537
|
+
}
|
|
5538
|
+
function isPlausibleKey(key) {
|
|
5539
|
+
const trimmed = key.trim();
|
|
5540
|
+
return /^sk-[A-Za-z0-9_-]{16,}$/.test(trimmed);
|
|
5541
|
+
}
|
|
5542
|
+
function redactKey(key) {
|
|
5543
|
+
if (!key) return "";
|
|
5544
|
+
if (key.length <= 12) return "****";
|
|
5545
|
+
return `${key.slice(0, 6)}\u2026${key.slice(-4)}`;
|
|
5546
|
+
}
|
|
5373
5547
|
|
|
5374
5548
|
// src/tools/jobs.ts
|
|
5375
5549
|
import { spawn as spawn2 } from "child_process";
|
|
@@ -5659,6 +5833,417 @@ function snapshot(job) {
|
|
|
5659
5833
|
};
|
|
5660
5834
|
}
|
|
5661
5835
|
|
|
5836
|
+
// src/tools/shell-chain.ts
|
|
5837
|
+
import { spawn as spawn3 } from "child_process";
|
|
5838
|
+
import { closeSync, openSync } from "fs";
|
|
5839
|
+
import * as pathMod3 from "path";
|
|
5840
|
+
var UnsupportedSyntaxError = class extends Error {
|
|
5841
|
+
constructor(detail) {
|
|
5842
|
+
super(`run_command: ${detail}`);
|
|
5843
|
+
this.name = "UnsupportedSyntaxError";
|
|
5844
|
+
}
|
|
5845
|
+
};
|
|
5846
|
+
function splitOnChainOps(cmd) {
|
|
5847
|
+
const segs = [];
|
|
5848
|
+
const ops = [];
|
|
5849
|
+
let segStart = 0;
|
|
5850
|
+
let i = 0;
|
|
5851
|
+
let quote = null;
|
|
5852
|
+
let atTokenStart = true;
|
|
5853
|
+
while (i < cmd.length) {
|
|
5854
|
+
const ch = cmd[i];
|
|
5855
|
+
if (quote) {
|
|
5856
|
+
if (ch === quote) quote = null;
|
|
5857
|
+
else if (ch === "\\" && quote === '"' && i + 1 < cmd.length) i++;
|
|
5858
|
+
i++;
|
|
5859
|
+
atTokenStart = false;
|
|
5860
|
+
continue;
|
|
5861
|
+
}
|
|
5862
|
+
if (ch === '"' || ch === "'") {
|
|
5863
|
+
quote = ch;
|
|
5864
|
+
i++;
|
|
5865
|
+
atTokenStart = false;
|
|
5866
|
+
continue;
|
|
5867
|
+
}
|
|
5868
|
+
if (ch === " " || ch === " ") {
|
|
5869
|
+
i++;
|
|
5870
|
+
atTokenStart = true;
|
|
5871
|
+
continue;
|
|
5872
|
+
}
|
|
5873
|
+
if (atTokenStart) {
|
|
5874
|
+
let op = null;
|
|
5875
|
+
let opLen = 0;
|
|
5876
|
+
const next = cmd[i + 1];
|
|
5877
|
+
if (ch === "|" && next === "|") {
|
|
5878
|
+
op = "||";
|
|
5879
|
+
opLen = 2;
|
|
5880
|
+
} else if (ch === "&" && next === "&") {
|
|
5881
|
+
op = "&&";
|
|
5882
|
+
opLen = 2;
|
|
5883
|
+
} else if (ch === "|") {
|
|
5884
|
+
op = "|";
|
|
5885
|
+
opLen = 1;
|
|
5886
|
+
} else if (ch === ";") {
|
|
5887
|
+
op = ";";
|
|
5888
|
+
opLen = 1;
|
|
5889
|
+
}
|
|
5890
|
+
if (op !== null) {
|
|
5891
|
+
segs.push(cmd.slice(segStart, i));
|
|
5892
|
+
ops.push(op);
|
|
5893
|
+
i += opLen;
|
|
5894
|
+
segStart = i;
|
|
5895
|
+
atTokenStart = true;
|
|
5896
|
+
continue;
|
|
5897
|
+
}
|
|
5898
|
+
}
|
|
5899
|
+
i++;
|
|
5900
|
+
atTokenStart = false;
|
|
5901
|
+
}
|
|
5902
|
+
segs.push(cmd.slice(segStart));
|
|
5903
|
+
return { segs, ops };
|
|
5904
|
+
}
|
|
5905
|
+
function parseSegment(segStr) {
|
|
5906
|
+
const argv = [];
|
|
5907
|
+
const redirects = [];
|
|
5908
|
+
let cur = "";
|
|
5909
|
+
let curHasContent = false;
|
|
5910
|
+
let pending = null;
|
|
5911
|
+
let quote = null;
|
|
5912
|
+
const flush = () => {
|
|
5913
|
+
if (!curHasContent && cur.length === 0) return;
|
|
5914
|
+
if (pending) {
|
|
5915
|
+
redirects.push({ kind: pending, target: cur });
|
|
5916
|
+
pending = null;
|
|
5917
|
+
} else {
|
|
5918
|
+
argv.push(cur);
|
|
5919
|
+
}
|
|
5920
|
+
cur = "";
|
|
5921
|
+
curHasContent = false;
|
|
5922
|
+
};
|
|
5923
|
+
let i = 0;
|
|
5924
|
+
while (i < segStr.length) {
|
|
5925
|
+
const ch = segStr[i];
|
|
5926
|
+
if (quote) {
|
|
5927
|
+
if (ch === quote) {
|
|
5928
|
+
quote = null;
|
|
5929
|
+
} else if (ch === "\\" && quote === '"' && i + 1 < segStr.length) {
|
|
5930
|
+
cur += segStr[++i] ?? "";
|
|
5931
|
+
curHasContent = true;
|
|
5932
|
+
} else {
|
|
5933
|
+
cur += ch;
|
|
5934
|
+
curHasContent = true;
|
|
5935
|
+
}
|
|
5936
|
+
i++;
|
|
5937
|
+
continue;
|
|
5938
|
+
}
|
|
5939
|
+
if (ch === '"' || ch === "'") {
|
|
5940
|
+
quote = ch;
|
|
5941
|
+
curHasContent = true;
|
|
5942
|
+
i++;
|
|
5943
|
+
continue;
|
|
5944
|
+
}
|
|
5945
|
+
if (ch === " " || ch === " ") {
|
|
5946
|
+
flush();
|
|
5947
|
+
i++;
|
|
5948
|
+
continue;
|
|
5949
|
+
}
|
|
5950
|
+
if (cur.length === 0 && !curHasContent) {
|
|
5951
|
+
const remaining = segStr.slice(i);
|
|
5952
|
+
let matched = null;
|
|
5953
|
+
if (remaining.startsWith("2>&1")) matched = { op: "2>&1", len: 4 };
|
|
5954
|
+
else if (remaining.startsWith("&>")) matched = { op: "&>", len: 2 };
|
|
5955
|
+
else if (remaining.startsWith("2>>")) matched = { op: "2>>", len: 3 };
|
|
5956
|
+
else if (remaining.startsWith("2>")) matched = { op: "2>", len: 2 };
|
|
5957
|
+
else if (remaining.startsWith(">>")) matched = { op: ">>", len: 2 };
|
|
5958
|
+
else if (remaining.startsWith(">")) matched = { op: ">", len: 1 };
|
|
5959
|
+
else if (remaining.startsWith("<<")) {
|
|
5960
|
+
throw new UnsupportedSyntaxError(
|
|
5961
|
+
`shell operator "<<" is not supported \u2014 heredoc / here-string is not implemented; pass input via a "<" file or the binary's --input flag`
|
|
5962
|
+
);
|
|
5963
|
+
} else if (remaining.startsWith("<")) matched = { op: "<", len: 1 };
|
|
5964
|
+
if (matched) {
|
|
5965
|
+
if (pending !== null) {
|
|
5966
|
+
throw new UnsupportedSyntaxError(
|
|
5967
|
+
`redirect "${pending}" is missing a target file before "${matched.op}"`
|
|
5968
|
+
);
|
|
5969
|
+
}
|
|
5970
|
+
if (matched.op === "2>&1") {
|
|
5971
|
+
redirects.push({ kind: "2>&1", target: "" });
|
|
5972
|
+
} else {
|
|
5973
|
+
pending = matched.op;
|
|
5974
|
+
}
|
|
5975
|
+
i += matched.len;
|
|
5976
|
+
continue;
|
|
5977
|
+
}
|
|
5978
|
+
if (ch === "&") {
|
|
5979
|
+
throw new UnsupportedSyntaxError(
|
|
5980
|
+
'shell operator "&" is not supported \u2014 background runs need run_background, not run_command. Wrap a literal `&` arg in quotes.'
|
|
5981
|
+
);
|
|
5982
|
+
}
|
|
5983
|
+
}
|
|
5984
|
+
cur += ch;
|
|
5985
|
+
curHasContent = true;
|
|
5986
|
+
i++;
|
|
5987
|
+
}
|
|
5988
|
+
if (quote) throw new Error(`unclosed ${quote} in command`);
|
|
5989
|
+
flush();
|
|
5990
|
+
if (pending) throw new UnsupportedSyntaxError(`redirect "${pending}" is missing a target file`);
|
|
5991
|
+
if (argv.length === 0 && redirects.length > 0) {
|
|
5992
|
+
throw new UnsupportedSyntaxError(
|
|
5993
|
+
"redirect without a command \u2014 segment must have at least one program argument"
|
|
5994
|
+
);
|
|
5995
|
+
}
|
|
5996
|
+
validateRedirectFds(redirects);
|
|
5997
|
+
return { argv, redirects };
|
|
5998
|
+
}
|
|
5999
|
+
function validateRedirectFds(redirects) {
|
|
6000
|
+
let stdin = 0;
|
|
6001
|
+
let stdout = 0;
|
|
6002
|
+
let stderr = 0;
|
|
6003
|
+
for (const r of redirects) {
|
|
6004
|
+
if (r.kind === "<") stdin++;
|
|
6005
|
+
else if (r.kind === ">" || r.kind === ">>") stdout++;
|
|
6006
|
+
else if (r.kind === "2>" || r.kind === "2>>" || r.kind === "2>&1") stderr++;
|
|
6007
|
+
else if (r.kind === "&>") {
|
|
6008
|
+
stdout++;
|
|
6009
|
+
stderr++;
|
|
6010
|
+
}
|
|
6011
|
+
}
|
|
6012
|
+
if (stdin > 1) throw new UnsupportedSyntaxError("multiple `<` stdin redirects in one segment");
|
|
6013
|
+
if (stdout > 1)
|
|
6014
|
+
throw new UnsupportedSyntaxError(
|
|
6015
|
+
"multiple stdout redirects in one segment (`>` / `>>` / `&>` conflict)"
|
|
6016
|
+
);
|
|
6017
|
+
if (stderr > 1)
|
|
6018
|
+
throw new UnsupportedSyntaxError(
|
|
6019
|
+
"multiple stderr redirects in one segment (`2>` / `2>>` / `&>` / `2>&1` conflict)"
|
|
6020
|
+
);
|
|
6021
|
+
}
|
|
6022
|
+
function parseCommandChain(cmd) {
|
|
6023
|
+
const { segs, ops } = splitOnChainOps(cmd);
|
|
6024
|
+
const segments = [];
|
|
6025
|
+
for (let i = 0; i < segs.length; i++) {
|
|
6026
|
+
const trimmed = segs[i].trim();
|
|
6027
|
+
if (trimmed.length === 0) {
|
|
6028
|
+
const op = i === 0 ? ops[0] : ops[i - 1];
|
|
6029
|
+
throw new UnsupportedSyntaxError(
|
|
6030
|
+
i === 0 ? `empty segment before "${op}"` : i === segs.length - 1 ? `chain ends with "${op}"` : `empty segment between "${ops[i - 1]}" and "${ops[i]}"`
|
|
6031
|
+
);
|
|
6032
|
+
}
|
|
6033
|
+
segments.push(parseSegment(trimmed));
|
|
6034
|
+
}
|
|
6035
|
+
if (ops.length === 0 && segments[0].redirects.length === 0) return null;
|
|
6036
|
+
return { segments, ops };
|
|
6037
|
+
}
|
|
6038
|
+
function chainAllowed(chain, isAllowed2) {
|
|
6039
|
+
for (const seg of chain.segments) {
|
|
6040
|
+
if (!isAllowed2(seg.argv.join(" "))) return false;
|
|
6041
|
+
}
|
|
6042
|
+
return true;
|
|
6043
|
+
}
|
|
6044
|
+
function groupChain(chain) {
|
|
6045
|
+
const groups = [{ segments: [chain.segments[0]], opBefore: null }];
|
|
6046
|
+
for (let i = 0; i < chain.ops.length; i++) {
|
|
6047
|
+
const op = chain.ops[i];
|
|
6048
|
+
const next = chain.segments[i + 1];
|
|
6049
|
+
if (op === "|") {
|
|
6050
|
+
groups[groups.length - 1].segments.push(next);
|
|
6051
|
+
} else {
|
|
6052
|
+
groups.push({ segments: [next], opBefore: op });
|
|
6053
|
+
}
|
|
6054
|
+
}
|
|
6055
|
+
return groups;
|
|
6056
|
+
}
|
|
6057
|
+
async function runChain(chain, opts) {
|
|
6058
|
+
const groups = groupChain(chain);
|
|
6059
|
+
const buf = new OutputBuffer(opts.maxOutputChars * 2 * 4);
|
|
6060
|
+
const deadline = Date.now() + opts.timeoutSec * 1e3;
|
|
6061
|
+
let lastExit = 0;
|
|
6062
|
+
let timedOut = false;
|
|
6063
|
+
for (const group of groups) {
|
|
6064
|
+
if (group.opBefore === "&&" && lastExit !== 0) continue;
|
|
6065
|
+
if (group.opBefore === "||" && lastExit === 0) continue;
|
|
6066
|
+
const remainingMs = deadline - Date.now();
|
|
6067
|
+
if (remainingMs <= 0) {
|
|
6068
|
+
timedOut = true;
|
|
6069
|
+
break;
|
|
6070
|
+
}
|
|
6071
|
+
const result = await runPipeGroup(group.segments, {
|
|
6072
|
+
cwd: opts.cwd,
|
|
6073
|
+
timeoutMs: remainingMs,
|
|
6074
|
+
buf,
|
|
6075
|
+
signal: opts.signal
|
|
6076
|
+
});
|
|
6077
|
+
lastExit = result.exitCode;
|
|
6078
|
+
if (result.timedOut) {
|
|
6079
|
+
timedOut = true;
|
|
6080
|
+
break;
|
|
6081
|
+
}
|
|
6082
|
+
if (opts.signal?.aborted) break;
|
|
6083
|
+
}
|
|
6084
|
+
const output = buf.toString();
|
|
6085
|
+
const truncated = output.length > opts.maxOutputChars ? `${output.slice(0, opts.maxOutputChars)}
|
|
6086
|
+
|
|
6087
|
+
[\u2026 truncated ${output.length - opts.maxOutputChars} chars \u2026]` : output;
|
|
6088
|
+
return { exitCode: lastExit, output: truncated, timedOut };
|
|
6089
|
+
}
|
|
6090
|
+
function openRedirects(redirects, cwd) {
|
|
6091
|
+
let stdinFd = null;
|
|
6092
|
+
let stdoutFd = null;
|
|
6093
|
+
let stderrFd = null;
|
|
6094
|
+
let mergeStderrToStdout = false;
|
|
6095
|
+
let bothFd = null;
|
|
6096
|
+
const toClose = [];
|
|
6097
|
+
const open = (target, flags) => {
|
|
6098
|
+
const resolved = pathMod3.resolve(cwd, target);
|
|
6099
|
+
const fd = openSync(resolved, flags);
|
|
6100
|
+
toClose.push(fd);
|
|
6101
|
+
return fd;
|
|
6102
|
+
};
|
|
6103
|
+
for (const r of redirects) {
|
|
6104
|
+
if (r.kind === "<") stdinFd = open(r.target, "r");
|
|
6105
|
+
else if (r.kind === ">") stdoutFd = open(r.target, "w");
|
|
6106
|
+
else if (r.kind === ">>") stdoutFd = open(r.target, "a");
|
|
6107
|
+
else if (r.kind === "2>") stderrFd = open(r.target, "w");
|
|
6108
|
+
else if (r.kind === "2>>") stderrFd = open(r.target, "a");
|
|
6109
|
+
else if (r.kind === "&>") {
|
|
6110
|
+
bothFd = open(r.target, "w");
|
|
6111
|
+
stdoutFd = bothFd;
|
|
6112
|
+
stderrFd = bothFd;
|
|
6113
|
+
} else if (r.kind === "2>&1") {
|
|
6114
|
+
mergeStderrToStdout = true;
|
|
6115
|
+
}
|
|
6116
|
+
}
|
|
6117
|
+
return { stdinFd, stdoutFd, stderrFd, mergeStderrToStdout, toClose };
|
|
6118
|
+
}
|
|
6119
|
+
async function runPipeGroup(segments, opts) {
|
|
6120
|
+
const env = { ...process.env, PYTHONIOENCODING: "utf-8", PYTHONUTF8: "1" };
|
|
6121
|
+
const children = [];
|
|
6122
|
+
const allFds = [];
|
|
6123
|
+
let timedOut = false;
|
|
6124
|
+
const killAll = () => {
|
|
6125
|
+
for (const c of children) killProcessTree2(c);
|
|
6126
|
+
};
|
|
6127
|
+
const killTimer = setTimeout(() => {
|
|
6128
|
+
timedOut = true;
|
|
6129
|
+
killAll();
|
|
6130
|
+
}, opts.timeoutMs);
|
|
6131
|
+
const onAbort = () => killAll();
|
|
6132
|
+
if (opts.signal?.aborted) {
|
|
6133
|
+
onAbort();
|
|
6134
|
+
} else {
|
|
6135
|
+
opts.signal?.addEventListener("abort", onAbort, { once: true });
|
|
6136
|
+
}
|
|
6137
|
+
try {
|
|
6138
|
+
for (let i = 0; i < segments.length; i++) {
|
|
6139
|
+
const isFirst = i === 0;
|
|
6140
|
+
const isLast = i === segments.length - 1;
|
|
6141
|
+
const seg = segments[i];
|
|
6142
|
+
const io = openRedirects(seg.redirects, opts.cwd);
|
|
6143
|
+
allFds.push(...io.toClose);
|
|
6144
|
+
const { bin, args, spawnOverrides } = prepareSpawn(seg.argv);
|
|
6145
|
+
const stdoutSpec = io.stdoutFd !== null ? io.stdoutFd : "pipe";
|
|
6146
|
+
const stderrSpec = io.stderrFd !== null ? io.stderrFd : io.mergeStderrToStdout ? stdoutSpec : "pipe";
|
|
6147
|
+
const stdinSpec = io.stdinFd !== null ? io.stdinFd : isFirst ? "ignore" : "pipe";
|
|
6148
|
+
const spawnOpts = {
|
|
6149
|
+
cwd: opts.cwd,
|
|
6150
|
+
shell: false,
|
|
6151
|
+
windowsHide: true,
|
|
6152
|
+
env,
|
|
6153
|
+
stdio: [stdinSpec, stdoutSpec, stderrSpec],
|
|
6154
|
+
...spawnOverrides
|
|
6155
|
+
};
|
|
6156
|
+
let child;
|
|
6157
|
+
try {
|
|
6158
|
+
child = spawn3(bin, args, spawnOpts);
|
|
6159
|
+
} catch (err) {
|
|
6160
|
+
for (const fd of allFds) tryClose(fd);
|
|
6161
|
+
killAll();
|
|
6162
|
+
clearTimeout(killTimer);
|
|
6163
|
+
opts.signal?.removeEventListener("abort", onAbort);
|
|
6164
|
+
throw err;
|
|
6165
|
+
}
|
|
6166
|
+
children.push(child);
|
|
6167
|
+
if (!isFirst && io.stdinFd === null) {
|
|
6168
|
+
const prev = children[i - 1];
|
|
6169
|
+
prev.stdout?.on("error", () => {
|
|
6170
|
+
});
|
|
6171
|
+
child.stdin?.on("error", () => {
|
|
6172
|
+
});
|
|
6173
|
+
const prevMergesStderr = segments[i - 1].redirects.some((r) => r.kind === "2>&1") && !!prev.stderr;
|
|
6174
|
+
if (prevMergesStderr && prev.stderr) {
|
|
6175
|
+
prev.stderr.on("error", () => {
|
|
6176
|
+
});
|
|
6177
|
+
let openSources = 2;
|
|
6178
|
+
const closeIfDone = () => {
|
|
6179
|
+
if (--openSources === 0) child.stdin?.end();
|
|
6180
|
+
};
|
|
6181
|
+
prev.stdout?.pipe(child.stdin, { end: false });
|
|
6182
|
+
prev.stderr.pipe(child.stdin, { end: false });
|
|
6183
|
+
prev.stdout?.once("end", closeIfDone);
|
|
6184
|
+
prev.stderr.once("end", closeIfDone);
|
|
6185
|
+
} else {
|
|
6186
|
+
prev.stdout?.pipe(child.stdin);
|
|
6187
|
+
}
|
|
6188
|
+
}
|
|
6189
|
+
if (child.stderr && io.stderrFd === null && !(io.mergeStderrToStdout && !isLast)) {
|
|
6190
|
+
child.stderr.on("data", (chunk) => opts.buf.push(toBuf(chunk)));
|
|
6191
|
+
}
|
|
6192
|
+
if (isLast && child.stdout && io.stdoutFd === null) {
|
|
6193
|
+
child.stdout.on("data", (chunk) => opts.buf.push(toBuf(chunk)));
|
|
6194
|
+
if (io.mergeStderrToStdout && child.stderr && io.stderrFd === null) {
|
|
6195
|
+
child.stderr.removeAllListeners("data");
|
|
6196
|
+
child.stderr.on("data", (chunk) => opts.buf.push(toBuf(chunk)));
|
|
6197
|
+
}
|
|
6198
|
+
}
|
|
6199
|
+
}
|
|
6200
|
+
const exits = await Promise.all(
|
|
6201
|
+
children.map(
|
|
6202
|
+
(c) => new Promise((resolve10) => {
|
|
6203
|
+
c.once("error", () => resolve10(null));
|
|
6204
|
+
c.once("close", (code) => resolve10(code));
|
|
6205
|
+
})
|
|
6206
|
+
)
|
|
6207
|
+
);
|
|
6208
|
+
return { exitCode: exits[exits.length - 1] ?? null, timedOut };
|
|
6209
|
+
} finally {
|
|
6210
|
+
for (const fd of allFds) tryClose(fd);
|
|
6211
|
+
clearTimeout(killTimer);
|
|
6212
|
+
opts.signal?.removeEventListener("abort", onAbort);
|
|
6213
|
+
}
|
|
6214
|
+
}
|
|
6215
|
+
function tryClose(fd) {
|
|
6216
|
+
try {
|
|
6217
|
+
closeSync(fd);
|
|
6218
|
+
} catch {
|
|
6219
|
+
}
|
|
6220
|
+
}
|
|
6221
|
+
function toBuf(chunk) {
|
|
6222
|
+
return typeof chunk === "string" ? Buffer.from(chunk) : chunk;
|
|
6223
|
+
}
|
|
6224
|
+
var OutputBuffer = class {
|
|
6225
|
+
constructor(cap) {
|
|
6226
|
+
this.cap = cap;
|
|
6227
|
+
}
|
|
6228
|
+
cap;
|
|
6229
|
+
chunks = [];
|
|
6230
|
+
bytes = 0;
|
|
6231
|
+
push(b) {
|
|
6232
|
+
if (this.bytes >= this.cap) return;
|
|
6233
|
+
const remaining = this.cap - this.bytes;
|
|
6234
|
+
if (b.length > remaining) {
|
|
6235
|
+
this.chunks.push(b.subarray(0, remaining));
|
|
6236
|
+
this.bytes = this.cap;
|
|
6237
|
+
} else {
|
|
6238
|
+
this.chunks.push(b);
|
|
6239
|
+
this.bytes += b.length;
|
|
6240
|
+
}
|
|
6241
|
+
}
|
|
6242
|
+
toString() {
|
|
6243
|
+
return smartDecodeOutput(Buffer.concat(this.chunks));
|
|
6244
|
+
}
|
|
6245
|
+
};
|
|
6246
|
+
|
|
5662
6247
|
// src/tools/shell.ts
|
|
5663
6248
|
function killProcessTree2(child) {
|
|
5664
6249
|
if (!child.pid || child.killed) return;
|
|
@@ -5830,17 +6415,31 @@ function isAllowed(cmd, extra = []) {
|
|
|
5830
6415
|
}
|
|
5831
6416
|
return false;
|
|
5832
6417
|
}
|
|
6418
|
+
function isCommandAllowed(cmd, extra = []) {
|
|
6419
|
+
let chain;
|
|
6420
|
+
try {
|
|
6421
|
+
chain = parseCommandChain(cmd);
|
|
6422
|
+
} catch {
|
|
6423
|
+
return false;
|
|
6424
|
+
}
|
|
6425
|
+
if (chain === null) return isAllowed(cmd, extra);
|
|
6426
|
+
return chainAllowed(chain, (seg) => isAllowed(seg, extra));
|
|
6427
|
+
}
|
|
5833
6428
|
async function runCommand(cmd, opts) {
|
|
6429
|
+
const timeoutSec = opts.timeoutSec ?? DEFAULT_TIMEOUT_SEC;
|
|
6430
|
+
const maxChars = opts.maxOutputChars ?? DEFAULT_MAX_OUTPUT_CHARS;
|
|
5834
6431
|
const argv = tokenizeCommand(cmd);
|
|
5835
6432
|
if (argv.length === 0) throw new Error("run_command: empty command");
|
|
5836
|
-
const
|
|
5837
|
-
if (
|
|
5838
|
-
|
|
5839
|
-
|
|
5840
|
-
|
|
6433
|
+
const chain = parseCommandChain(cmd);
|
|
6434
|
+
if (chain !== null) {
|
|
6435
|
+
return await runChain(chain, {
|
|
6436
|
+
cwd: opts.cwd,
|
|
6437
|
+
timeoutSec,
|
|
6438
|
+
maxOutputChars: maxChars,
|
|
6439
|
+
signal: opts.signal
|
|
6440
|
+
});
|
|
5841
6441
|
}
|
|
5842
|
-
const timeoutMs =
|
|
5843
|
-
const maxChars = opts.maxOutputChars ?? DEFAULT_MAX_OUTPUT_CHARS;
|
|
6442
|
+
const timeoutMs = timeoutSec * 1e3;
|
|
5844
6443
|
const spawnOpts = {
|
|
5845
6444
|
cwd: opts.cwd,
|
|
5846
6445
|
shell: false,
|
|
@@ -5858,10 +6457,10 @@ async function runCommand(cmd, opts) {
|
|
|
5858
6457
|
};
|
|
5859
6458
|
const { bin, args, spawnOverrides } = prepareSpawn(argv);
|
|
5860
6459
|
const effectiveSpawnOpts = { ...spawnOpts, ...spawnOverrides };
|
|
5861
|
-
return await new Promise((
|
|
6460
|
+
return await new Promise((resolve10, reject) => {
|
|
5862
6461
|
let child;
|
|
5863
6462
|
try {
|
|
5864
|
-
child =
|
|
6463
|
+
child = spawn4(bin, args, effectiveSpawnOpts);
|
|
5865
6464
|
} catch (err) {
|
|
5866
6465
|
reject(err);
|
|
5867
6466
|
return;
|
|
@@ -5912,7 +6511,7 @@ async function runCommand(cmd, opts) {
|
|
|
5912
6511
|
const output = buf.length > maxChars ? `${buf.slice(0, maxChars)}
|
|
5913
6512
|
|
|
5914
6513
|
[\u2026 truncated ${buf.length - maxChars} chars \u2026]` : buf;
|
|
5915
|
-
|
|
6514
|
+
resolve10({ exitCode: code, output, timedOut });
|
|
5916
6515
|
});
|
|
5917
6516
|
});
|
|
5918
6517
|
}
|
|
@@ -5934,16 +6533,16 @@ function resolveExecutable(cmd, opts = {}) {
|
|
|
5934
6533
|
const platform = opts.platform ?? process.platform;
|
|
5935
6534
|
if (platform !== "win32") return cmd;
|
|
5936
6535
|
if (!cmd) return cmd;
|
|
5937
|
-
if (cmd.includes("/") || cmd.includes("\\") ||
|
|
5938
|
-
if (
|
|
6536
|
+
if (cmd.includes("/") || cmd.includes("\\") || pathMod4.isAbsolute(cmd)) return cmd;
|
|
6537
|
+
if (pathMod4.extname(cmd)) return cmd;
|
|
5939
6538
|
const env = opts.env ?? process.env;
|
|
5940
6539
|
const pathExt = (env.PATHEXT ?? ".COM;.EXE;.BAT;.CMD").split(";").map((e) => e.trim()).filter(Boolean);
|
|
5941
|
-
const delimiter2 = opts.pathDelimiter ?? (platform === "win32" ? ";" :
|
|
6540
|
+
const delimiter2 = opts.pathDelimiter ?? (platform === "win32" ? ";" : pathMod4.delimiter);
|
|
5942
6541
|
const pathDirs = (env.PATH ?? "").split(delimiter2).filter(Boolean);
|
|
5943
6542
|
const isFile = opts.isFile ?? defaultIsFile;
|
|
5944
6543
|
for (const dir of pathDirs) {
|
|
5945
6544
|
for (const ext of pathExt) {
|
|
5946
|
-
const full =
|
|
6545
|
+
const full = pathMod4.win32.join(dir, cmd + ext);
|
|
5947
6546
|
if (isFile(full)) return full;
|
|
5948
6547
|
}
|
|
5949
6548
|
}
|
|
@@ -6013,8 +6612,8 @@ function withUtf8Codepage(cmdline) {
|
|
|
6013
6612
|
function isBareWindowsName(s) {
|
|
6014
6613
|
if (!s) return false;
|
|
6015
6614
|
if (s.includes("/") || s.includes("\\")) return false;
|
|
6016
|
-
if (
|
|
6017
|
-
if (
|
|
6615
|
+
if (pathMod4.isAbsolute(s)) return false;
|
|
6616
|
+
if (pathMod4.extname(s)) return false;
|
|
6018
6617
|
return true;
|
|
6019
6618
|
}
|
|
6020
6619
|
function quoteForCmdExe(arg) {
|
|
@@ -6033,7 +6632,7 @@ var NeedsConfirmationError = class extends Error {
|
|
|
6033
6632
|
}
|
|
6034
6633
|
};
|
|
6035
6634
|
function registerShellTools(registry, opts) {
|
|
6036
|
-
const rootDir =
|
|
6635
|
+
const rootDir = pathMod4.resolve(opts.rootDir);
|
|
6037
6636
|
const timeoutSec = opts.timeoutSec ?? DEFAULT_TIMEOUT_SEC;
|
|
6038
6637
|
const maxOutputChars = opts.maxOutputChars ?? DEFAULT_MAX_OUTPUT_CHARS;
|
|
6039
6638
|
const jobs = opts.jobs ?? new JobRegistry();
|
|
@@ -6044,7 +6643,7 @@ function registerShellTools(registry, opts) {
|
|
|
6044
6643
|
const isAllowAll = typeof opts.allowAll === "function" ? opts.allowAll : () => opts.allowAll === true;
|
|
6045
6644
|
registry.register({
|
|
6046
6645
|
name: "run_command",
|
|
6047
|
-
description: "Run a shell command in the project root and return its combined stdout+stderr.\n\nConstraints (read these before the first call):\n\u2022
|
|
6646
|
+
description: "Run a shell command in the project root and return its combined stdout+stderr.\n\nConstraints (read these before the first call):\n\u2022 Chain operators `|`, `||`, `&&`, `;` ARE supported \u2014 parsed natively, no shell invoked, so semantics are identical on Windows / macOS / Linux. Each chain segment is allowlist-checked individually: `git status | grep main` runs if both halves are allowed.\n\u2022 File redirects ARE supported: `>` truncate, `>>` append, `<` stdin from file, `2>` / `2>>` stderr to file, `2>&1` merge stderr\u2192stdout, `&>` both to file. Targets resolve relative to the project root. At most one redirect per fd per segment.\n\u2022 Background `&`, heredoc `<<`, command substitution `$(\u2026)`, subshells `(\u2026)`, and process substitution `<(\u2026)` are NOT supported. Wrap a literal `&` arg in quotes; for input use a `<` file or the binary's own --input flag.\n\u2022 Env-var expansion `$VAR` is NOT performed \u2014 `$VAR` is passed as a literal string. Use the binary's own --env flag or substitute the value yourself.\n\u2022 `cd` DOES NOT PERSIST between calls \u2014 each call spawns a fresh process rooted at the project. If a tool needs a subdirectory, pass it via the tool's own flag (`npm --prefix`, `cargo -C`, `git -C`, `pytest tests/\u2026`), NOT via a preceding `cd`.\n\u2022 Glob patterns (`*.ts`) are passed through as literal arguments \u2014 no shell expansion. Use `grep -r`, `rg`, `find -name`, etc.\n\u2022 Avoid commands with unbounded output (`netstat -ano`, `find /`, etc.) \u2014 they waste tokens. Filter at source: `netstat -ano -p TCP`, `find src -name '*.ts'`, `grep -c`, `wc -l`.\n\nCommon read-only inspection and test/lint/typecheck commands run immediately; anything that could mutate state, install dependencies, or touch the network is refused until the user confirms it in the TUI. Prefer this over asking the user to run a command manually \u2014 after edits, run the project's tests to verify.",
|
|
6048
6647
|
// Plan-mode gate: allow allowlisted commands through (git status,
|
|
6049
6648
|
// cargo check, ls, grep …) so the model can actually investigate
|
|
6050
6649
|
// during planning. Anything that would otherwise trigger a
|
|
@@ -6053,14 +6652,14 @@ function registerShellTools(registry, opts) {
|
|
|
6053
6652
|
if (isAllowAll()) return true;
|
|
6054
6653
|
const cmd = typeof args?.command === "string" ? args.command.trim() : "";
|
|
6055
6654
|
if (!cmd) return false;
|
|
6056
|
-
return
|
|
6655
|
+
return isCommandAllowed(cmd, getExtraAllowed());
|
|
6057
6656
|
},
|
|
6058
6657
|
parameters: {
|
|
6059
6658
|
type: "object",
|
|
6060
6659
|
properties: {
|
|
6061
6660
|
command: {
|
|
6062
6661
|
type: "string",
|
|
6063
|
-
description: 'Full command line.
|
|
6662
|
+
description: 'Full command line. POSIX-ish quoting. Chain operators `|`, `||`, `&&`, `;` and file redirects `>` / `>>` / `<` / `2>` / `2>>` / `2>&1` / `&>` work natively (no shell). Background `&`, heredoc `<<`, env-var expansion `$VAR`, and command substitution `$(\u2026)` are rejected (or passed through as literal in the case of `$VAR`). To pass an operator character as a literal argument (e.g. a regex), wrap it in quotes: `grep "a|b" file.txt`.'
|
|
6064
6663
|
},
|
|
6065
6664
|
timeoutSec: {
|
|
6066
6665
|
type: "integer",
|
|
@@ -6072,8 +6671,17 @@ function registerShellTools(registry, opts) {
|
|
|
6072
6671
|
fn: async (args, ctx) => {
|
|
6073
6672
|
const cmd = args.command.trim();
|
|
6074
6673
|
if (!cmd) throw new Error("run_command: empty command");
|
|
6075
|
-
if (!isAllowAll() && !
|
|
6076
|
-
|
|
6674
|
+
if (!isAllowAll() && !isCommandAllowed(cmd, getExtraAllowed())) {
|
|
6675
|
+
const gate = ctx?.confirmationGate ?? pauseGate;
|
|
6676
|
+
const choice = await gate.ask({ kind: "run_command", payload: { command: cmd } });
|
|
6677
|
+
if (choice.type === "deny") {
|
|
6678
|
+
throw new Error(
|
|
6679
|
+
`user denied: ${cmd}${choice.denyContext ? ` \u2014 ${choice.denyContext}` : ""}`
|
|
6680
|
+
);
|
|
6681
|
+
}
|
|
6682
|
+
if (choice.type === "always_allow") {
|
|
6683
|
+
addProjectShellAllowed(rootDir, choice.prefix);
|
|
6684
|
+
}
|
|
6077
6685
|
}
|
|
6078
6686
|
const effectiveTimeout = Math.max(1, Math.min(600, args.timeoutSec ?? timeoutSec));
|
|
6079
6687
|
const result = await runCommand(cmd, {
|
|
@@ -6105,8 +6713,17 @@ function registerShellTools(registry, opts) {
|
|
|
6105
6713
|
fn: async (args, ctx) => {
|
|
6106
6714
|
const cmd = args.command.trim();
|
|
6107
6715
|
if (!cmd) throw new Error("run_background: empty command");
|
|
6108
|
-
if (!isAllowAll() && !
|
|
6109
|
-
|
|
6716
|
+
if (!isAllowAll() && !isCommandAllowed(cmd, getExtraAllowed())) {
|
|
6717
|
+
const gate = ctx?.confirmationGate ?? pauseGate;
|
|
6718
|
+
const choice = await gate.ask({ kind: "run_background", payload: { command: cmd } });
|
|
6719
|
+
if (choice.type === "deny") {
|
|
6720
|
+
throw new Error(
|
|
6721
|
+
`user denied: ${cmd}${choice.denyContext ? ` \u2014 ${choice.denyContext}` : ""}`
|
|
6722
|
+
);
|
|
6723
|
+
}
|
|
6724
|
+
if (choice.type === "always_allow") {
|
|
6725
|
+
addProjectShellAllowed(rootDir, choice.prefix);
|
|
6726
|
+
}
|
|
6110
6727
|
}
|
|
6111
6728
|
const result = await jobs.start(cmd, {
|
|
6112
6729
|
cwd: rootDir,
|
|
@@ -6435,12 +7052,12 @@ ${i + 1}. ${r.title}`);
|
|
|
6435
7052
|
}
|
|
6436
7053
|
|
|
6437
7054
|
// src/env.ts
|
|
6438
|
-
import { readFileSync as
|
|
6439
|
-
import { resolve as
|
|
7055
|
+
import { readFileSync as readFileSync10 } from "fs";
|
|
7056
|
+
import { resolve as resolve8 } from "path";
|
|
6440
7057
|
function loadDotenv(path2 = ".env") {
|
|
6441
7058
|
let raw;
|
|
6442
7059
|
try {
|
|
6443
|
-
raw =
|
|
7060
|
+
raw = readFileSync10(resolve8(process.cwd(), path2), "utf8");
|
|
6444
7061
|
} catch {
|
|
6445
7062
|
return;
|
|
6446
7063
|
}
|
|
@@ -6459,7 +7076,7 @@ function loadDotenv(path2 = ".env") {
|
|
|
6459
7076
|
}
|
|
6460
7077
|
|
|
6461
7078
|
// src/transcript/log.ts
|
|
6462
|
-
import { createWriteStream, readFileSync as
|
|
7079
|
+
import { createWriteStream, readFileSync as readFileSync11 } from "fs";
|
|
6463
7080
|
function recordFromLoopEvent(ev, extra) {
|
|
6464
7081
|
const rec = {
|
|
6465
7082
|
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
@@ -6510,7 +7127,7 @@ function openTranscriptFile(path2, meta) {
|
|
|
6510
7127
|
return stream;
|
|
6511
7128
|
}
|
|
6512
7129
|
function readTranscript(path2) {
|
|
6513
|
-
const raw =
|
|
7130
|
+
const raw = readFileSync11(path2, "utf8");
|
|
6514
7131
|
return parseTranscript(raw);
|
|
6515
7132
|
}
|
|
6516
7133
|
function isPlanStateEmptyShape(s) {
|
|
@@ -6957,25 +7574,25 @@ function truncate(s, n) {
|
|
|
6957
7574
|
}
|
|
6958
7575
|
|
|
6959
7576
|
// src/version.ts
|
|
6960
|
-
import { existsSync as existsSync9, mkdirSync as
|
|
6961
|
-
import { homedir as
|
|
6962
|
-
import { dirname as
|
|
7577
|
+
import { existsSync as existsSync9, mkdirSync as mkdirSync4, readFileSync as readFileSync12, writeFileSync as writeFileSync4 } from "fs";
|
|
7578
|
+
import { homedir as homedir6 } from "os";
|
|
7579
|
+
import { dirname as dirname5, join as join10 } from "path";
|
|
6963
7580
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
6964
7581
|
var REGISTRY_URL = "https://registry.npmjs.org/reasonix/latest";
|
|
6965
7582
|
var LATEST_CACHE_TTL_MS = 24 * 60 * 60 * 1e3;
|
|
6966
7583
|
var LATEST_FETCH_TIMEOUT_MS = 2e3;
|
|
6967
7584
|
function readPackageVersion() {
|
|
6968
7585
|
try {
|
|
6969
|
-
let dir =
|
|
7586
|
+
let dir = dirname5(fileURLToPath2(import.meta.url));
|
|
6970
7587
|
for (let i = 0; i < 6; i++) {
|
|
6971
|
-
const p =
|
|
7588
|
+
const p = join10(dir, "package.json");
|
|
6972
7589
|
if (existsSync9(p)) {
|
|
6973
|
-
const pkg = JSON.parse(
|
|
7590
|
+
const pkg = JSON.parse(readFileSync12(p, "utf8"));
|
|
6974
7591
|
if (pkg?.name === "reasonix" && typeof pkg.version === "string") {
|
|
6975
7592
|
return pkg.version;
|
|
6976
7593
|
}
|
|
6977
7594
|
}
|
|
6978
|
-
const parent =
|
|
7595
|
+
const parent = dirname5(dir);
|
|
6979
7596
|
if (parent === dir) break;
|
|
6980
7597
|
dir = parent;
|
|
6981
7598
|
}
|
|
@@ -6985,11 +7602,11 @@ function readPackageVersion() {
|
|
|
6985
7602
|
}
|
|
6986
7603
|
var VERSION = readPackageVersion();
|
|
6987
7604
|
function cachePath(homeDirOverride) {
|
|
6988
|
-
return
|
|
7605
|
+
return join10(homeDirOverride ?? homedir6(), ".reasonix", "version-cache.json");
|
|
6989
7606
|
}
|
|
6990
7607
|
function readCache(homeDirOverride) {
|
|
6991
7608
|
try {
|
|
6992
|
-
const raw =
|
|
7609
|
+
const raw = readFileSync12(cachePath(homeDirOverride), "utf8");
|
|
6993
7610
|
const parsed = JSON.parse(raw);
|
|
6994
7611
|
if (parsed && typeof parsed.version === "string" && typeof parsed.checkedAt === "number") {
|
|
6995
7612
|
return parsed;
|
|
@@ -7001,8 +7618,8 @@ function readCache(homeDirOverride) {
|
|
|
7001
7618
|
function writeCache(entry, homeDirOverride) {
|
|
7002
7619
|
try {
|
|
7003
7620
|
const p = cachePath(homeDirOverride);
|
|
7004
|
-
|
|
7005
|
-
|
|
7621
|
+
mkdirSync4(dirname5(p), { recursive: true });
|
|
7622
|
+
writeFileSync4(p, JSON.stringify(entry), "utf8");
|
|
7006
7623
|
} catch {
|
|
7007
7624
|
}
|
|
7008
7625
|
}
|
|
@@ -7194,7 +7811,7 @@ var McpClient = class {
|
|
|
7194
7811
|
const id = this.nextId++;
|
|
7195
7812
|
const frame = { jsonrpc: "2.0", id, method, params };
|
|
7196
7813
|
let abortHandler = null;
|
|
7197
|
-
const promise = new Promise((
|
|
7814
|
+
const promise = new Promise((resolve10, reject) => {
|
|
7198
7815
|
const timeout = setTimeout(() => {
|
|
7199
7816
|
this.pending.delete(id);
|
|
7200
7817
|
if (abortHandler && signal) signal.removeEventListener("abort", abortHandler);
|
|
@@ -7203,7 +7820,7 @@ var McpClient = class {
|
|
|
7203
7820
|
);
|
|
7204
7821
|
}, this.requestTimeoutMs);
|
|
7205
7822
|
this.pending.set(id, {
|
|
7206
|
-
resolve:
|
|
7823
|
+
resolve: resolve10,
|
|
7207
7824
|
reject,
|
|
7208
7825
|
timeout
|
|
7209
7826
|
});
|
|
@@ -7228,9 +7845,12 @@ var McpClient = class {
|
|
|
7228
7845
|
signal.addEventListener("abort", abortHandler, { once: true });
|
|
7229
7846
|
}
|
|
7230
7847
|
});
|
|
7848
|
+
promise.catch(() => void 0);
|
|
7231
7849
|
try {
|
|
7232
|
-
await this.transport.send(frame);
|
|
7850
|
+
await Promise.race([this.transport.send(frame), promise.then(() => void 0)]);
|
|
7233
7851
|
} catch (err) {
|
|
7852
|
+
const pending = this.pending.get(id);
|
|
7853
|
+
if (pending) clearTimeout(pending.timeout);
|
|
7234
7854
|
this.pending.delete(id);
|
|
7235
7855
|
if (abortHandler && signal) signal.removeEventListener("abort", abortHandler);
|
|
7236
7856
|
throw err;
|
|
@@ -7285,7 +7905,7 @@ var McpClient = class {
|
|
|
7285
7905
|
};
|
|
7286
7906
|
|
|
7287
7907
|
// src/mcp/stdio.ts
|
|
7288
|
-
import { spawn as
|
|
7908
|
+
import { spawn as spawn5 } from "child_process";
|
|
7289
7909
|
var StdioTransport = class {
|
|
7290
7910
|
child;
|
|
7291
7911
|
queue = [];
|
|
@@ -7300,14 +7920,14 @@ var StdioTransport = class {
|
|
|
7300
7920
|
opts.command,
|
|
7301
7921
|
...(opts.args ?? []).map((a) => quoteArg(a, process.platform === "win32"))
|
|
7302
7922
|
].join(" ");
|
|
7303
|
-
this.child =
|
|
7923
|
+
this.child = spawn5(line, [], {
|
|
7304
7924
|
env,
|
|
7305
7925
|
cwd: opts.cwd,
|
|
7306
7926
|
stdio: ["pipe", "pipe", "inherit"],
|
|
7307
7927
|
shell: true
|
|
7308
7928
|
});
|
|
7309
7929
|
} else {
|
|
7310
|
-
this.child =
|
|
7930
|
+
this.child = spawn5(opts.command, opts.args ?? [], {
|
|
7311
7931
|
env,
|
|
7312
7932
|
cwd: opts.cwd,
|
|
7313
7933
|
stdio: ["pipe", "pipe", "inherit"]
|
|
@@ -7326,12 +7946,12 @@ var StdioTransport = class {
|
|
|
7326
7946
|
}
|
|
7327
7947
|
async send(message) {
|
|
7328
7948
|
if (this.closed) throw new Error("MCP transport is closed");
|
|
7329
|
-
return new Promise((
|
|
7949
|
+
return new Promise((resolve10, reject) => {
|
|
7330
7950
|
const line = `${JSON.stringify(message)}
|
|
7331
7951
|
`;
|
|
7332
7952
|
this.child.stdin.write(line, "utf8", (err) => {
|
|
7333
7953
|
if (err) reject(err);
|
|
7334
|
-
else
|
|
7954
|
+
else resolve10();
|
|
7335
7955
|
});
|
|
7336
7956
|
});
|
|
7337
7957
|
}
|
|
@@ -7342,8 +7962,8 @@ var StdioTransport = class {
|
|
|
7342
7962
|
continue;
|
|
7343
7963
|
}
|
|
7344
7964
|
if (this.closed) return;
|
|
7345
|
-
const next = await new Promise((
|
|
7346
|
-
this.waiters.push(
|
|
7965
|
+
const next = await new Promise((resolve10) => {
|
|
7966
|
+
this.waiters.push(resolve10);
|
|
7347
7967
|
});
|
|
7348
7968
|
if (next === null) return;
|
|
7349
7969
|
yield next;
|
|
@@ -7412,8 +8032,8 @@ var SseTransport = class {
|
|
|
7412
8032
|
constructor(opts) {
|
|
7413
8033
|
this.url = opts.url;
|
|
7414
8034
|
this.headers = opts.headers ?? {};
|
|
7415
|
-
this.endpointReady = new Promise((
|
|
7416
|
-
this.resolveEndpoint =
|
|
8035
|
+
this.endpointReady = new Promise((resolve10, reject) => {
|
|
8036
|
+
this.resolveEndpoint = resolve10;
|
|
7417
8037
|
this.rejectEndpoint = reject;
|
|
7418
8038
|
});
|
|
7419
8039
|
this.endpointReady.catch(() => void 0);
|
|
@@ -7440,8 +8060,8 @@ var SseTransport = class {
|
|
|
7440
8060
|
continue;
|
|
7441
8061
|
}
|
|
7442
8062
|
if (this.closed) return;
|
|
7443
|
-
const next = await new Promise((
|
|
7444
|
-
this.waiters.push(
|
|
8063
|
+
const next = await new Promise((resolve10) => {
|
|
8064
|
+
this.waiters.push(resolve10);
|
|
7445
8065
|
});
|
|
7446
8066
|
if (next === null) return;
|
|
7447
8067
|
yield next;
|
|
@@ -7627,8 +8247,8 @@ var StreamableHttpTransport = class {
|
|
|
7627
8247
|
continue;
|
|
7628
8248
|
}
|
|
7629
8249
|
if (this.closed) return;
|
|
7630
|
-
const next = await new Promise((
|
|
7631
|
-
this.waiters.push(
|
|
8250
|
+
const next = await new Promise((resolve10) => {
|
|
8251
|
+
this.waiters.push(resolve10);
|
|
7632
8252
|
});
|
|
7633
8253
|
if (next === null) return;
|
|
7634
8254
|
yield next;
|
|
@@ -7798,8 +8418,8 @@ async function trySection(load) {
|
|
|
7798
8418
|
}
|
|
7799
8419
|
|
|
7800
8420
|
// src/code/edit-blocks.ts
|
|
7801
|
-
import { existsSync as existsSync10, mkdirSync as
|
|
7802
|
-
import { dirname as
|
|
8421
|
+
import { existsSync as existsSync10, mkdirSync as mkdirSync5, readFileSync as readFileSync13, unlinkSync as unlinkSync3, writeFileSync as writeFileSync5 } from "fs";
|
|
8422
|
+
import { dirname as dirname6, resolve as resolve9 } from "path";
|
|
7803
8423
|
var BLOCK_RE = /^(\S[^\n]*)\n<{7} SEARCH\n([\s\S]*?)\n?={7}\n([\s\S]*?)\n?>{7} REPLACE/gm;
|
|
7804
8424
|
function parseEditBlocks(text) {
|
|
7805
8425
|
const out = [];
|
|
@@ -7817,8 +8437,8 @@ function parseEditBlocks(text) {
|
|
|
7817
8437
|
return out;
|
|
7818
8438
|
}
|
|
7819
8439
|
function applyEditBlock(block, rootDir) {
|
|
7820
|
-
const absRoot =
|
|
7821
|
-
const absTarget =
|
|
8440
|
+
const absRoot = resolve9(rootDir);
|
|
8441
|
+
const absTarget = resolve9(absRoot, block.path);
|
|
7822
8442
|
if (absTarget !== absRoot && !absTarget.startsWith(`${absRoot}${sep()}`)) {
|
|
7823
8443
|
return {
|
|
7824
8444
|
path: block.path,
|
|
@@ -7837,11 +8457,11 @@ function applyEditBlock(block, rootDir) {
|
|
|
7837
8457
|
message: "file does not exist; to create it, use an empty SEARCH block"
|
|
7838
8458
|
};
|
|
7839
8459
|
}
|
|
7840
|
-
|
|
7841
|
-
|
|
8460
|
+
mkdirSync5(dirname6(absTarget), { recursive: true });
|
|
8461
|
+
writeFileSync5(absTarget, block.replace, "utf8");
|
|
7842
8462
|
return { path: block.path, status: "created" };
|
|
7843
8463
|
}
|
|
7844
|
-
const content =
|
|
8464
|
+
const content = readFileSync13(absTarget, "utf8");
|
|
7845
8465
|
if (searchEmpty) {
|
|
7846
8466
|
return {
|
|
7847
8467
|
path: block.path,
|
|
@@ -7861,7 +8481,7 @@ function applyEditBlock(block, rootDir) {
|
|
|
7861
8481
|
};
|
|
7862
8482
|
}
|
|
7863
8483
|
const replaced = `${content.slice(0, idx)}${adaptedReplace}${content.slice(idx + adaptedSearch.length)}`;
|
|
7864
|
-
|
|
8484
|
+
writeFileSync5(absTarget, replaced, "utf8");
|
|
7865
8485
|
return { path: block.path, status: "applied" };
|
|
7866
8486
|
} catch (err) {
|
|
7867
8487
|
return { path: block.path, status: "error", message: err.message };
|
|
@@ -7871,19 +8491,19 @@ function applyEditBlocks(blocks, rootDir) {
|
|
|
7871
8491
|
return blocks.map((b) => applyEditBlock(b, rootDir));
|
|
7872
8492
|
}
|
|
7873
8493
|
function snapshotBeforeEdits(blocks, rootDir) {
|
|
7874
|
-
const absRoot =
|
|
8494
|
+
const absRoot = resolve9(rootDir);
|
|
7875
8495
|
const seen = /* @__PURE__ */ new Set();
|
|
7876
8496
|
const snapshots = [];
|
|
7877
8497
|
for (const b of blocks) {
|
|
7878
8498
|
if (seen.has(b.path)) continue;
|
|
7879
8499
|
seen.add(b.path);
|
|
7880
|
-
const abs =
|
|
8500
|
+
const abs = resolve9(absRoot, b.path);
|
|
7881
8501
|
if (!existsSync10(abs)) {
|
|
7882
8502
|
snapshots.push({ path: b.path, prevContent: null });
|
|
7883
8503
|
continue;
|
|
7884
8504
|
}
|
|
7885
8505
|
try {
|
|
7886
|
-
snapshots.push({ path: b.path, prevContent:
|
|
8506
|
+
snapshots.push({ path: b.path, prevContent: readFileSync13(abs, "utf8") });
|
|
7887
8507
|
} catch {
|
|
7888
8508
|
snapshots.push({ path: b.path, prevContent: null });
|
|
7889
8509
|
}
|
|
@@ -7891,9 +8511,9 @@ function snapshotBeforeEdits(blocks, rootDir) {
|
|
|
7891
8511
|
return snapshots;
|
|
7892
8512
|
}
|
|
7893
8513
|
function restoreSnapshots(snapshots, rootDir) {
|
|
7894
|
-
const absRoot =
|
|
8514
|
+
const absRoot = resolve9(rootDir);
|
|
7895
8515
|
return snapshots.map((snap) => {
|
|
7896
|
-
const abs =
|
|
8516
|
+
const abs = resolve9(absRoot, snap.path);
|
|
7897
8517
|
if (abs !== absRoot && !abs.startsWith(`${absRoot}${sep()}`)) {
|
|
7898
8518
|
return {
|
|
7899
8519
|
path: snap.path,
|
|
@@ -7910,7 +8530,7 @@ function restoreSnapshots(snapshots, rootDir) {
|
|
|
7910
8530
|
message: "removed (the edit had created it)"
|
|
7911
8531
|
};
|
|
7912
8532
|
}
|
|
7913
|
-
|
|
8533
|
+
writeFileSync5(abs, snap.prevContent, "utf8");
|
|
7914
8534
|
return {
|
|
7915
8535
|
path: snap.path,
|
|
7916
8536
|
status: "applied",
|
|
@@ -7929,8 +8549,8 @@ function lineEndingOf(text) {
|
|
|
7929
8549
|
}
|
|
7930
8550
|
|
|
7931
8551
|
// src/code/prompt.ts
|
|
7932
|
-
import { existsSync as existsSync11, readFileSync as
|
|
7933
|
-
import { join as
|
|
8552
|
+
import { existsSync as existsSync11, readFileSync as readFileSync14 } from "fs";
|
|
8553
|
+
import { join as join11 } from "path";
|
|
7934
8554
|
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.
|
|
7935
8555
|
|
|
7936
8556
|
# Cite or shut up \u2014 non-negotiable
|
|
@@ -8132,12 +8752,12 @@ If \`semantic_search\` returns nothing useful (low scores, off-topic), THEN fall
|
|
|
8132
8752
|
function codeSystemPrompt(rootDir, opts = {}) {
|
|
8133
8753
|
const base = opts.hasSemanticSearch ? `${CODE_SYSTEM_PROMPT}${SEMANTIC_SEARCH_ROUTING}` : CODE_SYSTEM_PROMPT;
|
|
8134
8754
|
const withMemory = applyMemoryStack(base, rootDir);
|
|
8135
|
-
const gitignorePath =
|
|
8755
|
+
const gitignorePath = join11(rootDir, ".gitignore");
|
|
8136
8756
|
let result = withMemory;
|
|
8137
8757
|
if (existsSync11(gitignorePath)) {
|
|
8138
8758
|
let content;
|
|
8139
8759
|
try {
|
|
8140
|
-
content =
|
|
8760
|
+
content = readFileSync14(gitignorePath, "utf8");
|
|
8141
8761
|
} catch {
|
|
8142
8762
|
}
|
|
8143
8763
|
if (content !== void 0) {
|
|
@@ -8167,49 +8787,6 @@ ${appendParts.join("\n\n")}`;
|
|
|
8167
8787
|
return result;
|
|
8168
8788
|
}
|
|
8169
8789
|
|
|
8170
|
-
// src/config.ts
|
|
8171
|
-
import { chmodSync as chmodSync2, mkdirSync as mkdirSync5, readFileSync as readFileSync14, writeFileSync as writeFileSync5 } from "fs";
|
|
8172
|
-
import { homedir as homedir6 } from "os";
|
|
8173
|
-
import { dirname as dirname6, join as join11 } from "path";
|
|
8174
|
-
function defaultConfigPath() {
|
|
8175
|
-
return join11(homedir6(), ".reasonix", "config.json");
|
|
8176
|
-
}
|
|
8177
|
-
function readConfig(path2 = defaultConfigPath()) {
|
|
8178
|
-
try {
|
|
8179
|
-
const raw = readFileSync14(path2, "utf8");
|
|
8180
|
-
const parsed = JSON.parse(raw);
|
|
8181
|
-
if (parsed && typeof parsed === "object") return parsed;
|
|
8182
|
-
} catch {
|
|
8183
|
-
}
|
|
8184
|
-
return {};
|
|
8185
|
-
}
|
|
8186
|
-
function writeConfig(cfg, path2 = defaultConfigPath()) {
|
|
8187
|
-
mkdirSync5(dirname6(path2), { recursive: true });
|
|
8188
|
-
writeFileSync5(path2, JSON.stringify(cfg, null, 2), "utf8");
|
|
8189
|
-
try {
|
|
8190
|
-
chmodSync2(path2, 384);
|
|
8191
|
-
} catch {
|
|
8192
|
-
}
|
|
8193
|
-
}
|
|
8194
|
-
function loadApiKey(path2 = defaultConfigPath()) {
|
|
8195
|
-
if (process.env.DEEPSEEK_API_KEY) return process.env.DEEPSEEK_API_KEY;
|
|
8196
|
-
return readConfig(path2).apiKey;
|
|
8197
|
-
}
|
|
8198
|
-
function saveApiKey(key, path2 = defaultConfigPath()) {
|
|
8199
|
-
const cfg = readConfig(path2);
|
|
8200
|
-
cfg.apiKey = key.trim();
|
|
8201
|
-
writeConfig(cfg, path2);
|
|
8202
|
-
}
|
|
8203
|
-
function isPlausibleKey(key) {
|
|
8204
|
-
const trimmed = key.trim();
|
|
8205
|
-
return /^sk-[A-Za-z0-9_-]{16,}$/.test(trimmed);
|
|
8206
|
-
}
|
|
8207
|
-
function redactKey(key) {
|
|
8208
|
-
if (!key) return "";
|
|
8209
|
-
if (key.length <= 12) return "****";
|
|
8210
|
-
return `${key.slice(0, 6)}\u2026${key.slice(-4)}`;
|
|
8211
|
-
}
|
|
8212
|
-
|
|
8213
8790
|
// src/telemetry/usage.ts
|
|
8214
8791
|
import {
|
|
8215
8792
|
appendFileSync as appendFileSync2,
|