reasonix 0.4.21 → 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 +303 -293
- package/dist/cli/index.js +634 -43
- package/dist/cli/index.js.map +1 -1
- package/dist/index.d.ts +287 -4
- package/dist/index.js +416 -82
- 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",
|
|
@@ -4260,11 +4481,109 @@ function sep() {
|
|
|
4260
4481
|
return process.platform === "win32" ? "\\" : "/";
|
|
4261
4482
|
}
|
|
4262
4483
|
|
|
4263
|
-
// src/
|
|
4264
|
-
|
|
4484
|
+
// src/version.ts
|
|
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";
|
|
4488
|
+
import { fileURLToPath } from "url";
|
|
4489
|
+
var REGISTRY_URL = "https://registry.npmjs.org/reasonix/latest";
|
|
4490
|
+
var LATEST_CACHE_TTL_MS = 24 * 60 * 60 * 1e3;
|
|
4491
|
+
var LATEST_FETCH_TIMEOUT_MS = 2e3;
|
|
4492
|
+
function readPackageVersion() {
|
|
4493
|
+
try {
|
|
4494
|
+
let dir = dirname5(fileURLToPath(import.meta.url));
|
|
4495
|
+
for (let i = 0; i < 6; i++) {
|
|
4496
|
+
const p = join5(dir, "package.json");
|
|
4497
|
+
if (existsSync5(p)) {
|
|
4498
|
+
const pkg = JSON.parse(readFileSync7(p, "utf8"));
|
|
4499
|
+
if (pkg?.name === "reasonix" && typeof pkg.version === "string") {
|
|
4500
|
+
return pkg.version;
|
|
4501
|
+
}
|
|
4502
|
+
}
|
|
4503
|
+
const parent = dirname5(dir);
|
|
4504
|
+
if (parent === dir) break;
|
|
4505
|
+
dir = parent;
|
|
4506
|
+
}
|
|
4507
|
+
} catch {
|
|
4508
|
+
}
|
|
4509
|
+
return "0.0.0-dev";
|
|
4510
|
+
}
|
|
4511
|
+
var VERSION = readPackageVersion();
|
|
4512
|
+
function cachePath(homeDirOverride) {
|
|
4513
|
+
return join5(homeDirOverride ?? homedir4(), ".reasonix", "version-cache.json");
|
|
4514
|
+
}
|
|
4515
|
+
function readCache(homeDirOverride) {
|
|
4516
|
+
try {
|
|
4517
|
+
const raw = readFileSync7(cachePath(homeDirOverride), "utf8");
|
|
4518
|
+
const parsed = JSON.parse(raw);
|
|
4519
|
+
if (parsed && typeof parsed.version === "string" && typeof parsed.checkedAt === "number") {
|
|
4520
|
+
return parsed;
|
|
4521
|
+
}
|
|
4522
|
+
} catch {
|
|
4523
|
+
}
|
|
4524
|
+
return null;
|
|
4525
|
+
}
|
|
4526
|
+
function writeCache(entry, homeDirOverride) {
|
|
4527
|
+
try {
|
|
4528
|
+
const p = cachePath(homeDirOverride);
|
|
4529
|
+
mkdirSync4(dirname5(p), { recursive: true });
|
|
4530
|
+
writeFileSync4(p, JSON.stringify(entry), "utf8");
|
|
4531
|
+
} catch {
|
|
4532
|
+
}
|
|
4533
|
+
}
|
|
4534
|
+
async function getLatestVersion(opts = {}) {
|
|
4535
|
+
const ttl = opts.ttlMs ?? LATEST_CACHE_TTL_MS;
|
|
4536
|
+
if (!opts.force) {
|
|
4537
|
+
const cached = readCache(opts.homeDir);
|
|
4538
|
+
if (cached && Date.now() - cached.checkedAt < ttl) return cached.version;
|
|
4539
|
+
}
|
|
4540
|
+
const fetchImpl = opts.fetchImpl ?? globalThis.fetch;
|
|
4541
|
+
if (!fetchImpl) return null;
|
|
4542
|
+
const url = opts.registryUrl ?? REGISTRY_URL;
|
|
4543
|
+
const timeout = opts.timeoutMs ?? LATEST_FETCH_TIMEOUT_MS;
|
|
4544
|
+
const controller = new AbortController();
|
|
4545
|
+
const timer = setTimeout(() => controller.abort(), timeout);
|
|
4546
|
+
try {
|
|
4547
|
+
const res = await fetchImpl(url, {
|
|
4548
|
+
signal: controller.signal,
|
|
4549
|
+
headers: { accept: "application/json" }
|
|
4550
|
+
});
|
|
4551
|
+
if (!res.ok) return null;
|
|
4552
|
+
const body = await res.json();
|
|
4553
|
+
if (typeof body.version !== "string") return null;
|
|
4554
|
+
writeCache({ version: body.version, checkedAt: Date.now() }, opts.homeDir);
|
|
4555
|
+
return body.version;
|
|
4556
|
+
} catch {
|
|
4557
|
+
return null;
|
|
4558
|
+
} finally {
|
|
4559
|
+
clearTimeout(timer);
|
|
4560
|
+
}
|
|
4561
|
+
}
|
|
4562
|
+
function compareVersions(a, b) {
|
|
4563
|
+
const [aCore = "0", aPre = ""] = a.split("-", 2);
|
|
4564
|
+
const [bCore = "0", bPre = ""] = b.split("-", 2);
|
|
4565
|
+
const aParts = aCore.split(".").map((p) => Number.parseInt(p, 10) || 0);
|
|
4566
|
+
const bParts = bCore.split(".").map((p) => Number.parseInt(p, 10) || 0);
|
|
4567
|
+
for (let i = 0; i < 3; i++) {
|
|
4568
|
+
const diff = (aParts[i] ?? 0) - (bParts[i] ?? 0);
|
|
4569
|
+
if (diff !== 0) return diff;
|
|
4570
|
+
}
|
|
4571
|
+
if (!aPre && !bPre) return 0;
|
|
4572
|
+
if (!aPre) return 1;
|
|
4573
|
+
if (!bPre) return -1;
|
|
4574
|
+
return aPre < bPre ? -1 : aPre > bPre ? 1 : 0;
|
|
4575
|
+
}
|
|
4576
|
+
function isNpxInstall() {
|
|
4577
|
+
const bin = process.argv[1] ?? "";
|
|
4578
|
+
if (/[/\\]_npx[/\\]/.test(bin)) return true;
|
|
4579
|
+
if (/[/\\]\.pnpm[/\\]/.test(bin) && /dlx/i.test(bin)) return true;
|
|
4580
|
+
const ua = process.env.npm_config_user_agent ?? "";
|
|
4581
|
+
if (ua.includes("npx/")) return true;
|
|
4582
|
+
return false;
|
|
4583
|
+
}
|
|
4265
4584
|
|
|
4266
4585
|
// src/cli/commands/chat.tsx
|
|
4267
|
-
import { existsSync as
|
|
4586
|
+
import { existsSync as existsSync6, statSync as statSync3 } from "fs";
|
|
4268
4587
|
import { render } from "ink";
|
|
4269
4588
|
import React15, { useState as useState7 } from "react";
|
|
4270
4589
|
|
|
@@ -5255,7 +5574,8 @@ function StatsPanel({
|
|
|
5255
5574
|
harvestOn,
|
|
5256
5575
|
branchBudget,
|
|
5257
5576
|
planMode,
|
|
5258
|
-
balance
|
|
5577
|
+
balance,
|
|
5578
|
+
updateAvailable
|
|
5259
5579
|
}) {
|
|
5260
5580
|
const hitPct = (summary.cacheHitRatio * 100).toFixed(1);
|
|
5261
5581
|
const hitColor = summary.cacheHitRatio >= 0.7 ? "green" : summary.cacheHitRatio >= 0.4 ? "yellow" : "red";
|
|
@@ -5263,7 +5583,7 @@ function StatsPanel({
|
|
|
5263
5583
|
const ctxMax = DEEPSEEK_CONTEXT_TOKENS[model] ?? DEFAULT_CONTEXT_TOKENS;
|
|
5264
5584
|
const ctxRatio = summary.lastPromptTokens / ctxMax;
|
|
5265
5585
|
const ctxColor = ctxRatio >= 0.8 ? "red" : ctxRatio >= 0.5 ? "yellow" : void 0;
|
|
5266
|
-
return /* @__PURE__ */ React11.createElement(Box10, { borderStyle: "round", borderColor: "cyan", flexDirection: "column", paddingX: 1 }, /* @__PURE__ */ React11.createElement(Box10, { justifyContent: "space-between" }, /* @__PURE__ */ React11.createElement(Text10, null, /* @__PURE__ */ React11.createElement(Text10, { color: "cyan", bold: true }, "Reasonix"), /* @__PURE__ */ React11.createElement(Text10, { dimColor: true }, " \xB7 model "), /* @__PURE__ */ React11.createElement(Text10, { color: "yellow" }, model), /* @__PURE__ */ React11.createElement(Text10, { dimColor: true }, " \xB7 prefix "), /* @__PURE__ */ React11.createElement(Text10, { dimColor: true }, prefixHash), harvestOn ? /* @__PURE__ */ React11.createElement(Text10, { color: "magenta" }, " \xB7 harvest") : null, branchOn ? /* @__PURE__ */ React11.createElement(Text10, { color: "blue" }, " \xB7 branch", branchBudget) : null, planMode ? /* @__PURE__ */ React11.createElement(Text10, { color: "red", bold: true }, " ", "\xB7 PLAN") : null), /* @__PURE__ */ React11.createElement(Text10, { dimColor: true }, "turns ", summary.turns, " \xB7 type /help")), /* @__PURE__ */ React11.createElement(Box10, { marginTop: 1, gap: 3 }, /* @__PURE__ */ React11.createElement(Text10, null, /* @__PURE__ */ React11.createElement(Text10, { dimColor: true }, "cache hit "), /* @__PURE__ */ React11.createElement(Text10, { color: hitColor, bold: true }, hitPct, "%")), /* @__PURE__ */ React11.createElement(Text10, null, /* @__PURE__ */ React11.createElement(Text10, { dimColor: true }, "cost "), /* @__PURE__ */ React11.createElement(Text10, { color: "green", bold: true }, "$", summary.totalCostUsd.toFixed(6)), /* @__PURE__ */ React11.createElement(Text10, { dimColor: true }, " (in ", "$", summary.totalInputCostUsd.toFixed(6), " \xB7 out ", "$", summary.totalOutputCostUsd.toFixed(6), ")")), summary.lastPromptTokens > 0 ? /* @__PURE__ */ React11.createElement(Text10, null, /* @__PURE__ */ React11.createElement(Text10, { dimColor: true }, "ctx "), /* @__PURE__ */ React11.createElement(Text10, { color: ctxColor, bold: ctxColor !== void 0 }, formatTokens(summary.lastPromptTokens), "/", formatTokens(ctxMax)), /* @__PURE__ */ React11.createElement(Text10, { dimColor: true }, " (", (ctxRatio * 100).toFixed(0), "%)"), ctxRatio >= 0.8 ? /* @__PURE__ */ React11.createElement(Text10, { color: "red", bold: true }, " ", "\xB7 /compact") : null) : null, balance ? /* @__PURE__ */ React11.createElement(Text10, null, /* @__PURE__ */ React11.createElement(Text10, { dimColor: true }, "balance "), /* @__PURE__ */ React11.createElement(Text10, { color: balance.total < 1 ? "red" : balance.total < 5 ? "yellow" : "green", bold: true }, balance.currency === "USD" ? "$" : "", balance.total.toFixed(2), balance.currency !== "USD" ? ` ${balance.currency}` : "")) : null));
|
|
5586
|
+
return /* @__PURE__ */ React11.createElement(Box10, { borderStyle: "round", borderColor: "cyan", flexDirection: "column", paddingX: 1 }, /* @__PURE__ */ React11.createElement(Box10, { justifyContent: "space-between" }, /* @__PURE__ */ React11.createElement(Text10, null, /* @__PURE__ */ React11.createElement(Text10, { color: "cyan", bold: true }, "Reasonix"), /* @__PURE__ */ React11.createElement(Text10, { dimColor: true }, ` v${VERSION}`), /* @__PURE__ */ React11.createElement(Text10, { dimColor: true }, " \xB7 model "), /* @__PURE__ */ React11.createElement(Text10, { color: "yellow" }, model), /* @__PURE__ */ React11.createElement(Text10, { dimColor: true }, " \xB7 prefix "), /* @__PURE__ */ React11.createElement(Text10, { dimColor: true }, prefixHash), harvestOn ? /* @__PURE__ */ React11.createElement(Text10, { color: "magenta" }, " \xB7 harvest") : null, branchOn ? /* @__PURE__ */ React11.createElement(Text10, { color: "blue" }, " \xB7 branch", branchBudget) : null, planMode ? /* @__PURE__ */ React11.createElement(Text10, { color: "red", bold: true }, " ", "\xB7 PLAN") : null), /* @__PURE__ */ React11.createElement(Text10, null, updateAvailable ? /* @__PURE__ */ React11.createElement(Text10, { color: "yellow", bold: true }, `update: ${updateAvailable} \xB7 `) : null, /* @__PURE__ */ React11.createElement(Text10, { dimColor: true }, "turns ", summary.turns, " \xB7 type /help"))), /* @__PURE__ */ React11.createElement(Box10, { marginTop: 1, gap: 3 }, /* @__PURE__ */ React11.createElement(Text10, null, /* @__PURE__ */ React11.createElement(Text10, { dimColor: true }, "cache hit "), /* @__PURE__ */ React11.createElement(Text10, { color: hitColor, bold: true }, hitPct, "%")), /* @__PURE__ */ React11.createElement(Text10, null, /* @__PURE__ */ React11.createElement(Text10, { dimColor: true }, "cost "), /* @__PURE__ */ React11.createElement(Text10, { color: "green", bold: true }, "$", summary.totalCostUsd.toFixed(6)), /* @__PURE__ */ React11.createElement(Text10, { dimColor: true }, " (in ", "$", summary.totalInputCostUsd.toFixed(6), " \xB7 out ", "$", summary.totalOutputCostUsd.toFixed(6), ")")), summary.lastPromptTokens > 0 ? /* @__PURE__ */ React11.createElement(Text10, null, /* @__PURE__ */ React11.createElement(Text10, { dimColor: true }, "ctx "), /* @__PURE__ */ React11.createElement(Text10, { color: ctxColor, bold: ctxColor !== void 0 }, formatTokens(summary.lastPromptTokens), "/", formatTokens(ctxMax)), /* @__PURE__ */ React11.createElement(Text10, { dimColor: true }, " (", (ctxRatio * 100).toFixed(0), "%)"), ctxRatio >= 0.8 ? /* @__PURE__ */ React11.createElement(Text10, { color: "red", bold: true }, " ", "\xB7 /compact") : null) : null, balance ? /* @__PURE__ */ React11.createElement(Text10, null, /* @__PURE__ */ React11.createElement(Text10, { dimColor: true }, "balance "), /* @__PURE__ */ React11.createElement(Text10, { color: balance.total < 1 ? "red" : balance.total < 5 ? "yellow" : "green", bold: true }, balance.currency === "USD" ? "$" : "", balance.total.toFixed(2), balance.currency !== "USD" ? ` ${balance.currency}` : "")) : null));
|
|
5267
5587
|
}
|
|
5268
5588
|
function formatTokens(n) {
|
|
5269
5589
|
if (n < 1e3) return String(n);
|
|
@@ -5296,6 +5616,15 @@ var SLASH_COMMANDS = [
|
|
|
5296
5616
|
argsHint: "[list|show <name>|<name> [args]]",
|
|
5297
5617
|
summary: "list / run user skills (<project>/.reasonix/skills + ~/.reasonix/skills)"
|
|
5298
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
|
+
},
|
|
5299
5628
|
{ cmd: "think", summary: "dump the last turn's full R1 reasoning (reasoner only)" },
|
|
5300
5629
|
{ cmd: "retry", summary: "truncate & resend your last message (fresh sample)" },
|
|
5301
5630
|
{ cmd: "compact", argsHint: "[cap]", summary: "shrink oversized tool results in the log" },
|
|
@@ -5469,6 +5798,13 @@ function handleSlash(cmd, args, loop, ctx = {}) {
|
|
|
5469
5798
|
case "skills": {
|
|
5470
5799
|
return handleSkillSlash(args, ctx);
|
|
5471
5800
|
}
|
|
5801
|
+
case "hook":
|
|
5802
|
+
case "hooks": {
|
|
5803
|
+
return handleHooksSlash(args, loop, ctx);
|
|
5804
|
+
}
|
|
5805
|
+
case "update": {
|
|
5806
|
+
return handleUpdateSlash(ctx);
|
|
5807
|
+
}
|
|
5472
5808
|
case "think":
|
|
5473
5809
|
case "reasoning": {
|
|
5474
5810
|
const raw = loop.scratch.reasoning;
|
|
@@ -5705,6 +6041,93 @@ ${entry.text}`
|
|
|
5705
6041
|
return { unknown: true, info: `unknown command: /${cmd} (try /help)` };
|
|
5706
6042
|
}
|
|
5707
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
|
+
}
|
|
5708
6131
|
function handleSkillSlash(args, ctx) {
|
|
5709
6132
|
const store = new SkillStore({ projectRoot: ctx.codeRoot });
|
|
5710
6133
|
const sub = (args[0] ?? "").toLowerCase();
|
|
@@ -6030,6 +6453,12 @@ function App({
|
|
|
6030
6453
|
const [toolProgress, setToolProgress] = useState5(null);
|
|
6031
6454
|
const [statusLine, setStatusLine] = useState5(null);
|
|
6032
6455
|
const [balance, setBalance] = useState5(null);
|
|
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();
|
|
6033
6462
|
const lastEditSnapshots = useRef2(null);
|
|
6034
6463
|
const pendingEdits = useRef2([]);
|
|
6035
6464
|
const [pendingShell, setPendingShell] = useState5(null);
|
|
@@ -6085,10 +6514,23 @@ function App({
|
|
|
6085
6514
|
system,
|
|
6086
6515
|
toolSpecs: tools?.specs()
|
|
6087
6516
|
});
|
|
6088
|
-
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
|
+
});
|
|
6089
6528
|
loopRef.current = l;
|
|
6090
6529
|
return l;
|
|
6091
6530
|
}, [model, system, harvest2, branch, session, tools]);
|
|
6531
|
+
useEffect2(() => {
|
|
6532
|
+
loop.hooks = hookList;
|
|
6533
|
+
}, [loop, hookList]);
|
|
6092
6534
|
useEffect2(() => {
|
|
6093
6535
|
let cancelled = false;
|
|
6094
6536
|
void (async () => {
|
|
@@ -6101,6 +6543,17 @@ function App({
|
|
|
6101
6543
|
cancelled = true;
|
|
6102
6544
|
};
|
|
6103
6545
|
}, [loop]);
|
|
6546
|
+
useEffect2(() => {
|
|
6547
|
+
let cancelled = false;
|
|
6548
|
+
void (async () => {
|
|
6549
|
+
const latest = await getLatestVersion();
|
|
6550
|
+
if (cancelled || !latest) return;
|
|
6551
|
+
setLatestVersion(latest);
|
|
6552
|
+
})();
|
|
6553
|
+
return () => {
|
|
6554
|
+
cancelled = true;
|
|
6555
|
+
};
|
|
6556
|
+
}, []);
|
|
6104
6557
|
useEffect2(() => {
|
|
6105
6558
|
if (!progressSink) return;
|
|
6106
6559
|
progressSink.current = (info) => {
|
|
@@ -6272,7 +6725,19 @@ function App({
|
|
|
6272
6725
|
memoryRoot: codeMode?.rootDir ?? process.cwd(),
|
|
6273
6726
|
planMode,
|
|
6274
6727
|
setPlanMode: codeMode ? togglePlanMode : void 0,
|
|
6275
|
-
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
|
+
}
|
|
6276
6741
|
});
|
|
6277
6742
|
if (result.exit) {
|
|
6278
6743
|
transcriptRef.current?.end();
|
|
@@ -6310,6 +6775,23 @@ function App({
|
|
|
6310
6775
|
return;
|
|
6311
6776
|
}
|
|
6312
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
|
+
}
|
|
6313
6795
|
promptHistory.current.push(text);
|
|
6314
6796
|
setHistorical((prev) => [...prev, { id: `u-${Date.now()}`, role: "user", text }]);
|
|
6315
6797
|
const assistantId = `a-${Date.now()}`;
|
|
@@ -6470,6 +6952,28 @@ function App({
|
|
|
6470
6952
|
}
|
|
6471
6953
|
}
|
|
6472
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
|
+
}
|
|
6473
6977
|
} finally {
|
|
6474
6978
|
if (timer) clearInterval(timer);
|
|
6475
6979
|
setStreaming(null);
|
|
@@ -6495,7 +6999,10 @@ function App({
|
|
|
6495
6999
|
codeMode,
|
|
6496
7000
|
codeUndo,
|
|
6497
7001
|
exit,
|
|
7002
|
+
hookCwd,
|
|
7003
|
+
hookList,
|
|
6498
7004
|
loop,
|
|
7005
|
+
latestVersion,
|
|
6499
7006
|
mcpSpecs,
|
|
6500
7007
|
mcpServers,
|
|
6501
7008
|
planMode,
|
|
@@ -6659,7 +7166,8 @@ Stay in plan mode \u2014 address the feedback (explore more if needed), then sub
|
|
|
6659
7166
|
harvestOn: loop.harvestEnabled,
|
|
6660
7167
|
branchBudget: loop.branchOptions.budget,
|
|
6661
7168
|
planMode,
|
|
6662
|
-
balance
|
|
7169
|
+
balance,
|
|
7170
|
+
updateAvailable
|
|
6663
7171
|
}
|
|
6664
7172
|
), /* @__PURE__ */ React12.createElement(Static, { items: historical }, (item) => /* @__PURE__ */ React12.createElement(EventRow, { key: item.id, event: item })), !PLAIN_UI && !pendingShell && !pendingPlan && !stagedInput && streaming ? /* @__PURE__ */ React12.createElement(Box11, { marginY: 1 }, /* @__PURE__ */ React12.createElement(EventRow, { event: streaming })) : null, !PLAIN_UI && !pendingShell && !pendingPlan && !stagedInput && ongoingTool ? /* @__PURE__ */ React12.createElement(OngoingToolRow, { tool: ongoingTool, progress: toolProgress }) : null, !PLAIN_UI && !pendingShell && !pendingPlan && !stagedInput && !ongoingTool && statusLine ? /* @__PURE__ */ React12.createElement(StatusRow, { text: statusLine }) : null, !PLAIN_UI && !pendingShell && !pendingPlan && !stagedInput && busy && !streaming && !ongoingTool && !statusLine ? /* @__PURE__ */ React12.createElement(StatusRow, { text: "processing\u2026" }) : null, stagedInput ? /* @__PURE__ */ React12.createElement(
|
|
6665
7173
|
PlanRefineInput,
|
|
@@ -7016,7 +7524,7 @@ async function chatCommand(opts) {
|
|
|
7016
7524
|
const prior = loadSessionMessages(opts.session);
|
|
7017
7525
|
if (prior.length > 0) {
|
|
7018
7526
|
const p = sessionPath(opts.session);
|
|
7019
|
-
const mtime =
|
|
7527
|
+
const mtime = existsSync6(p) ? statSync3(p).mtime : /* @__PURE__ */ new Date();
|
|
7020
7528
|
sessionPreview = { messageCount: prior.length, lastActive: mtime };
|
|
7021
7529
|
}
|
|
7022
7530
|
} else if (opts.session && opts.forceNew) {
|
|
@@ -7085,7 +7593,7 @@ async function codeCommand(opts = {}) {
|
|
|
7085
7593
|
}
|
|
7086
7594
|
|
|
7087
7595
|
// src/cli/commands/diff.ts
|
|
7088
|
-
import { writeFileSync as
|
|
7596
|
+
import { writeFileSync as writeFileSync5 } from "fs";
|
|
7089
7597
|
import { basename as basename2 } from "path";
|
|
7090
7598
|
import { render as render2 } from "ink";
|
|
7091
7599
|
import React18 from "react";
|
|
@@ -7231,7 +7739,7 @@ async function diffCommand(opts) {
|
|
|
7231
7739
|
if (wantMarkdown) {
|
|
7232
7740
|
console.log(renderSummaryTable(report));
|
|
7233
7741
|
const md = renderMarkdown(report);
|
|
7234
|
-
|
|
7742
|
+
writeFileSync5(opts.mdPath, md, "utf8");
|
|
7235
7743
|
console.log(`
|
|
7236
7744
|
markdown report written to ${opts.mdPath}`);
|
|
7237
7745
|
return;
|
|
@@ -8038,13 +8546,13 @@ async function setupCommand(_opts = {}) {
|
|
|
8038
8546
|
}
|
|
8039
8547
|
|
|
8040
8548
|
// src/cli/commands/stats.ts
|
|
8041
|
-
import { existsSync as
|
|
8549
|
+
import { existsSync as existsSync7, readFileSync as readFileSync8 } from "fs";
|
|
8042
8550
|
function statsCommand(opts) {
|
|
8043
|
-
if (!
|
|
8551
|
+
if (!existsSync7(opts.transcript)) {
|
|
8044
8552
|
console.error(`no such transcript: ${opts.transcript}`);
|
|
8045
8553
|
process.exit(1);
|
|
8046
8554
|
}
|
|
8047
|
-
const lines =
|
|
8555
|
+
const lines = readFileSync8(opts.transcript, "utf8").split(/\r?\n/).filter(Boolean);
|
|
8048
8556
|
let assistantTurns = 0;
|
|
8049
8557
|
let toolCalls = 0;
|
|
8050
8558
|
let lastTurn = 0;
|
|
@@ -8063,6 +8571,84 @@ function statsCommand(opts) {
|
|
|
8063
8571
|
console.log(`last turn index: ${lastTurn}`);
|
|
8064
8572
|
}
|
|
8065
8573
|
|
|
8574
|
+
// src/cli/commands/update.ts
|
|
8575
|
+
import { spawn as spawn4 } from "child_process";
|
|
8576
|
+
function planUpdate(input) {
|
|
8577
|
+
const diff = compareVersions(input.current, input.latest);
|
|
8578
|
+
if (diff > 0) {
|
|
8579
|
+
return {
|
|
8580
|
+
action: "newer-local",
|
|
8581
|
+
message: `current (${input.current}) is newer than the published ${input.latest} \u2014 nothing to do.`
|
|
8582
|
+
};
|
|
8583
|
+
}
|
|
8584
|
+
if (diff === 0) {
|
|
8585
|
+
return { action: "up-to-date", message: `reasonix ${input.current} is up to date.` };
|
|
8586
|
+
}
|
|
8587
|
+
if (input.npx) {
|
|
8588
|
+
return {
|
|
8589
|
+
action: "npx-hint",
|
|
8590
|
+
message: [
|
|
8591
|
+
`reasonix ${input.latest} is available.`,
|
|
8592
|
+
"you're running via npx \u2014 the next `npx reasonix ...` launch will auto-fetch",
|
|
8593
|
+
"the latest (npx caches packages for a short window). to force a refresh",
|
|
8594
|
+
"sooner, clear the cache: `npm cache clean --force`."
|
|
8595
|
+
].join("\n")
|
|
8596
|
+
};
|
|
8597
|
+
}
|
|
8598
|
+
return {
|
|
8599
|
+
action: "run-npm-install",
|
|
8600
|
+
message: `upgrading reasonix ${input.current} \u2192 ${input.latest}`,
|
|
8601
|
+
command: ["npm", "install", "-g", "reasonix@latest"]
|
|
8602
|
+
};
|
|
8603
|
+
}
|
|
8604
|
+
function defaultSpawn(argv) {
|
|
8605
|
+
return new Promise((resolve6, reject) => {
|
|
8606
|
+
const child = spawn4(argv[0], argv.slice(1), {
|
|
8607
|
+
stdio: "inherit",
|
|
8608
|
+
shell: process.platform === "win32"
|
|
8609
|
+
});
|
|
8610
|
+
child.once("error", reject);
|
|
8611
|
+
child.once("exit", (code) => resolve6(code ?? 1));
|
|
8612
|
+
});
|
|
8613
|
+
}
|
|
8614
|
+
async function updateCommand(opts = {}) {
|
|
8615
|
+
const write = opts.write ?? ((m) => process.stdout.write(m));
|
|
8616
|
+
const exit = opts.exit ?? ((c) => process.exit(c));
|
|
8617
|
+
const fetchLatest = opts.fetchLatest ?? (() => getLatestVersion({ force: true }));
|
|
8618
|
+
const isNpx = opts.isNpx ?? isNpxInstall;
|
|
8619
|
+
const doSpawn = opts.spawnInstall ?? defaultSpawn;
|
|
8620
|
+
write(`current: reasonix ${VERSION}
|
|
8621
|
+
`);
|
|
8622
|
+
const latest = await fetchLatest();
|
|
8623
|
+
if (!latest) {
|
|
8624
|
+
write("could not reach registry.npmjs.org \u2014 check your network.\n");
|
|
8625
|
+
exit(1);
|
|
8626
|
+
return;
|
|
8627
|
+
}
|
|
8628
|
+
write(`latest: reasonix ${latest}
|
|
8629
|
+
`);
|
|
8630
|
+
const plan = planUpdate({ current: VERSION, latest, npx: isNpx() });
|
|
8631
|
+
write(`
|
|
8632
|
+
${plan.message}
|
|
8633
|
+
`);
|
|
8634
|
+
if (plan.action !== "run-npm-install" || !plan.command) return;
|
|
8635
|
+
if (opts.dryRun) {
|
|
8636
|
+
write(`(dry run) would run: ${plan.command.join(" ")}
|
|
8637
|
+
`);
|
|
8638
|
+
return;
|
|
8639
|
+
}
|
|
8640
|
+
write(`
|
|
8641
|
+
running: ${plan.command.join(" ")}
|
|
8642
|
+
`);
|
|
8643
|
+
const code = await doSpawn(plan.command);
|
|
8644
|
+
if (code !== 0) {
|
|
8645
|
+
write(`
|
|
8646
|
+
npm exited with code ${code}. upgrade did not complete.
|
|
8647
|
+
`);
|
|
8648
|
+
exit(code);
|
|
8649
|
+
}
|
|
8650
|
+
}
|
|
8651
|
+
|
|
8066
8652
|
// src/cli/commands/version.ts
|
|
8067
8653
|
function versionCommand() {
|
|
8068
8654
|
console.log(`reasonix ${VERSION}`);
|
|
@@ -8255,6 +8841,11 @@ mcp.command("inspect <spec>").description(
|
|
|
8255
8841
|
}
|
|
8256
8842
|
});
|
|
8257
8843
|
program.command("version").description("Print Reasonix version.").action(versionCommand);
|
|
8844
|
+
program.command("update").description(
|
|
8845
|
+
"Check the npm registry for a newer Reasonix and install it. Detects npx vs global install; for npx users, prints a cache-refresh hint instead of running `npm i -g`."
|
|
8846
|
+
).option("--dry-run", "Print the plan without executing the install").action(async (opts) => {
|
|
8847
|
+
await updateCommand({ dryRun: !!opts.dryRun });
|
|
8848
|
+
});
|
|
8258
8849
|
program.parseAsync(process.argv).catch((err) => {
|
|
8259
8850
|
console.error(err);
|
|
8260
8851
|
process.exit(1);
|