reasonix 0.4.22 → 0.4.23
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/README.md +54 -0
- package/dist/cli/index.js +444 -48
- package/dist/cli/index.js.map +1 -1
- package/dist/index.d.ts +207 -1
- package/dist/index.js +319 -88
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/cli/index.js
CHANGED
|
@@ -506,6 +506,174 @@ function resolveTemperatures(budget, custom) {
|
|
|
506
506
|
return out;
|
|
507
507
|
}
|
|
508
508
|
|
|
509
|
+
// src/hooks.ts
|
|
510
|
+
import { spawn } from "child_process";
|
|
511
|
+
import { existsSync, readFileSync as readFileSync2 } from "fs";
|
|
512
|
+
import { homedir as homedir2 } from "os";
|
|
513
|
+
import { join as join2 } from "path";
|
|
514
|
+
var HOOK_EVENTS = [
|
|
515
|
+
"PreToolUse",
|
|
516
|
+
"PostToolUse",
|
|
517
|
+
"UserPromptSubmit",
|
|
518
|
+
"Stop"
|
|
519
|
+
];
|
|
520
|
+
var BLOCKING_EVENTS = /* @__PURE__ */ new Set(["PreToolUse", "UserPromptSubmit"]);
|
|
521
|
+
var DEFAULT_TIMEOUTS_MS = {
|
|
522
|
+
PreToolUse: 5e3,
|
|
523
|
+
UserPromptSubmit: 5e3,
|
|
524
|
+
PostToolUse: 3e4,
|
|
525
|
+
Stop: 3e4
|
|
526
|
+
};
|
|
527
|
+
var HOOK_SETTINGS_FILENAME = "settings.json";
|
|
528
|
+
var HOOK_SETTINGS_DIRNAME = ".reasonix";
|
|
529
|
+
function globalSettingsPath(homeDirOverride) {
|
|
530
|
+
return join2(homeDirOverride ?? homedir2(), HOOK_SETTINGS_DIRNAME, HOOK_SETTINGS_FILENAME);
|
|
531
|
+
}
|
|
532
|
+
function projectSettingsPath(projectRoot) {
|
|
533
|
+
return join2(projectRoot, HOOK_SETTINGS_DIRNAME, HOOK_SETTINGS_FILENAME);
|
|
534
|
+
}
|
|
535
|
+
function readSettingsFile(path) {
|
|
536
|
+
if (!existsSync(path)) return null;
|
|
537
|
+
try {
|
|
538
|
+
const raw = readFileSync2(path, "utf8");
|
|
539
|
+
const parsed = JSON.parse(raw);
|
|
540
|
+
if (parsed && typeof parsed === "object") return parsed;
|
|
541
|
+
} catch {
|
|
542
|
+
}
|
|
543
|
+
return null;
|
|
544
|
+
}
|
|
545
|
+
function loadHooks(opts = {}) {
|
|
546
|
+
const out = [];
|
|
547
|
+
if (opts.projectRoot) {
|
|
548
|
+
const projPath = projectSettingsPath(opts.projectRoot);
|
|
549
|
+
const settings2 = readSettingsFile(projPath);
|
|
550
|
+
if (settings2) appendResolved(out, settings2, "project", projPath);
|
|
551
|
+
}
|
|
552
|
+
const globalPath = globalSettingsPath(opts.homeDir);
|
|
553
|
+
const settings = readSettingsFile(globalPath);
|
|
554
|
+
if (settings) appendResolved(out, settings, "global", globalPath);
|
|
555
|
+
return out;
|
|
556
|
+
}
|
|
557
|
+
function appendResolved(out, settings, scope, source) {
|
|
558
|
+
if (!settings.hooks) return;
|
|
559
|
+
for (const event of HOOK_EVENTS) {
|
|
560
|
+
const list = settings.hooks[event];
|
|
561
|
+
if (!Array.isArray(list)) continue;
|
|
562
|
+
for (const cfg of list) {
|
|
563
|
+
if (!cfg || typeof cfg.command !== "string" || cfg.command.trim() === "") continue;
|
|
564
|
+
out.push({ ...cfg, event, scope, source });
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
function matchesTool(hook, toolName) {
|
|
569
|
+
if (hook.event !== "PreToolUse" && hook.event !== "PostToolUse") return true;
|
|
570
|
+
const m = hook.match;
|
|
571
|
+
if (!m || m === "*") return true;
|
|
572
|
+
try {
|
|
573
|
+
const re = new RegExp(`^(?:${m})$`);
|
|
574
|
+
return re.test(toolName);
|
|
575
|
+
} catch {
|
|
576
|
+
return false;
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
function defaultSpawner(input) {
|
|
580
|
+
return new Promise((resolve6) => {
|
|
581
|
+
const child = spawn(input.command, {
|
|
582
|
+
cwd: input.cwd,
|
|
583
|
+
shell: true,
|
|
584
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
585
|
+
});
|
|
586
|
+
let stdout2 = "";
|
|
587
|
+
let stderr = "";
|
|
588
|
+
let timedOut = false;
|
|
589
|
+
const timer = setTimeout(() => {
|
|
590
|
+
timedOut = true;
|
|
591
|
+
child.kill("SIGTERM");
|
|
592
|
+
setTimeout(() => {
|
|
593
|
+
try {
|
|
594
|
+
child.kill("SIGKILL");
|
|
595
|
+
} catch {
|
|
596
|
+
}
|
|
597
|
+
}, 500);
|
|
598
|
+
}, input.timeoutMs);
|
|
599
|
+
child.stdout.on("data", (chunk) => {
|
|
600
|
+
stdout2 += chunk.toString("utf8");
|
|
601
|
+
});
|
|
602
|
+
child.stderr.on("data", (chunk) => {
|
|
603
|
+
stderr += chunk.toString("utf8");
|
|
604
|
+
});
|
|
605
|
+
child.once("error", (err) => {
|
|
606
|
+
clearTimeout(timer);
|
|
607
|
+
resolve6({
|
|
608
|
+
exitCode: null,
|
|
609
|
+
stdout: stdout2,
|
|
610
|
+
stderr,
|
|
611
|
+
timedOut: false,
|
|
612
|
+
spawnError: err
|
|
613
|
+
});
|
|
614
|
+
});
|
|
615
|
+
child.once("close", (code) => {
|
|
616
|
+
clearTimeout(timer);
|
|
617
|
+
resolve6({
|
|
618
|
+
exitCode: code,
|
|
619
|
+
stdout: stdout2.trim(),
|
|
620
|
+
stderr: stderr.trim(),
|
|
621
|
+
timedOut
|
|
622
|
+
});
|
|
623
|
+
});
|
|
624
|
+
try {
|
|
625
|
+
child.stdin.write(input.stdin);
|
|
626
|
+
child.stdin.end();
|
|
627
|
+
} catch {
|
|
628
|
+
}
|
|
629
|
+
});
|
|
630
|
+
}
|
|
631
|
+
function formatHookOutcomeMessage(outcome) {
|
|
632
|
+
if (outcome.decision === "pass") return "";
|
|
633
|
+
const detail = (outcome.stderr || outcome.stdout || "").trim();
|
|
634
|
+
const tag = `${outcome.hook.scope}/${outcome.hook.event}`;
|
|
635
|
+
const cmd = outcome.hook.command.length > 60 ? `${outcome.hook.command.slice(0, 60)}\u2026` : outcome.hook.command;
|
|
636
|
+
const head = `hook ${tag} \`${cmd}\` ${outcome.decision}`;
|
|
637
|
+
return detail ? `${head}: ${detail}` : head;
|
|
638
|
+
}
|
|
639
|
+
function decideOutcome(event, raw) {
|
|
640
|
+
if (raw.spawnError) return "error";
|
|
641
|
+
if (raw.timedOut) return BLOCKING_EVENTS.has(event) ? "block" : "warn";
|
|
642
|
+
if (raw.exitCode === 0) return "pass";
|
|
643
|
+
if (raw.exitCode === 2 && BLOCKING_EVENTS.has(event)) return "block";
|
|
644
|
+
return "warn";
|
|
645
|
+
}
|
|
646
|
+
async function runHooks(opts) {
|
|
647
|
+
const spawner = opts.spawner ?? defaultSpawner;
|
|
648
|
+
const event = opts.payload.event;
|
|
649
|
+
const toolName = opts.payload.toolName ?? "";
|
|
650
|
+
const matching = opts.hooks.filter((h) => h.event === event && matchesTool(h, toolName));
|
|
651
|
+
const outcomes = [];
|
|
652
|
+
let blocked = false;
|
|
653
|
+
const stdin2 = `${JSON.stringify(opts.payload)}
|
|
654
|
+
`;
|
|
655
|
+
for (const hook of matching) {
|
|
656
|
+
const start = Date.now();
|
|
657
|
+
const timeoutMs = hook.timeout ?? DEFAULT_TIMEOUTS_MS[event];
|
|
658
|
+
const cwd = hook.cwd ?? opts.payload.cwd;
|
|
659
|
+
const raw = await spawner({ command: hook.command, cwd, stdin: stdin2, timeoutMs });
|
|
660
|
+
const decision = decideOutcome(event, raw);
|
|
661
|
+
outcomes.push({
|
|
662
|
+
hook,
|
|
663
|
+
decision,
|
|
664
|
+
exitCode: raw.exitCode,
|
|
665
|
+
stdout: raw.stdout,
|
|
666
|
+
stderr: raw.stderr || (raw.spawnError ? raw.spawnError.message : "") || (raw.timedOut ? `hook timed out after ${timeoutMs}ms` : ""),
|
|
667
|
+
durationMs: Date.now() - start
|
|
668
|
+
});
|
|
669
|
+
if (decision === "block") {
|
|
670
|
+
blocked = true;
|
|
671
|
+
break;
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
return { event, outcomes, blocked };
|
|
675
|
+
}
|
|
676
|
+
|
|
509
677
|
// src/repair/flatten.ts
|
|
510
678
|
function analyzeSchema(schema) {
|
|
511
679
|
if (!schema) return { shouldFlatten: false, leafCount: 0, maxDepth: 0 };
|
|
@@ -1128,21 +1296,21 @@ function signature2(call) {
|
|
|
1128
1296
|
import {
|
|
1129
1297
|
appendFileSync,
|
|
1130
1298
|
chmodSync as chmodSync2,
|
|
1131
|
-
existsSync,
|
|
1299
|
+
existsSync as existsSync2,
|
|
1132
1300
|
mkdirSync as mkdirSync2,
|
|
1133
|
-
readFileSync as
|
|
1301
|
+
readFileSync as readFileSync3,
|
|
1134
1302
|
readdirSync,
|
|
1135
1303
|
statSync,
|
|
1136
1304
|
unlinkSync,
|
|
1137
1305
|
writeFileSync as writeFileSync2
|
|
1138
1306
|
} from "fs";
|
|
1139
|
-
import { homedir as
|
|
1140
|
-
import { dirname as dirname2, join as
|
|
1307
|
+
import { homedir as homedir3 } from "os";
|
|
1308
|
+
import { dirname as dirname2, join as join3 } from "path";
|
|
1141
1309
|
function sessionsDir() {
|
|
1142
|
-
return
|
|
1310
|
+
return join3(homedir3(), ".reasonix", "sessions");
|
|
1143
1311
|
}
|
|
1144
1312
|
function sessionPath(name) {
|
|
1145
|
-
return
|
|
1313
|
+
return join3(sessionsDir(), `${sanitizeName(name)}.jsonl`);
|
|
1146
1314
|
}
|
|
1147
1315
|
function sanitizeName(name) {
|
|
1148
1316
|
const cleaned = name.replace(/[^\w\-\u4e00-\u9fa5]/g, "_").slice(0, 64);
|
|
@@ -1150,9 +1318,9 @@ function sanitizeName(name) {
|
|
|
1150
1318
|
}
|
|
1151
1319
|
function loadSessionMessages(name) {
|
|
1152
1320
|
const path = sessionPath(name);
|
|
1153
|
-
if (!
|
|
1321
|
+
if (!existsSync2(path)) return [];
|
|
1154
1322
|
try {
|
|
1155
|
-
const raw =
|
|
1323
|
+
const raw = readFileSync3(path, "utf8");
|
|
1156
1324
|
const out = [];
|
|
1157
1325
|
for (const line of raw.split(/\r?\n/)) {
|
|
1158
1326
|
const trimmed = line.trim();
|
|
@@ -1180,11 +1348,11 @@ function appendSessionMessage(name, message) {
|
|
|
1180
1348
|
}
|
|
1181
1349
|
function listSessions() {
|
|
1182
1350
|
const dir = sessionsDir();
|
|
1183
|
-
if (!
|
|
1351
|
+
if (!existsSync2(dir)) return [];
|
|
1184
1352
|
try {
|
|
1185
1353
|
const files = readdirSync(dir).filter((f) => f.endsWith(".jsonl"));
|
|
1186
1354
|
return files.map((file) => {
|
|
1187
|
-
const path =
|
|
1355
|
+
const path = join3(dir, file);
|
|
1188
1356
|
const stat = statSync(path);
|
|
1189
1357
|
const name = file.replace(/\.jsonl$/, "");
|
|
1190
1358
|
const messageCount = countLines(path);
|
|
@@ -1216,7 +1384,7 @@ function rewriteSession(name, messages) {
|
|
|
1216
1384
|
}
|
|
1217
1385
|
function countLines(path) {
|
|
1218
1386
|
try {
|
|
1219
|
-
const raw =
|
|
1387
|
+
const raw = readFileSync3(path, "utf8");
|
|
1220
1388
|
return raw.split(/\r?\n/).filter((l) => l.trim()).length;
|
|
1221
1389
|
} catch {
|
|
1222
1390
|
return 0;
|
|
@@ -1330,6 +1498,14 @@ var CacheFirstLoop = class {
|
|
|
1330
1498
|
branchEnabled;
|
|
1331
1499
|
branchOptions;
|
|
1332
1500
|
sessionName;
|
|
1501
|
+
/**
|
|
1502
|
+
* Hook list, mutable so `/hooks reload` can swap it without
|
|
1503
|
+
* reconstructing the loop. Default empty — the filter cost on a
|
|
1504
|
+
* tool call is one array length check.
|
|
1505
|
+
*/
|
|
1506
|
+
hooks;
|
|
1507
|
+
/** `cwd` reported to hook stdin. Resolved once at construction. */
|
|
1508
|
+
hookCwd;
|
|
1333
1509
|
/** Number of messages that were pre-loaded from the session file. */
|
|
1334
1510
|
resumedMessageCount;
|
|
1335
1511
|
_turn = 0;
|
|
@@ -1348,6 +1524,8 @@ var CacheFirstLoop = class {
|
|
|
1348
1524
|
this.tools = opts.tools ?? new ToolRegistry();
|
|
1349
1525
|
this.model = opts.model ?? "deepseek-chat";
|
|
1350
1526
|
this.maxToolIters = opts.maxToolIters ?? 64;
|
|
1527
|
+
this.hooks = opts.hooks ?? [];
|
|
1528
|
+
this.hookCwd = opts.hookCwd ?? process.cwd();
|
|
1351
1529
|
if (typeof opts.branch === "number") {
|
|
1352
1530
|
this.branchOptions = { budget: opts.branch };
|
|
1353
1531
|
} else if (opts.branch && typeof opts.branch === "object") {
|
|
@@ -1811,7 +1989,37 @@ var CacheFirstLoop = class {
|
|
|
1811
1989
|
toolName: name,
|
|
1812
1990
|
toolArgs: args
|
|
1813
1991
|
};
|
|
1814
|
-
const
|
|
1992
|
+
const parsedArgs = safeParseToolArgs(args);
|
|
1993
|
+
const preReport = await runHooks({
|
|
1994
|
+
hooks: this.hooks,
|
|
1995
|
+
payload: {
|
|
1996
|
+
event: "PreToolUse",
|
|
1997
|
+
cwd: this.hookCwd,
|
|
1998
|
+
toolName: name,
|
|
1999
|
+
toolArgs: parsedArgs
|
|
2000
|
+
}
|
|
2001
|
+
});
|
|
2002
|
+
for (const w of hookWarnings(preReport.outcomes, this._turn)) yield w;
|
|
2003
|
+
let result;
|
|
2004
|
+
if (preReport.blocked) {
|
|
2005
|
+
const blocking = preReport.outcomes[preReport.outcomes.length - 1];
|
|
2006
|
+
const reason = (blocking?.stderr || blocking?.stdout || "blocked by PreToolUse hook").trim();
|
|
2007
|
+
result = `[hook block] ${blocking?.hook.command ?? "<unknown>"}
|
|
2008
|
+
${reason}`;
|
|
2009
|
+
} else {
|
|
2010
|
+
result = await this.tools.dispatch(name, args, { signal });
|
|
2011
|
+
const postReport = await runHooks({
|
|
2012
|
+
hooks: this.hooks,
|
|
2013
|
+
payload: {
|
|
2014
|
+
event: "PostToolUse",
|
|
2015
|
+
cwd: this.hookCwd,
|
|
2016
|
+
toolName: name,
|
|
2017
|
+
toolArgs: parsedArgs,
|
|
2018
|
+
toolResult: result
|
|
2019
|
+
}
|
|
2020
|
+
});
|
|
2021
|
+
for (const w of hookWarnings(postReport.outcomes, this._turn)) yield w;
|
|
2022
|
+
}
|
|
1815
2023
|
this.appendAndPersist({
|
|
1816
2024
|
role: "tool",
|
|
1817
2025
|
tool_call_id: call.id ?? "",
|
|
@@ -1898,6 +2106,19 @@ function stripHallucinatedToolMarkup(s) {
|
|
|
1898
2106
|
out = out.replace(/<|DSML|[\s\S]*$/g, "");
|
|
1899
2107
|
return out.trim();
|
|
1900
2108
|
}
|
|
2109
|
+
function safeParseToolArgs(raw) {
|
|
2110
|
+
try {
|
|
2111
|
+
return JSON.parse(raw);
|
|
2112
|
+
} catch {
|
|
2113
|
+
return raw;
|
|
2114
|
+
}
|
|
2115
|
+
}
|
|
2116
|
+
function* hookWarnings(outcomes, turn) {
|
|
2117
|
+
for (const o of outcomes) {
|
|
2118
|
+
if (o.decision === "pass") continue;
|
|
2119
|
+
yield { turn, role: "warning", content: formatHookOutcomeMessage(o) };
|
|
2120
|
+
}
|
|
2121
|
+
}
|
|
1901
2122
|
function reasonPrefixFor(reason, iterCap) {
|
|
1902
2123
|
if (reason === "aborted") return "[aborted by user (Esc) \u2014 summarizing what I found so far]";
|
|
1903
2124
|
if (reason === "context-guard") {
|
|
@@ -2493,8 +2714,8 @@ function registerPlanTool(registry, opts = {}) {
|
|
|
2493
2714
|
}
|
|
2494
2715
|
|
|
2495
2716
|
// src/tools/shell.ts
|
|
2496
|
-
import { spawn } from "child_process";
|
|
2497
|
-
import { existsSync as
|
|
2717
|
+
import { spawn as spawn2 } from "child_process";
|
|
2718
|
+
import { existsSync as existsSync3, statSync as statSync2 } from "fs";
|
|
2498
2719
|
import * as pathMod2 from "path";
|
|
2499
2720
|
var DEFAULT_TIMEOUT_SEC = 60;
|
|
2500
2721
|
var DEFAULT_MAX_OUTPUT_CHARS = 32e3;
|
|
@@ -2617,7 +2838,7 @@ async function runCommand(cmd, opts) {
|
|
|
2617
2838
|
return await new Promise((resolve6, reject) => {
|
|
2618
2839
|
let child;
|
|
2619
2840
|
try {
|
|
2620
|
-
child =
|
|
2841
|
+
child = spawn2(bin, args, effectiveSpawnOpts);
|
|
2621
2842
|
} catch (err) {
|
|
2622
2843
|
reject(err);
|
|
2623
2844
|
return;
|
|
@@ -2672,7 +2893,7 @@ function resolveExecutable(cmd, opts = {}) {
|
|
|
2672
2893
|
}
|
|
2673
2894
|
function defaultIsFile(full) {
|
|
2674
2895
|
try {
|
|
2675
|
-
return
|
|
2896
|
+
return existsSync3(full) && statSync2(full).isFile();
|
|
2676
2897
|
} catch {
|
|
2677
2898
|
return false;
|
|
2678
2899
|
}
|
|
@@ -2975,12 +3196,12 @@ ${i + 1}. ${r.title}`);
|
|
|
2975
3196
|
}
|
|
2976
3197
|
|
|
2977
3198
|
// src/env.ts
|
|
2978
|
-
import { readFileSync as
|
|
3199
|
+
import { readFileSync as readFileSync4 } from "fs";
|
|
2979
3200
|
import { resolve as resolve3 } from "path";
|
|
2980
3201
|
function loadDotenv(path = ".env") {
|
|
2981
3202
|
let raw;
|
|
2982
3203
|
try {
|
|
2983
|
-
raw =
|
|
3204
|
+
raw = readFileSync4(resolve3(process.cwd(), path), "utf8");
|
|
2984
3205
|
} catch {
|
|
2985
3206
|
return;
|
|
2986
3207
|
}
|
|
@@ -2999,7 +3220,7 @@ function loadDotenv(path = ".env") {
|
|
|
2999
3220
|
}
|
|
3000
3221
|
|
|
3001
3222
|
// src/transcript.ts
|
|
3002
|
-
import { createWriteStream, readFileSync as
|
|
3223
|
+
import { createWriteStream, readFileSync as readFileSync5 } from "fs";
|
|
3003
3224
|
function recordFromLoopEvent(ev, extra) {
|
|
3004
3225
|
const rec = {
|
|
3005
3226
|
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
@@ -3050,7 +3271,7 @@ function openTranscriptFile(path, meta) {
|
|
|
3050
3271
|
return stream;
|
|
3051
3272
|
}
|
|
3052
3273
|
function readTranscript(path) {
|
|
3053
|
-
const raw =
|
|
3274
|
+
const raw = readFileSync5(path, "utf8");
|
|
3054
3275
|
return parseTranscript(raw);
|
|
3055
3276
|
}
|
|
3056
3277
|
function isPlanStateEmptyShape(s) {
|
|
@@ -3784,7 +4005,7 @@ var McpClient = class {
|
|
|
3784
4005
|
};
|
|
3785
4006
|
|
|
3786
4007
|
// src/mcp/stdio.ts
|
|
3787
|
-
import { spawn as
|
|
4008
|
+
import { spawn as spawn3 } from "child_process";
|
|
3788
4009
|
var StdioTransport = class {
|
|
3789
4010
|
child;
|
|
3790
4011
|
queue = [];
|
|
@@ -3799,14 +4020,14 @@ var StdioTransport = class {
|
|
|
3799
4020
|
opts.command,
|
|
3800
4021
|
...(opts.args ?? []).map((a) => quoteArg(a, process.platform === "win32"))
|
|
3801
4022
|
].join(" ");
|
|
3802
|
-
this.child =
|
|
4023
|
+
this.child = spawn3(line, [], {
|
|
3803
4024
|
env,
|
|
3804
4025
|
cwd: opts.cwd,
|
|
3805
4026
|
stdio: ["pipe", "pipe", "inherit"],
|
|
3806
4027
|
shell: true
|
|
3807
4028
|
});
|
|
3808
4029
|
} else {
|
|
3809
|
-
this.child =
|
|
4030
|
+
this.child = spawn3(opts.command, opts.args ?? [], {
|
|
3810
4031
|
env,
|
|
3811
4032
|
cwd: opts.cwd,
|
|
3812
4033
|
stdio: ["pipe", "pipe", "inherit"]
|
|
@@ -4136,7 +4357,7 @@ async function trySection(load) {
|
|
|
4136
4357
|
}
|
|
4137
4358
|
|
|
4138
4359
|
// src/code/edit-blocks.ts
|
|
4139
|
-
import { existsSync as
|
|
4360
|
+
import { existsSync as existsSync4, mkdirSync as mkdirSync3, readFileSync as readFileSync6, unlinkSync as unlinkSync2, writeFileSync as writeFileSync3 } from "fs";
|
|
4140
4361
|
import { dirname as dirname4, resolve as resolve4 } from "path";
|
|
4141
4362
|
var BLOCK_RE = /^(\S[^\n]*)\n<{7} SEARCH\n([\s\S]*?)\n?={7}\n([\s\S]*?)\n?>{7} REPLACE/gm;
|
|
4142
4363
|
function parseEditBlocks(text) {
|
|
@@ -4165,7 +4386,7 @@ function applyEditBlock(block, rootDir) {
|
|
|
4165
4386
|
};
|
|
4166
4387
|
}
|
|
4167
4388
|
const searchEmpty = block.search.length === 0;
|
|
4168
|
-
const exists =
|
|
4389
|
+
const exists = existsSync4(absTarget);
|
|
4169
4390
|
try {
|
|
4170
4391
|
if (!exists) {
|
|
4171
4392
|
if (!searchEmpty) {
|
|
@@ -4179,7 +4400,7 @@ function applyEditBlock(block, rootDir) {
|
|
|
4179
4400
|
writeFileSync3(absTarget, block.replace, "utf8");
|
|
4180
4401
|
return { path: block.path, status: "created" };
|
|
4181
4402
|
}
|
|
4182
|
-
const content =
|
|
4403
|
+
const content = readFileSync6(absTarget, "utf8");
|
|
4183
4404
|
if (searchEmpty) {
|
|
4184
4405
|
return {
|
|
4185
4406
|
path: block.path,
|
|
@@ -4213,12 +4434,12 @@ function snapshotBeforeEdits(blocks, rootDir) {
|
|
|
4213
4434
|
if (seen.has(b.path)) continue;
|
|
4214
4435
|
seen.add(b.path);
|
|
4215
4436
|
const abs = resolve4(absRoot, b.path);
|
|
4216
|
-
if (!
|
|
4437
|
+
if (!existsSync4(abs)) {
|
|
4217
4438
|
snapshots.push({ path: b.path, prevContent: null });
|
|
4218
4439
|
continue;
|
|
4219
4440
|
}
|
|
4220
4441
|
try {
|
|
4221
|
-
snapshots.push({ path: b.path, prevContent:
|
|
4442
|
+
snapshots.push({ path: b.path, prevContent: readFileSync6(abs, "utf8") });
|
|
4222
4443
|
} catch {
|
|
4223
4444
|
snapshots.push({ path: b.path, prevContent: null });
|
|
4224
4445
|
}
|
|
@@ -4238,7 +4459,7 @@ function restoreSnapshots(snapshots, rootDir) {
|
|
|
4238
4459
|
}
|
|
4239
4460
|
try {
|
|
4240
4461
|
if (snap.prevContent === null) {
|
|
4241
|
-
if (
|
|
4462
|
+
if (existsSync4(abs)) unlinkSync2(abs);
|
|
4242
4463
|
return {
|
|
4243
4464
|
path: snap.path,
|
|
4244
4465
|
status: "applied",
|
|
@@ -4261,9 +4482,9 @@ function sep() {
|
|
|
4261
4482
|
}
|
|
4262
4483
|
|
|
4263
4484
|
// src/version.ts
|
|
4264
|
-
import { existsSync as
|
|
4265
|
-
import { homedir as
|
|
4266
|
-
import { dirname as dirname5, join as
|
|
4485
|
+
import { existsSync as existsSync5, mkdirSync as mkdirSync4, readFileSync as readFileSync7, writeFileSync as writeFileSync4 } from "fs";
|
|
4486
|
+
import { homedir as homedir4 } from "os";
|
|
4487
|
+
import { dirname as dirname5, join as join5 } from "path";
|
|
4267
4488
|
import { fileURLToPath } from "url";
|
|
4268
4489
|
var REGISTRY_URL = "https://registry.npmjs.org/reasonix/latest";
|
|
4269
4490
|
var LATEST_CACHE_TTL_MS = 24 * 60 * 60 * 1e3;
|
|
@@ -4272,9 +4493,9 @@ function readPackageVersion() {
|
|
|
4272
4493
|
try {
|
|
4273
4494
|
let dir = dirname5(fileURLToPath(import.meta.url));
|
|
4274
4495
|
for (let i = 0; i < 6; i++) {
|
|
4275
|
-
const p =
|
|
4276
|
-
if (
|
|
4277
|
-
const pkg = JSON.parse(
|
|
4496
|
+
const p = join5(dir, "package.json");
|
|
4497
|
+
if (existsSync5(p)) {
|
|
4498
|
+
const pkg = JSON.parse(readFileSync7(p, "utf8"));
|
|
4278
4499
|
if (pkg?.name === "reasonix" && typeof pkg.version === "string") {
|
|
4279
4500
|
return pkg.version;
|
|
4280
4501
|
}
|
|
@@ -4289,11 +4510,11 @@ function readPackageVersion() {
|
|
|
4289
4510
|
}
|
|
4290
4511
|
var VERSION = readPackageVersion();
|
|
4291
4512
|
function cachePath(homeDirOverride) {
|
|
4292
|
-
return
|
|
4513
|
+
return join5(homeDirOverride ?? homedir4(), ".reasonix", "version-cache.json");
|
|
4293
4514
|
}
|
|
4294
4515
|
function readCache(homeDirOverride) {
|
|
4295
4516
|
try {
|
|
4296
|
-
const raw =
|
|
4517
|
+
const raw = readFileSync7(cachePath(homeDirOverride), "utf8");
|
|
4297
4518
|
const parsed = JSON.parse(raw);
|
|
4298
4519
|
if (parsed && typeof parsed.version === "string" && typeof parsed.checkedAt === "number") {
|
|
4299
4520
|
return parsed;
|
|
@@ -4362,7 +4583,7 @@ function isNpxInstall() {
|
|
|
4362
4583
|
}
|
|
4363
4584
|
|
|
4364
4585
|
// src/cli/commands/chat.tsx
|
|
4365
|
-
import { existsSync as
|
|
4586
|
+
import { existsSync as existsSync6, statSync as statSync3 } from "fs";
|
|
4366
4587
|
import { render } from "ink";
|
|
4367
4588
|
import React15, { useState as useState7 } from "react";
|
|
4368
4589
|
|
|
@@ -5395,6 +5616,15 @@ var SLASH_COMMANDS = [
|
|
|
5395
5616
|
argsHint: "[list|show <name>|<name> [args]]",
|
|
5396
5617
|
summary: "list / run user skills (<project>/.reasonix/skills + ~/.reasonix/skills)"
|
|
5397
5618
|
},
|
|
5619
|
+
{
|
|
5620
|
+
cmd: "hooks",
|
|
5621
|
+
argsHint: "[reload]",
|
|
5622
|
+
summary: "list active hooks (settings.json under .reasonix/) \xB7 reload re-reads from disk"
|
|
5623
|
+
},
|
|
5624
|
+
{
|
|
5625
|
+
cmd: "update",
|
|
5626
|
+
summary: "show current vs latest version + the shell command to upgrade"
|
|
5627
|
+
},
|
|
5398
5628
|
{ cmd: "think", summary: "dump the last turn's full R1 reasoning (reasoner only)" },
|
|
5399
5629
|
{ cmd: "retry", summary: "truncate & resend your last message (fresh sample)" },
|
|
5400
5630
|
{ cmd: "compact", argsHint: "[cap]", summary: "shrink oversized tool results in the log" },
|
|
@@ -5568,6 +5798,13 @@ function handleSlash(cmd, args, loop, ctx = {}) {
|
|
|
5568
5798
|
case "skills": {
|
|
5569
5799
|
return handleSkillSlash(args, ctx);
|
|
5570
5800
|
}
|
|
5801
|
+
case "hook":
|
|
5802
|
+
case "hooks": {
|
|
5803
|
+
return handleHooksSlash(args, loop, ctx);
|
|
5804
|
+
}
|
|
5805
|
+
case "update": {
|
|
5806
|
+
return handleUpdateSlash(ctx);
|
|
5807
|
+
}
|
|
5571
5808
|
case "think":
|
|
5572
5809
|
case "reasoning": {
|
|
5573
5810
|
const raw = loop.scratch.reasoning;
|
|
@@ -5804,6 +6041,93 @@ ${entry.text}`
|
|
|
5804
6041
|
return { unknown: true, info: `unknown command: /${cmd} (try /help)` };
|
|
5805
6042
|
}
|
|
5806
6043
|
}
|
|
6044
|
+
function handleUpdateSlash(ctx) {
|
|
6045
|
+
const latest = ctx.latestVersion ?? null;
|
|
6046
|
+
const lines = [`current: reasonix ${VERSION}`];
|
|
6047
|
+
if (latest === null) {
|
|
6048
|
+
ctx.refreshLatestVersion?.();
|
|
6049
|
+
lines.push(
|
|
6050
|
+
"latest: (not yet resolved \u2014 background check in flight or offline)",
|
|
6051
|
+
"",
|
|
6052
|
+
"triggered a fresh registry fetch \u2014 retry `/update` in a few seconds,",
|
|
6053
|
+
"or run `reasonix update` in another terminal to force it synchronously."
|
|
6054
|
+
);
|
|
6055
|
+
return { info: lines.join("\n") };
|
|
6056
|
+
}
|
|
6057
|
+
lines.push(`latest: reasonix ${latest}`);
|
|
6058
|
+
const diff = compareVersions(VERSION, latest);
|
|
6059
|
+
if (diff >= 0) {
|
|
6060
|
+
lines.push("", "you're on the latest. nothing to do.");
|
|
6061
|
+
return { info: lines.join("\n") };
|
|
6062
|
+
}
|
|
6063
|
+
if (isNpxInstall()) {
|
|
6064
|
+
lines.push(
|
|
6065
|
+
"",
|
|
6066
|
+
"you're running via npx \u2014 the next `npx reasonix ...` launch will auto-fetch.",
|
|
6067
|
+
"to force a refresh sooner: `npm cache clean --force`."
|
|
6068
|
+
);
|
|
6069
|
+
} else {
|
|
6070
|
+
lines.push(
|
|
6071
|
+
"",
|
|
6072
|
+
"to upgrade, exit this session and run:",
|
|
6073
|
+
" reasonix update (interactive, dry-run supported via --dry-run)",
|
|
6074
|
+
" npm install -g reasonix@latest (direct)",
|
|
6075
|
+
"",
|
|
6076
|
+
"in-session install is deliberately disabled \u2014 the npm spawn would",
|
|
6077
|
+
"corrupt this TUI's rendering and Windows can lock the running binary."
|
|
6078
|
+
);
|
|
6079
|
+
}
|
|
6080
|
+
return { info: lines.join("\n") };
|
|
6081
|
+
}
|
|
6082
|
+
function handleHooksSlash(args, loop, ctx) {
|
|
6083
|
+
const sub = (args[0] ?? "").toLowerCase();
|
|
6084
|
+
if (sub === "reload") {
|
|
6085
|
+
if (!ctx.reloadHooks) {
|
|
6086
|
+
return {
|
|
6087
|
+
info: "/hooks reload is not available in this context (no reload callback wired)."
|
|
6088
|
+
};
|
|
6089
|
+
}
|
|
6090
|
+
const count = ctx.reloadHooks();
|
|
6091
|
+
return { info: `\u25B8 reloaded hooks \xB7 ${count} active` };
|
|
6092
|
+
}
|
|
6093
|
+
if (sub !== "" && sub !== "list" && sub !== "ls") {
|
|
6094
|
+
return {
|
|
6095
|
+
info: "usage: /hooks list active hooks\n /hooks reload re-read settings.json files"
|
|
6096
|
+
};
|
|
6097
|
+
}
|
|
6098
|
+
const hooks = loop.hooks;
|
|
6099
|
+
const projPath = ctx.codeRoot ? projectSettingsPath(ctx.codeRoot) : void 0;
|
|
6100
|
+
const globPath = globalSettingsPath();
|
|
6101
|
+
if (hooks.length === 0) {
|
|
6102
|
+
const lines2 = [
|
|
6103
|
+
"no hooks configured.",
|
|
6104
|
+
"",
|
|
6105
|
+
"drop a settings.json with a `hooks` key into either of:",
|
|
6106
|
+
ctx.codeRoot ? ` \xB7 ${projPath} (project)` : " \xB7 <project>/.reasonix/settings.json (project)",
|
|
6107
|
+
` \xB7 ${globPath} (global)`,
|
|
6108
|
+
"",
|
|
6109
|
+
"events: PreToolUse, PostToolUse, UserPromptSubmit, Stop",
|
|
6110
|
+
"exit 0 = pass \xB7 exit 2 = block (Pre*) \xB7 other = warn"
|
|
6111
|
+
];
|
|
6112
|
+
return { info: lines2.join("\n") };
|
|
6113
|
+
}
|
|
6114
|
+
const grouped = /* @__PURE__ */ new Map();
|
|
6115
|
+
for (const event of HOOK_EVENTS) grouped.set(event, []);
|
|
6116
|
+
for (const h of hooks) grouped.get(h.event)?.push(h);
|
|
6117
|
+
const lines = [`\u25B8 ${hooks.length} hook(s) loaded`];
|
|
6118
|
+
for (const event of HOOK_EVENTS) {
|
|
6119
|
+
const list = grouped.get(event) ?? [];
|
|
6120
|
+
if (list.length === 0) continue;
|
|
6121
|
+
lines.push("", `${event}:`);
|
|
6122
|
+
for (const h of list) {
|
|
6123
|
+
const match = h.match && h.match !== "*" ? ` match=${h.match}` : "";
|
|
6124
|
+
const desc = h.description ? ` \u2014 ${h.description}` : "";
|
|
6125
|
+
lines.push(` [${h.scope}]${match} ${h.command}${desc}`);
|
|
6126
|
+
}
|
|
6127
|
+
}
|
|
6128
|
+
lines.push("", `sources: project=${projPath ?? "(none \u2014 chat mode)"} \xB7 global=${globPath}`);
|
|
6129
|
+
return { info: lines.join("\n") };
|
|
6130
|
+
}
|
|
5807
6131
|
function handleSkillSlash(args, ctx) {
|
|
5808
6132
|
const store = new SkillStore({ projectRoot: ctx.codeRoot });
|
|
5809
6133
|
const sub = (args[0] ?? "").toLowerCase();
|
|
@@ -6129,7 +6453,12 @@ function App({
|
|
|
6129
6453
|
const [toolProgress, setToolProgress] = useState5(null);
|
|
6130
6454
|
const [statusLine, setStatusLine] = useState5(null);
|
|
6131
6455
|
const [balance, setBalance] = useState5(null);
|
|
6132
|
-
const [
|
|
6456
|
+
const [latestVersion, setLatestVersion] = useState5(null);
|
|
6457
|
+
const updateAvailable = latestVersion && compareVersions(VERSION, latestVersion) < 0 ? latestVersion : null;
|
|
6458
|
+
const [hookList, setHookList] = useState5(
|
|
6459
|
+
() => loadHooks({ projectRoot: codeMode?.rootDir })
|
|
6460
|
+
);
|
|
6461
|
+
const hookCwd = codeMode?.rootDir ?? process.cwd();
|
|
6133
6462
|
const lastEditSnapshots = useRef2(null);
|
|
6134
6463
|
const pendingEdits = useRef2([]);
|
|
6135
6464
|
const [pendingShell, setPendingShell] = useState5(null);
|
|
@@ -6185,10 +6514,23 @@ function App({
|
|
|
6185
6514
|
system,
|
|
6186
6515
|
toolSpecs: tools?.specs()
|
|
6187
6516
|
});
|
|
6188
|
-
const l = new CacheFirstLoop({
|
|
6517
|
+
const l = new CacheFirstLoop({
|
|
6518
|
+
client,
|
|
6519
|
+
prefix,
|
|
6520
|
+
tools,
|
|
6521
|
+
model,
|
|
6522
|
+
harvest: harvest2,
|
|
6523
|
+
branch,
|
|
6524
|
+
session,
|
|
6525
|
+
hooks: hookList,
|
|
6526
|
+
hookCwd
|
|
6527
|
+
});
|
|
6189
6528
|
loopRef.current = l;
|
|
6190
6529
|
return l;
|
|
6191
6530
|
}, [model, system, harvest2, branch, session, tools]);
|
|
6531
|
+
useEffect2(() => {
|
|
6532
|
+
loop.hooks = hookList;
|
|
6533
|
+
}, [loop, hookList]);
|
|
6192
6534
|
useEffect2(() => {
|
|
6193
6535
|
let cancelled = false;
|
|
6194
6536
|
void (async () => {
|
|
@@ -6206,7 +6548,7 @@ function App({
|
|
|
6206
6548
|
void (async () => {
|
|
6207
6549
|
const latest = await getLatestVersion();
|
|
6208
6550
|
if (cancelled || !latest) return;
|
|
6209
|
-
|
|
6551
|
+
setLatestVersion(latest);
|
|
6210
6552
|
})();
|
|
6211
6553
|
return () => {
|
|
6212
6554
|
cancelled = true;
|
|
@@ -6383,7 +6725,19 @@ function App({
|
|
|
6383
6725
|
memoryRoot: codeMode?.rootDir ?? process.cwd(),
|
|
6384
6726
|
planMode,
|
|
6385
6727
|
setPlanMode: codeMode ? togglePlanMode : void 0,
|
|
6386
|
-
clearPendingPlan: codeMode ? clearPendingPlan : void 0
|
|
6728
|
+
clearPendingPlan: codeMode ? clearPendingPlan : void 0,
|
|
6729
|
+
reloadHooks: () => {
|
|
6730
|
+
const fresh = loadHooks({ projectRoot: codeMode?.rootDir });
|
|
6731
|
+
setHookList(fresh);
|
|
6732
|
+
return fresh.length;
|
|
6733
|
+
},
|
|
6734
|
+
latestVersion,
|
|
6735
|
+
refreshLatestVersion: () => {
|
|
6736
|
+
void (async () => {
|
|
6737
|
+
const fresh = await getLatestVersion({ force: true });
|
|
6738
|
+
if (fresh) setLatestVersion(fresh);
|
|
6739
|
+
})();
|
|
6740
|
+
}
|
|
6387
6741
|
});
|
|
6388
6742
|
if (result.exit) {
|
|
6389
6743
|
transcriptRef.current?.end();
|
|
@@ -6421,6 +6775,23 @@ function App({
|
|
|
6421
6775
|
return;
|
|
6422
6776
|
}
|
|
6423
6777
|
}
|
|
6778
|
+
if (hookList.some((h) => h.event === "UserPromptSubmit")) {
|
|
6779
|
+
const promptReport = await runHooks({
|
|
6780
|
+
hooks: hookList,
|
|
6781
|
+
payload: { event: "UserPromptSubmit", cwd: hookCwd, prompt: text }
|
|
6782
|
+
});
|
|
6783
|
+
if (promptReport.outcomes.length > 0) {
|
|
6784
|
+
setHistorical((prev) => [
|
|
6785
|
+
...prev,
|
|
6786
|
+
...promptReport.outcomes.filter((o) => o.decision !== "pass").map((o) => ({
|
|
6787
|
+
id: `hp-${Date.now()}-${Math.random()}`,
|
|
6788
|
+
role: "warning",
|
|
6789
|
+
text: formatHookOutcomeMessage(o)
|
|
6790
|
+
}))
|
|
6791
|
+
]);
|
|
6792
|
+
}
|
|
6793
|
+
if (promptReport.blocked) return;
|
|
6794
|
+
}
|
|
6424
6795
|
promptHistory.current.push(text);
|
|
6425
6796
|
setHistorical((prev) => [...prev, { id: `u-${Date.now()}`, role: "user", text }]);
|
|
6426
6797
|
const assistantId = `a-${Date.now()}`;
|
|
@@ -6581,6 +6952,28 @@ function App({
|
|
|
6581
6952
|
}
|
|
6582
6953
|
}
|
|
6583
6954
|
flush();
|
|
6955
|
+
if (hookList.some((h) => h.event === "Stop")) {
|
|
6956
|
+
const stopReport = await runHooks({
|
|
6957
|
+
hooks: hookList,
|
|
6958
|
+
payload: {
|
|
6959
|
+
event: "Stop",
|
|
6960
|
+
cwd: hookCwd,
|
|
6961
|
+
lastAssistantText: streamRef.text,
|
|
6962
|
+
turn: loop.stats.summary().turns
|
|
6963
|
+
}
|
|
6964
|
+
});
|
|
6965
|
+
for (const o of stopReport.outcomes) {
|
|
6966
|
+
if (o.decision === "pass") continue;
|
|
6967
|
+
setHistorical((prev) => [
|
|
6968
|
+
...prev,
|
|
6969
|
+
{
|
|
6970
|
+
id: `hs-${Date.now()}-${Math.random()}`,
|
|
6971
|
+
role: "warning",
|
|
6972
|
+
text: formatHookOutcomeMessage(o)
|
|
6973
|
+
}
|
|
6974
|
+
]);
|
|
6975
|
+
}
|
|
6976
|
+
}
|
|
6584
6977
|
} finally {
|
|
6585
6978
|
if (timer) clearInterval(timer);
|
|
6586
6979
|
setStreaming(null);
|
|
@@ -6606,7 +6999,10 @@ function App({
|
|
|
6606
6999
|
codeMode,
|
|
6607
7000
|
codeUndo,
|
|
6608
7001
|
exit,
|
|
7002
|
+
hookCwd,
|
|
7003
|
+
hookList,
|
|
6609
7004
|
loop,
|
|
7005
|
+
latestVersion,
|
|
6610
7006
|
mcpSpecs,
|
|
6611
7007
|
mcpServers,
|
|
6612
7008
|
planMode,
|
|
@@ -7128,7 +7524,7 @@ async function chatCommand(opts) {
|
|
|
7128
7524
|
const prior = loadSessionMessages(opts.session);
|
|
7129
7525
|
if (prior.length > 0) {
|
|
7130
7526
|
const p = sessionPath(opts.session);
|
|
7131
|
-
const mtime =
|
|
7527
|
+
const mtime = existsSync6(p) ? statSync3(p).mtime : /* @__PURE__ */ new Date();
|
|
7132
7528
|
sessionPreview = { messageCount: prior.length, lastActive: mtime };
|
|
7133
7529
|
}
|
|
7134
7530
|
} else if (opts.session && opts.forceNew) {
|
|
@@ -8150,13 +8546,13 @@ async function setupCommand(_opts = {}) {
|
|
|
8150
8546
|
}
|
|
8151
8547
|
|
|
8152
8548
|
// src/cli/commands/stats.ts
|
|
8153
|
-
import { existsSync as
|
|
8549
|
+
import { existsSync as existsSync7, readFileSync as readFileSync8 } from "fs";
|
|
8154
8550
|
function statsCommand(opts) {
|
|
8155
|
-
if (!
|
|
8551
|
+
if (!existsSync7(opts.transcript)) {
|
|
8156
8552
|
console.error(`no such transcript: ${opts.transcript}`);
|
|
8157
8553
|
process.exit(1);
|
|
8158
8554
|
}
|
|
8159
|
-
const lines =
|
|
8555
|
+
const lines = readFileSync8(opts.transcript, "utf8").split(/\r?\n/).filter(Boolean);
|
|
8160
8556
|
let assistantTurns = 0;
|
|
8161
8557
|
let toolCalls = 0;
|
|
8162
8558
|
let lastTurn = 0;
|
|
@@ -8176,7 +8572,7 @@ function statsCommand(opts) {
|
|
|
8176
8572
|
}
|
|
8177
8573
|
|
|
8178
8574
|
// src/cli/commands/update.ts
|
|
8179
|
-
import { spawn as
|
|
8575
|
+
import { spawn as spawn4 } from "child_process";
|
|
8180
8576
|
function planUpdate(input) {
|
|
8181
8577
|
const diff = compareVersions(input.current, input.latest);
|
|
8182
8578
|
if (diff > 0) {
|
|
@@ -8207,7 +8603,7 @@ function planUpdate(input) {
|
|
|
8207
8603
|
}
|
|
8208
8604
|
function defaultSpawn(argv) {
|
|
8209
8605
|
return new Promise((resolve6, reject) => {
|
|
8210
|
-
const child =
|
|
8606
|
+
const child = spawn4(argv[0], argv.slice(1), {
|
|
8211
8607
|
stdio: "inherit",
|
|
8212
8608
|
shell: process.platform === "win32"
|
|
8213
8609
|
});
|