reasonix 0.4.22 → 0.4.24
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 +77 -0
- package/dist/cli/index.js +738 -96
- package/dist/cli/index.js.map +1 -1
- package/dist/index.d.ts +329 -1
- package/dist/index.js +452 -88
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -427,6 +427,174 @@ function resolveTemperatures(budget, custom) {
|
|
|
427
427
|
return out;
|
|
428
428
|
}
|
|
429
429
|
|
|
430
|
+
// src/hooks.ts
|
|
431
|
+
import { spawn } from "child_process";
|
|
432
|
+
import { existsSync, readFileSync } from "fs";
|
|
433
|
+
import { homedir } from "os";
|
|
434
|
+
import { join } from "path";
|
|
435
|
+
var HOOK_EVENTS = [
|
|
436
|
+
"PreToolUse",
|
|
437
|
+
"PostToolUse",
|
|
438
|
+
"UserPromptSubmit",
|
|
439
|
+
"Stop"
|
|
440
|
+
];
|
|
441
|
+
var BLOCKING_EVENTS = /* @__PURE__ */ new Set(["PreToolUse", "UserPromptSubmit"]);
|
|
442
|
+
var DEFAULT_TIMEOUTS_MS = {
|
|
443
|
+
PreToolUse: 5e3,
|
|
444
|
+
UserPromptSubmit: 5e3,
|
|
445
|
+
PostToolUse: 3e4,
|
|
446
|
+
Stop: 3e4
|
|
447
|
+
};
|
|
448
|
+
var HOOK_SETTINGS_FILENAME = "settings.json";
|
|
449
|
+
var HOOK_SETTINGS_DIRNAME = ".reasonix";
|
|
450
|
+
function globalSettingsPath(homeDirOverride) {
|
|
451
|
+
return join(homeDirOverride ?? homedir(), HOOK_SETTINGS_DIRNAME, HOOK_SETTINGS_FILENAME);
|
|
452
|
+
}
|
|
453
|
+
function projectSettingsPath(projectRoot) {
|
|
454
|
+
return join(projectRoot, HOOK_SETTINGS_DIRNAME, HOOK_SETTINGS_FILENAME);
|
|
455
|
+
}
|
|
456
|
+
function readSettingsFile(path) {
|
|
457
|
+
if (!existsSync(path)) return null;
|
|
458
|
+
try {
|
|
459
|
+
const raw = readFileSync(path, "utf8");
|
|
460
|
+
const parsed = JSON.parse(raw);
|
|
461
|
+
if (parsed && typeof parsed === "object") return parsed;
|
|
462
|
+
} catch {
|
|
463
|
+
}
|
|
464
|
+
return null;
|
|
465
|
+
}
|
|
466
|
+
function loadHooks(opts = {}) {
|
|
467
|
+
const out = [];
|
|
468
|
+
if (opts.projectRoot) {
|
|
469
|
+
const projPath = projectSettingsPath(opts.projectRoot);
|
|
470
|
+
const settings2 = readSettingsFile(projPath);
|
|
471
|
+
if (settings2) appendResolved(out, settings2, "project", projPath);
|
|
472
|
+
}
|
|
473
|
+
const globalPath = globalSettingsPath(opts.homeDir);
|
|
474
|
+
const settings = readSettingsFile(globalPath);
|
|
475
|
+
if (settings) appendResolved(out, settings, "global", globalPath);
|
|
476
|
+
return out;
|
|
477
|
+
}
|
|
478
|
+
function appendResolved(out, settings, scope, source) {
|
|
479
|
+
if (!settings.hooks) return;
|
|
480
|
+
for (const event of HOOK_EVENTS) {
|
|
481
|
+
const list = settings.hooks[event];
|
|
482
|
+
if (!Array.isArray(list)) continue;
|
|
483
|
+
for (const cfg of list) {
|
|
484
|
+
if (!cfg || typeof cfg.command !== "string" || cfg.command.trim() === "") continue;
|
|
485
|
+
out.push({ ...cfg, event, scope, source });
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
function matchesTool(hook, toolName) {
|
|
490
|
+
if (hook.event !== "PreToolUse" && hook.event !== "PostToolUse") return true;
|
|
491
|
+
const m = hook.match;
|
|
492
|
+
if (!m || m === "*") return true;
|
|
493
|
+
try {
|
|
494
|
+
const re = new RegExp(`^(?:${m})$`);
|
|
495
|
+
return re.test(toolName);
|
|
496
|
+
} catch {
|
|
497
|
+
return false;
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
function defaultSpawner(input) {
|
|
501
|
+
return new Promise((resolve7) => {
|
|
502
|
+
const child = spawn(input.command, {
|
|
503
|
+
cwd: input.cwd,
|
|
504
|
+
shell: true,
|
|
505
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
506
|
+
});
|
|
507
|
+
let stdout = "";
|
|
508
|
+
let stderr = "";
|
|
509
|
+
let timedOut = false;
|
|
510
|
+
const timer = setTimeout(() => {
|
|
511
|
+
timedOut = true;
|
|
512
|
+
child.kill("SIGTERM");
|
|
513
|
+
setTimeout(() => {
|
|
514
|
+
try {
|
|
515
|
+
child.kill("SIGKILL");
|
|
516
|
+
} catch {
|
|
517
|
+
}
|
|
518
|
+
}, 500);
|
|
519
|
+
}, input.timeoutMs);
|
|
520
|
+
child.stdout.on("data", (chunk) => {
|
|
521
|
+
stdout += chunk.toString("utf8");
|
|
522
|
+
});
|
|
523
|
+
child.stderr.on("data", (chunk) => {
|
|
524
|
+
stderr += chunk.toString("utf8");
|
|
525
|
+
});
|
|
526
|
+
child.once("error", (err) => {
|
|
527
|
+
clearTimeout(timer);
|
|
528
|
+
resolve7({
|
|
529
|
+
exitCode: null,
|
|
530
|
+
stdout,
|
|
531
|
+
stderr,
|
|
532
|
+
timedOut: false,
|
|
533
|
+
spawnError: err
|
|
534
|
+
});
|
|
535
|
+
});
|
|
536
|
+
child.once("close", (code) => {
|
|
537
|
+
clearTimeout(timer);
|
|
538
|
+
resolve7({
|
|
539
|
+
exitCode: code,
|
|
540
|
+
stdout: stdout.trim(),
|
|
541
|
+
stderr: stderr.trim(),
|
|
542
|
+
timedOut
|
|
543
|
+
});
|
|
544
|
+
});
|
|
545
|
+
try {
|
|
546
|
+
child.stdin.write(input.stdin);
|
|
547
|
+
child.stdin.end();
|
|
548
|
+
} catch {
|
|
549
|
+
}
|
|
550
|
+
});
|
|
551
|
+
}
|
|
552
|
+
function formatHookOutcomeMessage(outcome) {
|
|
553
|
+
if (outcome.decision === "pass") return "";
|
|
554
|
+
const detail = (outcome.stderr || outcome.stdout || "").trim();
|
|
555
|
+
const tag = `${outcome.hook.scope}/${outcome.hook.event}`;
|
|
556
|
+
const cmd = outcome.hook.command.length > 60 ? `${outcome.hook.command.slice(0, 60)}\u2026` : outcome.hook.command;
|
|
557
|
+
const head = `hook ${tag} \`${cmd}\` ${outcome.decision}`;
|
|
558
|
+
return detail ? `${head}: ${detail}` : head;
|
|
559
|
+
}
|
|
560
|
+
function decideOutcome(event, raw) {
|
|
561
|
+
if (raw.spawnError) return "error";
|
|
562
|
+
if (raw.timedOut) return BLOCKING_EVENTS.has(event) ? "block" : "warn";
|
|
563
|
+
if (raw.exitCode === 0) return "pass";
|
|
564
|
+
if (raw.exitCode === 2 && BLOCKING_EVENTS.has(event)) return "block";
|
|
565
|
+
return "warn";
|
|
566
|
+
}
|
|
567
|
+
async function runHooks(opts) {
|
|
568
|
+
const spawner = opts.spawner ?? defaultSpawner;
|
|
569
|
+
const event = opts.payload.event;
|
|
570
|
+
const toolName = opts.payload.toolName ?? "";
|
|
571
|
+
const matching = opts.hooks.filter((h) => h.event === event && matchesTool(h, toolName));
|
|
572
|
+
const outcomes = [];
|
|
573
|
+
let blocked = false;
|
|
574
|
+
const stdin = `${JSON.stringify(opts.payload)}
|
|
575
|
+
`;
|
|
576
|
+
for (const hook of matching) {
|
|
577
|
+
const start = Date.now();
|
|
578
|
+
const timeoutMs = hook.timeout ?? DEFAULT_TIMEOUTS_MS[event];
|
|
579
|
+
const cwd = hook.cwd ?? opts.payload.cwd;
|
|
580
|
+
const raw = await spawner({ command: hook.command, cwd, stdin, timeoutMs });
|
|
581
|
+
const decision = decideOutcome(event, raw);
|
|
582
|
+
outcomes.push({
|
|
583
|
+
hook,
|
|
584
|
+
decision,
|
|
585
|
+
exitCode: raw.exitCode,
|
|
586
|
+
stdout: raw.stdout,
|
|
587
|
+
stderr: raw.stderr || (raw.spawnError ? raw.spawnError.message : "") || (raw.timedOut ? `hook timed out after ${timeoutMs}ms` : ""),
|
|
588
|
+
durationMs: Date.now() - start
|
|
589
|
+
});
|
|
590
|
+
if (decision === "block") {
|
|
591
|
+
blocked = true;
|
|
592
|
+
break;
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
return { event, outcomes, blocked };
|
|
596
|
+
}
|
|
597
|
+
|
|
430
598
|
// src/repair/flatten.ts
|
|
431
599
|
function analyzeSchema(schema) {
|
|
432
600
|
if (!schema) return { shouldFlatten: false, leafCount: 0, maxDepth: 0 };
|
|
@@ -1049,21 +1217,21 @@ function signature2(call) {
|
|
|
1049
1217
|
import {
|
|
1050
1218
|
appendFileSync,
|
|
1051
1219
|
chmodSync,
|
|
1052
|
-
existsSync,
|
|
1220
|
+
existsSync as existsSync2,
|
|
1053
1221
|
mkdirSync,
|
|
1054
|
-
readFileSync,
|
|
1222
|
+
readFileSync as readFileSync2,
|
|
1055
1223
|
readdirSync,
|
|
1056
1224
|
statSync,
|
|
1057
1225
|
unlinkSync,
|
|
1058
1226
|
writeFileSync
|
|
1059
1227
|
} from "fs";
|
|
1060
|
-
import { homedir } from "os";
|
|
1061
|
-
import { dirname, join } from "path";
|
|
1228
|
+
import { homedir as homedir2 } from "os";
|
|
1229
|
+
import { dirname, join as join2 } from "path";
|
|
1062
1230
|
function sessionsDir() {
|
|
1063
|
-
return
|
|
1231
|
+
return join2(homedir2(), ".reasonix", "sessions");
|
|
1064
1232
|
}
|
|
1065
1233
|
function sessionPath(name) {
|
|
1066
|
-
return
|
|
1234
|
+
return join2(sessionsDir(), `${sanitizeName(name)}.jsonl`);
|
|
1067
1235
|
}
|
|
1068
1236
|
function sanitizeName(name) {
|
|
1069
1237
|
const cleaned = name.replace(/[^\w\-\u4e00-\u9fa5]/g, "_").slice(0, 64);
|
|
@@ -1071,9 +1239,9 @@ function sanitizeName(name) {
|
|
|
1071
1239
|
}
|
|
1072
1240
|
function loadSessionMessages(name) {
|
|
1073
1241
|
const path = sessionPath(name);
|
|
1074
|
-
if (!
|
|
1242
|
+
if (!existsSync2(path)) return [];
|
|
1075
1243
|
try {
|
|
1076
|
-
const raw =
|
|
1244
|
+
const raw = readFileSync2(path, "utf8");
|
|
1077
1245
|
const out = [];
|
|
1078
1246
|
for (const line of raw.split(/\r?\n/)) {
|
|
1079
1247
|
const trimmed = line.trim();
|
|
@@ -1101,11 +1269,11 @@ function appendSessionMessage(name, message) {
|
|
|
1101
1269
|
}
|
|
1102
1270
|
function listSessions() {
|
|
1103
1271
|
const dir = sessionsDir();
|
|
1104
|
-
if (!
|
|
1272
|
+
if (!existsSync2(dir)) return [];
|
|
1105
1273
|
try {
|
|
1106
1274
|
const files = readdirSync(dir).filter((f) => f.endsWith(".jsonl"));
|
|
1107
1275
|
return files.map((file) => {
|
|
1108
|
-
const path =
|
|
1276
|
+
const path = join2(dir, file);
|
|
1109
1277
|
const stat = statSync(path);
|
|
1110
1278
|
const name = file.replace(/\.jsonl$/, "");
|
|
1111
1279
|
const messageCount = countLines(path);
|
|
@@ -1137,7 +1305,7 @@ function rewriteSession(name, messages) {
|
|
|
1137
1305
|
}
|
|
1138
1306
|
function countLines(path) {
|
|
1139
1307
|
try {
|
|
1140
|
-
const raw =
|
|
1308
|
+
const raw = readFileSync2(path, "utf8");
|
|
1141
1309
|
return raw.split(/\r?\n/).filter((l) => l.trim()).length;
|
|
1142
1310
|
} catch {
|
|
1143
1311
|
return 0;
|
|
@@ -1251,6 +1419,14 @@ var CacheFirstLoop = class {
|
|
|
1251
1419
|
branchEnabled;
|
|
1252
1420
|
branchOptions;
|
|
1253
1421
|
sessionName;
|
|
1422
|
+
/**
|
|
1423
|
+
* Hook list, mutable so `/hooks reload` can swap it without
|
|
1424
|
+
* reconstructing the loop. Default empty — the filter cost on a
|
|
1425
|
+
* tool call is one array length check.
|
|
1426
|
+
*/
|
|
1427
|
+
hooks;
|
|
1428
|
+
/** `cwd` reported to hook stdin. Resolved once at construction. */
|
|
1429
|
+
hookCwd;
|
|
1254
1430
|
/** Number of messages that were pre-loaded from the session file. */
|
|
1255
1431
|
resumedMessageCount;
|
|
1256
1432
|
_turn = 0;
|
|
@@ -1269,6 +1445,8 @@ var CacheFirstLoop = class {
|
|
|
1269
1445
|
this.tools = opts.tools ?? new ToolRegistry();
|
|
1270
1446
|
this.model = opts.model ?? "deepseek-chat";
|
|
1271
1447
|
this.maxToolIters = opts.maxToolIters ?? 64;
|
|
1448
|
+
this.hooks = opts.hooks ?? [];
|
|
1449
|
+
this.hookCwd = opts.hookCwd ?? process.cwd();
|
|
1272
1450
|
if (typeof opts.branch === "number") {
|
|
1273
1451
|
this.branchOptions = { budget: opts.branch };
|
|
1274
1452
|
} else if (opts.branch && typeof opts.branch === "object") {
|
|
@@ -1732,7 +1910,37 @@ var CacheFirstLoop = class {
|
|
|
1732
1910
|
toolName: name,
|
|
1733
1911
|
toolArgs: args
|
|
1734
1912
|
};
|
|
1735
|
-
const
|
|
1913
|
+
const parsedArgs = safeParseToolArgs(args);
|
|
1914
|
+
const preReport = await runHooks({
|
|
1915
|
+
hooks: this.hooks,
|
|
1916
|
+
payload: {
|
|
1917
|
+
event: "PreToolUse",
|
|
1918
|
+
cwd: this.hookCwd,
|
|
1919
|
+
toolName: name,
|
|
1920
|
+
toolArgs: parsedArgs
|
|
1921
|
+
}
|
|
1922
|
+
});
|
|
1923
|
+
for (const w of hookWarnings(preReport.outcomes, this._turn)) yield w;
|
|
1924
|
+
let result;
|
|
1925
|
+
if (preReport.blocked) {
|
|
1926
|
+
const blocking = preReport.outcomes[preReport.outcomes.length - 1];
|
|
1927
|
+
const reason = (blocking?.stderr || blocking?.stdout || "blocked by PreToolUse hook").trim();
|
|
1928
|
+
result = `[hook block] ${blocking?.hook.command ?? "<unknown>"}
|
|
1929
|
+
${reason}`;
|
|
1930
|
+
} else {
|
|
1931
|
+
result = await this.tools.dispatch(name, args, { signal });
|
|
1932
|
+
const postReport = await runHooks({
|
|
1933
|
+
hooks: this.hooks,
|
|
1934
|
+
payload: {
|
|
1935
|
+
event: "PostToolUse",
|
|
1936
|
+
cwd: this.hookCwd,
|
|
1937
|
+
toolName: name,
|
|
1938
|
+
toolArgs: parsedArgs,
|
|
1939
|
+
toolResult: result
|
|
1940
|
+
}
|
|
1941
|
+
});
|
|
1942
|
+
for (const w of hookWarnings(postReport.outcomes, this._turn)) yield w;
|
|
1943
|
+
}
|
|
1736
1944
|
this.appendAndPersist({
|
|
1737
1945
|
role: "tool",
|
|
1738
1946
|
tool_call_id: call.id ?? "",
|
|
@@ -1819,6 +2027,19 @@ function stripHallucinatedToolMarkup(s) {
|
|
|
1819
2027
|
out = out.replace(/<|DSML|[\s\S]*$/g, "");
|
|
1820
2028
|
return out.trim();
|
|
1821
2029
|
}
|
|
2030
|
+
function safeParseToolArgs(raw) {
|
|
2031
|
+
try {
|
|
2032
|
+
return JSON.parse(raw);
|
|
2033
|
+
} catch {
|
|
2034
|
+
return raw;
|
|
2035
|
+
}
|
|
2036
|
+
}
|
|
2037
|
+
function* hookWarnings(outcomes, turn) {
|
|
2038
|
+
for (const o of outcomes) {
|
|
2039
|
+
if (o.decision === "pass") continue;
|
|
2040
|
+
yield { turn, role: "warning", content: formatHookOutcomeMessage(o) };
|
|
2041
|
+
}
|
|
2042
|
+
}
|
|
1822
2043
|
function reasonPrefixFor(reason, iterCap) {
|
|
1823
2044
|
if (reason === "aborted") return "[aborted by user (Esc) \u2014 summarizing what I found so far]";
|
|
1824
2045
|
if (reason === "context-guard") {
|
|
@@ -1908,16 +2129,16 @@ function formatLoopError(err) {
|
|
|
1908
2129
|
}
|
|
1909
2130
|
|
|
1910
2131
|
// src/project-memory.ts
|
|
1911
|
-
import { existsSync as
|
|
1912
|
-
import { join as
|
|
2132
|
+
import { existsSync as existsSync3, readFileSync as readFileSync3 } from "fs";
|
|
2133
|
+
import { join as join3 } from "path";
|
|
1913
2134
|
var PROJECT_MEMORY_FILE = "REASONIX.md";
|
|
1914
2135
|
var PROJECT_MEMORY_MAX_CHARS = 8e3;
|
|
1915
2136
|
function readProjectMemory(rootDir) {
|
|
1916
|
-
const path =
|
|
1917
|
-
if (!
|
|
2137
|
+
const path = join3(rootDir, PROJECT_MEMORY_FILE);
|
|
2138
|
+
if (!existsSync3(path)) return null;
|
|
1918
2139
|
let raw;
|
|
1919
2140
|
try {
|
|
1920
|
-
raw =
|
|
2141
|
+
raw = readFileSync3(path, "utf8");
|
|
1921
2142
|
} catch {
|
|
1922
2143
|
return null;
|
|
1923
2144
|
}
|
|
@@ -1953,20 +2174,20 @@ ${mem.content}
|
|
|
1953
2174
|
// src/user-memory.ts
|
|
1954
2175
|
import { createHash as createHash2 } from "crypto";
|
|
1955
2176
|
import {
|
|
1956
|
-
existsSync as
|
|
2177
|
+
existsSync as existsSync5,
|
|
1957
2178
|
mkdirSync as mkdirSync2,
|
|
1958
|
-
readFileSync as
|
|
2179
|
+
readFileSync as readFileSync5,
|
|
1959
2180
|
readdirSync as readdirSync3,
|
|
1960
2181
|
unlinkSync as unlinkSync2,
|
|
1961
2182
|
writeFileSync as writeFileSync2
|
|
1962
2183
|
} from "fs";
|
|
1963
|
-
import { homedir as
|
|
1964
|
-
import { join as
|
|
2184
|
+
import { homedir as homedir4 } from "os";
|
|
2185
|
+
import { join as join5, resolve as resolve2 } from "path";
|
|
1965
2186
|
|
|
1966
2187
|
// src/skills.ts
|
|
1967
|
-
import { existsSync as
|
|
1968
|
-
import { homedir as
|
|
1969
|
-
import { join as
|
|
2188
|
+
import { existsSync as existsSync4, readFileSync as readFileSync4, readdirSync as readdirSync2, statSync as statSync2 } from "fs";
|
|
2189
|
+
import { homedir as homedir3 } from "os";
|
|
2190
|
+
import { join as join4, resolve } from "path";
|
|
1970
2191
|
var SKILLS_DIRNAME = "skills";
|
|
1971
2192
|
var SKILL_FILE = "SKILL.md";
|
|
1972
2193
|
var SKILLS_INDEX_MAX_CHARS = 4e3;
|
|
@@ -1995,7 +2216,7 @@ var SkillStore = class {
|
|
|
1995
2216
|
homeDir;
|
|
1996
2217
|
projectRoot;
|
|
1997
2218
|
constructor(opts = {}) {
|
|
1998
|
-
this.homeDir = opts.homeDir ??
|
|
2219
|
+
this.homeDir = opts.homeDir ?? homedir3();
|
|
1999
2220
|
this.projectRoot = opts.projectRoot ? resolve(opts.projectRoot) : void 0;
|
|
2000
2221
|
}
|
|
2001
2222
|
/** True iff this store was configured with a project root. */
|
|
@@ -2011,11 +2232,11 @@ var SkillStore = class {
|
|
|
2011
2232
|
const out = [];
|
|
2012
2233
|
if (this.projectRoot) {
|
|
2013
2234
|
out.push({
|
|
2014
|
-
dir:
|
|
2235
|
+
dir: join4(this.projectRoot, ".reasonix", SKILLS_DIRNAME),
|
|
2015
2236
|
scope: "project"
|
|
2016
2237
|
});
|
|
2017
2238
|
}
|
|
2018
|
-
out.push({ dir:
|
|
2239
|
+
out.push({ dir: join4(this.homeDir, ".reasonix", SKILLS_DIRNAME), scope: "global" });
|
|
2019
2240
|
return out;
|
|
2020
2241
|
}
|
|
2021
2242
|
/**
|
|
@@ -2026,7 +2247,7 @@ var SkillStore = class {
|
|
|
2026
2247
|
list() {
|
|
2027
2248
|
const byName = /* @__PURE__ */ new Map();
|
|
2028
2249
|
for (const { dir, scope } of this.roots()) {
|
|
2029
|
-
if (!
|
|
2250
|
+
if (!existsSync4(dir)) continue;
|
|
2030
2251
|
let entries;
|
|
2031
2252
|
try {
|
|
2032
2253
|
entries = readdirSync2(dir, { withFileTypes: true });
|
|
@@ -2045,13 +2266,13 @@ var SkillStore = class {
|
|
|
2045
2266
|
read(name) {
|
|
2046
2267
|
if (!isValidSkillName(name)) return null;
|
|
2047
2268
|
for (const { dir, scope } of this.roots()) {
|
|
2048
|
-
if (!
|
|
2049
|
-
const dirCandidate =
|
|
2050
|
-
if (
|
|
2269
|
+
if (!existsSync4(dir)) continue;
|
|
2270
|
+
const dirCandidate = join4(dir, name, SKILL_FILE);
|
|
2271
|
+
if (existsSync4(dirCandidate) && statSync2(dirCandidate).isFile()) {
|
|
2051
2272
|
return this.parse(dirCandidate, name, scope);
|
|
2052
2273
|
}
|
|
2053
|
-
const flatCandidate =
|
|
2054
|
-
if (
|
|
2274
|
+
const flatCandidate = join4(dir, `${name}.md`);
|
|
2275
|
+
if (existsSync4(flatCandidate) && statSync2(flatCandidate).isFile()) {
|
|
2055
2276
|
return this.parse(flatCandidate, name, scope);
|
|
2056
2277
|
}
|
|
2057
2278
|
}
|
|
@@ -2060,21 +2281,21 @@ var SkillStore = class {
|
|
|
2060
2281
|
readEntry(dir, scope, entry) {
|
|
2061
2282
|
if (entry.isDirectory()) {
|
|
2062
2283
|
if (!isValidSkillName(entry.name)) return null;
|
|
2063
|
-
const file =
|
|
2064
|
-
if (!
|
|
2284
|
+
const file = join4(dir, entry.name, SKILL_FILE);
|
|
2285
|
+
if (!existsSync4(file)) return null;
|
|
2065
2286
|
return this.parse(file, entry.name, scope);
|
|
2066
2287
|
}
|
|
2067
2288
|
if (entry.isFile() && entry.name.endsWith(".md")) {
|
|
2068
2289
|
const stem = entry.name.slice(0, -3);
|
|
2069
2290
|
if (!isValidSkillName(stem)) return null;
|
|
2070
|
-
return this.parse(
|
|
2291
|
+
return this.parse(join4(dir, entry.name), stem, scope);
|
|
2071
2292
|
}
|
|
2072
2293
|
return null;
|
|
2073
2294
|
}
|
|
2074
2295
|
parse(path, stem, scope) {
|
|
2075
2296
|
let raw;
|
|
2076
2297
|
try {
|
|
2077
|
-
raw =
|
|
2298
|
+
raw = readFileSync4(path, "utf8");
|
|
2078
2299
|
} catch {
|
|
2079
2300
|
return null;
|
|
2080
2301
|
}
|
|
@@ -2137,15 +2358,15 @@ function projectHash(rootDir) {
|
|
|
2137
2358
|
}
|
|
2138
2359
|
function scopeDir(opts) {
|
|
2139
2360
|
if (opts.scope === "global") {
|
|
2140
|
-
return
|
|
2361
|
+
return join5(opts.homeDir, USER_MEMORY_DIR, "global");
|
|
2141
2362
|
}
|
|
2142
2363
|
if (!opts.projectRoot) {
|
|
2143
2364
|
throw new Error("scope=project requires a projectRoot on MemoryStore");
|
|
2144
2365
|
}
|
|
2145
|
-
return
|
|
2366
|
+
return join5(opts.homeDir, USER_MEMORY_DIR, projectHash(opts.projectRoot));
|
|
2146
2367
|
}
|
|
2147
2368
|
function ensureDir(p) {
|
|
2148
|
-
if (!
|
|
2369
|
+
if (!existsSync5(p)) mkdirSync2(p, { recursive: true });
|
|
2149
2370
|
}
|
|
2150
2371
|
function parseFrontmatter2(raw) {
|
|
2151
2372
|
const lines = raw.split(/\r?\n/);
|
|
@@ -2190,7 +2411,7 @@ var MemoryStore = class {
|
|
|
2190
2411
|
homeDir;
|
|
2191
2412
|
projectRoot;
|
|
2192
2413
|
constructor(opts = {}) {
|
|
2193
|
-
this.homeDir = opts.homeDir ??
|
|
2414
|
+
this.homeDir = opts.homeDir ?? join5(homedir4(), ".reasonix");
|
|
2194
2415
|
this.projectRoot = opts.projectRoot ? resolve2(opts.projectRoot) : void 0;
|
|
2195
2416
|
}
|
|
2196
2417
|
/** Directory this store writes `scope` files into, creating it if needed. */
|
|
@@ -2201,7 +2422,7 @@ var MemoryStore = class {
|
|
|
2201
2422
|
}
|
|
2202
2423
|
/** Absolute path to a memory file (no existence check). */
|
|
2203
2424
|
pathFor(scope, name) {
|
|
2204
|
-
return
|
|
2425
|
+
return join5(this.dir(scope), `${sanitizeMemoryName(name)}.md`);
|
|
2205
2426
|
}
|
|
2206
2427
|
/** True iff this store is configured with a project scope available. */
|
|
2207
2428
|
hasProjectScope() {
|
|
@@ -2213,14 +2434,14 @@ var MemoryStore = class {
|
|
|
2213
2434
|
*/
|
|
2214
2435
|
loadIndex(scope) {
|
|
2215
2436
|
if (scope === "project" && !this.projectRoot) return null;
|
|
2216
|
-
const file =
|
|
2437
|
+
const file = join5(
|
|
2217
2438
|
scopeDir({ homeDir: this.homeDir, scope, projectRoot: this.projectRoot }),
|
|
2218
2439
|
MEMORY_INDEX_FILE
|
|
2219
2440
|
);
|
|
2220
|
-
if (!
|
|
2441
|
+
if (!existsSync5(file)) return null;
|
|
2221
2442
|
let raw;
|
|
2222
2443
|
try {
|
|
2223
|
-
raw =
|
|
2444
|
+
raw = readFileSync5(file, "utf8");
|
|
2224
2445
|
} catch {
|
|
2225
2446
|
return null;
|
|
2226
2447
|
}
|
|
@@ -2235,10 +2456,10 @@ var MemoryStore = class {
|
|
|
2235
2456
|
/** Read one memory file's body (frontmatter stripped). Throws if missing. */
|
|
2236
2457
|
read(scope, name) {
|
|
2237
2458
|
const file = this.pathFor(scope, name);
|
|
2238
|
-
if (!
|
|
2459
|
+
if (!existsSync5(file)) {
|
|
2239
2460
|
throw new Error(`memory not found: scope=${scope} name=${name}`);
|
|
2240
2461
|
}
|
|
2241
|
-
const raw =
|
|
2462
|
+
const raw = readFileSync5(file, "utf8");
|
|
2242
2463
|
const { data, body } = parseFrontmatter2(raw);
|
|
2243
2464
|
return {
|
|
2244
2465
|
name: data.name ?? name,
|
|
@@ -2259,7 +2480,7 @@ var MemoryStore = class {
|
|
|
2259
2480
|
const scopes = this.projectRoot ? ["global", "project"] : ["global"];
|
|
2260
2481
|
for (const scope of scopes) {
|
|
2261
2482
|
const dir = scopeDir({ homeDir: this.homeDir, scope, projectRoot: this.projectRoot });
|
|
2262
|
-
if (!
|
|
2483
|
+
if (!existsSync5(dir)) continue;
|
|
2263
2484
|
let entries;
|
|
2264
2485
|
try {
|
|
2265
2486
|
entries = readdirSync3(dir);
|
|
@@ -2300,7 +2521,7 @@ var MemoryStore = class {
|
|
|
2300
2521
|
createdAt: todayIso()
|
|
2301
2522
|
};
|
|
2302
2523
|
const dir = this.dir(input.scope);
|
|
2303
|
-
const file =
|
|
2524
|
+
const file = join5(dir, `${name}.md`);
|
|
2304
2525
|
const content = `${formatFrontmatter(entry)}${body}
|
|
2305
2526
|
`;
|
|
2306
2527
|
writeFileSync2(file, content, "utf8");
|
|
@@ -2313,7 +2534,7 @@ var MemoryStore = class {
|
|
|
2313
2534
|
throw new Error("cannot delete project-scoped memory: no projectRoot configured");
|
|
2314
2535
|
}
|
|
2315
2536
|
const file = this.pathFor(scope, rawName);
|
|
2316
|
-
if (!
|
|
2537
|
+
if (!existsSync5(file)) return false;
|
|
2317
2538
|
unlinkSync2(file);
|
|
2318
2539
|
this.regenerateIndex(scope);
|
|
2319
2540
|
return true;
|
|
@@ -2326,7 +2547,7 @@ var MemoryStore = class {
|
|
|
2326
2547
|
*/
|
|
2327
2548
|
regenerateIndex(scope) {
|
|
2328
2549
|
const dir = scopeDir({ homeDir: this.homeDir, scope, projectRoot: this.projectRoot });
|
|
2329
|
-
if (!
|
|
2550
|
+
if (!existsSync5(dir)) return;
|
|
2330
2551
|
let files;
|
|
2331
2552
|
try {
|
|
2332
2553
|
files = readdirSync3(dir);
|
|
@@ -2334,9 +2555,9 @@ var MemoryStore = class {
|
|
|
2334
2555
|
return;
|
|
2335
2556
|
}
|
|
2336
2557
|
const mdFiles = files.filter((f) => f !== MEMORY_INDEX_FILE && f.endsWith(".md")).sort((a, b) => a.localeCompare(b));
|
|
2337
|
-
const indexPath =
|
|
2558
|
+
const indexPath = join5(dir, MEMORY_INDEX_FILE);
|
|
2338
2559
|
if (mdFiles.length === 0) {
|
|
2339
|
-
if (
|
|
2560
|
+
if (existsSync5(indexPath)) unlinkSync2(indexPath);
|
|
2340
2561
|
return;
|
|
2341
2562
|
}
|
|
2342
2563
|
const lines = [];
|
|
@@ -2899,8 +3120,8 @@ function registerPlanTool(registry, opts = {}) {
|
|
|
2899
3120
|
}
|
|
2900
3121
|
|
|
2901
3122
|
// src/tools/shell.ts
|
|
2902
|
-
import { spawn } from "child_process";
|
|
2903
|
-
import { existsSync as
|
|
3123
|
+
import { spawn as spawn2 } from "child_process";
|
|
3124
|
+
import { existsSync as existsSync6, statSync as statSync3 } from "fs";
|
|
2904
3125
|
import * as pathMod2 from "path";
|
|
2905
3126
|
var DEFAULT_TIMEOUT_SEC = 60;
|
|
2906
3127
|
var DEFAULT_MAX_OUTPUT_CHARS = 32e3;
|
|
@@ -3023,7 +3244,7 @@ async function runCommand(cmd, opts) {
|
|
|
3023
3244
|
return await new Promise((resolve7, reject) => {
|
|
3024
3245
|
let child;
|
|
3025
3246
|
try {
|
|
3026
|
-
child =
|
|
3247
|
+
child = spawn2(bin, args, effectiveSpawnOpts);
|
|
3027
3248
|
} catch (err) {
|
|
3028
3249
|
reject(err);
|
|
3029
3250
|
return;
|
|
@@ -3078,7 +3299,7 @@ function resolveExecutable(cmd, opts = {}) {
|
|
|
3078
3299
|
}
|
|
3079
3300
|
function defaultIsFile(full) {
|
|
3080
3301
|
try {
|
|
3081
|
-
return
|
|
3302
|
+
return existsSync6(full) && statSync3(full).isFile();
|
|
3082
3303
|
} catch {
|
|
3083
3304
|
return false;
|
|
3084
3305
|
}
|
|
@@ -3381,12 +3602,12 @@ ${i + 1}. ${r.title}`);
|
|
|
3381
3602
|
}
|
|
3382
3603
|
|
|
3383
3604
|
// src/env.ts
|
|
3384
|
-
import { readFileSync as
|
|
3605
|
+
import { readFileSync as readFileSync6 } from "fs";
|
|
3385
3606
|
import { resolve as resolve5 } from "path";
|
|
3386
3607
|
function loadDotenv(path = ".env") {
|
|
3387
3608
|
let raw;
|
|
3388
3609
|
try {
|
|
3389
|
-
raw =
|
|
3610
|
+
raw = readFileSync6(resolve5(process.cwd(), path), "utf8");
|
|
3390
3611
|
} catch {
|
|
3391
3612
|
return;
|
|
3392
3613
|
}
|
|
@@ -3405,7 +3626,7 @@ function loadDotenv(path = ".env") {
|
|
|
3405
3626
|
}
|
|
3406
3627
|
|
|
3407
3628
|
// src/transcript.ts
|
|
3408
|
-
import { createWriteStream, readFileSync as
|
|
3629
|
+
import { createWriteStream, readFileSync as readFileSync7 } from "fs";
|
|
3409
3630
|
function recordFromLoopEvent(ev, extra) {
|
|
3410
3631
|
const rec = {
|
|
3411
3632
|
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
@@ -3456,7 +3677,7 @@ function openTranscriptFile(path, meta) {
|
|
|
3456
3677
|
return stream;
|
|
3457
3678
|
}
|
|
3458
3679
|
function readTranscript(path) {
|
|
3459
|
-
const raw =
|
|
3680
|
+
const raw = readFileSync7(path, "utf8");
|
|
3460
3681
|
return parseTranscript(raw);
|
|
3461
3682
|
}
|
|
3462
3683
|
function isPlanStateEmptyShape(s) {
|
|
@@ -4159,7 +4380,7 @@ var McpClient = class {
|
|
|
4159
4380
|
};
|
|
4160
4381
|
|
|
4161
4382
|
// src/mcp/stdio.ts
|
|
4162
|
-
import { spawn as
|
|
4383
|
+
import { spawn as spawn3 } from "child_process";
|
|
4163
4384
|
var StdioTransport = class {
|
|
4164
4385
|
child;
|
|
4165
4386
|
queue = [];
|
|
@@ -4174,14 +4395,14 @@ var StdioTransport = class {
|
|
|
4174
4395
|
opts.command,
|
|
4175
4396
|
...(opts.args ?? []).map((a) => quoteArg(a, process.platform === "win32"))
|
|
4176
4397
|
].join(" ");
|
|
4177
|
-
this.child =
|
|
4398
|
+
this.child = spawn3(line, [], {
|
|
4178
4399
|
env,
|
|
4179
4400
|
cwd: opts.cwd,
|
|
4180
4401
|
stdio: ["pipe", "pipe", "inherit"],
|
|
4181
4402
|
shell: true
|
|
4182
4403
|
});
|
|
4183
4404
|
} else {
|
|
4184
|
-
this.child =
|
|
4405
|
+
this.child = spawn3(opts.command, opts.args ?? [], {
|
|
4185
4406
|
env,
|
|
4186
4407
|
cwd: opts.cwd,
|
|
4187
4408
|
stdio: ["pipe", "pipe", "inherit"]
|
|
@@ -4511,7 +4732,7 @@ async function trySection(load) {
|
|
|
4511
4732
|
}
|
|
4512
4733
|
|
|
4513
4734
|
// src/code/edit-blocks.ts
|
|
4514
|
-
import { existsSync as
|
|
4735
|
+
import { existsSync as existsSync7, mkdirSync as mkdirSync3, readFileSync as readFileSync8, unlinkSync as unlinkSync3, writeFileSync as writeFileSync3 } from "fs";
|
|
4515
4736
|
import { dirname as dirname3, resolve as resolve6 } from "path";
|
|
4516
4737
|
var BLOCK_RE = /^(\S[^\n]*)\n<{7} SEARCH\n([\s\S]*?)\n?={7}\n([\s\S]*?)\n?>{7} REPLACE/gm;
|
|
4517
4738
|
function parseEditBlocks(text) {
|
|
@@ -4540,7 +4761,7 @@ function applyEditBlock(block, rootDir) {
|
|
|
4540
4761
|
};
|
|
4541
4762
|
}
|
|
4542
4763
|
const searchEmpty = block.search.length === 0;
|
|
4543
|
-
const exists =
|
|
4764
|
+
const exists = existsSync7(absTarget);
|
|
4544
4765
|
try {
|
|
4545
4766
|
if (!exists) {
|
|
4546
4767
|
if (!searchEmpty) {
|
|
@@ -4554,7 +4775,7 @@ function applyEditBlock(block, rootDir) {
|
|
|
4554
4775
|
writeFileSync3(absTarget, block.replace, "utf8");
|
|
4555
4776
|
return { path: block.path, status: "created" };
|
|
4556
4777
|
}
|
|
4557
|
-
const content =
|
|
4778
|
+
const content = readFileSync8(absTarget, "utf8");
|
|
4558
4779
|
if (searchEmpty) {
|
|
4559
4780
|
return {
|
|
4560
4781
|
path: block.path,
|
|
@@ -4588,12 +4809,12 @@ function snapshotBeforeEdits(blocks, rootDir) {
|
|
|
4588
4809
|
if (seen.has(b.path)) continue;
|
|
4589
4810
|
seen.add(b.path);
|
|
4590
4811
|
const abs = resolve6(absRoot, b.path);
|
|
4591
|
-
if (!
|
|
4812
|
+
if (!existsSync7(abs)) {
|
|
4592
4813
|
snapshots.push({ path: b.path, prevContent: null });
|
|
4593
4814
|
continue;
|
|
4594
4815
|
}
|
|
4595
4816
|
try {
|
|
4596
|
-
snapshots.push({ path: b.path, prevContent:
|
|
4817
|
+
snapshots.push({ path: b.path, prevContent: readFileSync8(abs, "utf8") });
|
|
4597
4818
|
} catch {
|
|
4598
4819
|
snapshots.push({ path: b.path, prevContent: null });
|
|
4599
4820
|
}
|
|
@@ -4613,7 +4834,7 @@ function restoreSnapshots(snapshots, rootDir) {
|
|
|
4613
4834
|
}
|
|
4614
4835
|
try {
|
|
4615
4836
|
if (snap.prevContent === null) {
|
|
4616
|
-
if (
|
|
4837
|
+
if (existsSync7(abs)) unlinkSync3(abs);
|
|
4617
4838
|
return {
|
|
4618
4839
|
path: snap.path,
|
|
4619
4840
|
status: "applied",
|
|
@@ -4636,8 +4857,8 @@ function sep() {
|
|
|
4636
4857
|
}
|
|
4637
4858
|
|
|
4638
4859
|
// src/code/prompt.ts
|
|
4639
|
-
import { existsSync as
|
|
4640
|
-
import { join as
|
|
4860
|
+
import { existsSync as existsSync8, readFileSync as readFileSync9 } from "fs";
|
|
4861
|
+
import { join as join7 } from "path";
|
|
4641
4862
|
var CODE_SYSTEM_PROMPT = `You are Reasonix Code, a coding assistant. You have filesystem tools (read_file, write_file, list_directory, search_files, etc.) rooted at the user's working directory.
|
|
4642
4863
|
|
|
4643
4864
|
# When to propose a plan (submit_plan)
|
|
@@ -4712,11 +4933,11 @@ Before exploring the filesystem to answer a factual question, check whether the
|
|
|
4712
4933
|
`;
|
|
4713
4934
|
function codeSystemPrompt(rootDir) {
|
|
4714
4935
|
const withMemory = applyMemoryStack(CODE_SYSTEM_PROMPT, rootDir);
|
|
4715
|
-
const gitignorePath =
|
|
4716
|
-
if (!
|
|
4936
|
+
const gitignorePath = join7(rootDir, ".gitignore");
|
|
4937
|
+
if (!existsSync8(gitignorePath)) return withMemory;
|
|
4717
4938
|
let content;
|
|
4718
4939
|
try {
|
|
4719
|
-
content =
|
|
4940
|
+
content = readFileSync9(gitignorePath, "utf8");
|
|
4720
4941
|
} catch {
|
|
4721
4942
|
return withMemory;
|
|
4722
4943
|
}
|
|
@@ -4736,15 +4957,15 @@ ${truncated}
|
|
|
4736
4957
|
}
|
|
4737
4958
|
|
|
4738
4959
|
// src/config.ts
|
|
4739
|
-
import { chmodSync as chmodSync2, mkdirSync as mkdirSync4, readFileSync as
|
|
4740
|
-
import { homedir as
|
|
4741
|
-
import { dirname as dirname4, join as
|
|
4960
|
+
import { chmodSync as chmodSync2, mkdirSync as mkdirSync4, readFileSync as readFileSync10, writeFileSync as writeFileSync4 } from "fs";
|
|
4961
|
+
import { homedir as homedir5 } from "os";
|
|
4962
|
+
import { dirname as dirname4, join as join8 } from "path";
|
|
4742
4963
|
function defaultConfigPath() {
|
|
4743
|
-
return
|
|
4964
|
+
return join8(homedir5(), ".reasonix", "config.json");
|
|
4744
4965
|
}
|
|
4745
4966
|
function readConfig(path = defaultConfigPath()) {
|
|
4746
4967
|
try {
|
|
4747
|
-
const raw =
|
|
4968
|
+
const raw = readFileSync10(path, "utf8");
|
|
4748
4969
|
const parsed = JSON.parse(raw);
|
|
4749
4970
|
if (parsed && typeof parsed === "object") return parsed;
|
|
4750
4971
|
} catch {
|
|
@@ -4779,9 +5000,9 @@ function redactKey(key) {
|
|
|
4779
5000
|
}
|
|
4780
5001
|
|
|
4781
5002
|
// src/version.ts
|
|
4782
|
-
import { existsSync as
|
|
4783
|
-
import { homedir as
|
|
4784
|
-
import { dirname as dirname5, join as
|
|
5003
|
+
import { existsSync as existsSync9, mkdirSync as mkdirSync5, readFileSync as readFileSync11, writeFileSync as writeFileSync5 } from "fs";
|
|
5004
|
+
import { homedir as homedir6 } from "os";
|
|
5005
|
+
import { dirname as dirname5, join as join9 } from "path";
|
|
4785
5006
|
import { fileURLToPath } from "url";
|
|
4786
5007
|
var REGISTRY_URL = "https://registry.npmjs.org/reasonix/latest";
|
|
4787
5008
|
var LATEST_CACHE_TTL_MS = 24 * 60 * 60 * 1e3;
|
|
@@ -4790,9 +5011,9 @@ function readPackageVersion() {
|
|
|
4790
5011
|
try {
|
|
4791
5012
|
let dir = dirname5(fileURLToPath(import.meta.url));
|
|
4792
5013
|
for (let i = 0; i < 6; i++) {
|
|
4793
|
-
const p =
|
|
4794
|
-
if (
|
|
4795
|
-
const pkg = JSON.parse(
|
|
5014
|
+
const p = join9(dir, "package.json");
|
|
5015
|
+
if (existsSync9(p)) {
|
|
5016
|
+
const pkg = JSON.parse(readFileSync11(p, "utf8"));
|
|
4796
5017
|
if (pkg?.name === "reasonix" && typeof pkg.version === "string") {
|
|
4797
5018
|
return pkg.version;
|
|
4798
5019
|
}
|
|
@@ -4807,11 +5028,11 @@ function readPackageVersion() {
|
|
|
4807
5028
|
}
|
|
4808
5029
|
var VERSION = readPackageVersion();
|
|
4809
5030
|
function cachePath(homeDirOverride) {
|
|
4810
|
-
return
|
|
5031
|
+
return join9(homeDirOverride ?? homedir6(), ".reasonix", "version-cache.json");
|
|
4811
5032
|
}
|
|
4812
5033
|
function readCache(homeDirOverride) {
|
|
4813
5034
|
try {
|
|
4814
|
-
const raw =
|
|
5035
|
+
const raw = readFileSync11(cachePath(homeDirOverride), "utf8");
|
|
4815
5036
|
const parsed = JSON.parse(raw);
|
|
4816
5037
|
if (parsed && typeof parsed.version === "string" && typeof parsed.checkedAt === "number") {
|
|
4817
5038
|
return parsed;
|
|
@@ -4878,12 +5099,141 @@ function isNpxInstall() {
|
|
|
4878
5099
|
if (ua.includes("npx/")) return true;
|
|
4879
5100
|
return false;
|
|
4880
5101
|
}
|
|
5102
|
+
|
|
5103
|
+
// src/usage.ts
|
|
5104
|
+
import { appendFileSync as appendFileSync2, existsSync as existsSync10, mkdirSync as mkdirSync6, readFileSync as readFileSync12, statSync as statSync4 } from "fs";
|
|
5105
|
+
import { homedir as homedir7 } from "os";
|
|
5106
|
+
import { dirname as dirname6, join as join10 } from "path";
|
|
5107
|
+
function defaultUsageLogPath(homeDirOverride) {
|
|
5108
|
+
return join10(homeDirOverride ?? homedir7(), ".reasonix", "usage.jsonl");
|
|
5109
|
+
}
|
|
5110
|
+
function appendUsage(input) {
|
|
5111
|
+
const record = {
|
|
5112
|
+
ts: input.now ?? Date.now(),
|
|
5113
|
+
session: input.session,
|
|
5114
|
+
model: input.model,
|
|
5115
|
+
promptTokens: input.usage.promptTokens,
|
|
5116
|
+
completionTokens: input.usage.completionTokens,
|
|
5117
|
+
cacheHitTokens: input.usage.promptCacheHitTokens,
|
|
5118
|
+
cacheMissTokens: input.usage.promptCacheMissTokens,
|
|
5119
|
+
costUsd: costUsd(input.model, input.usage),
|
|
5120
|
+
claudeEquivUsd: claudeEquivalentCost(input.usage)
|
|
5121
|
+
};
|
|
5122
|
+
const path = input.path ?? defaultUsageLogPath();
|
|
5123
|
+
try {
|
|
5124
|
+
mkdirSync6(dirname6(path), { recursive: true });
|
|
5125
|
+
appendFileSync2(path, `${JSON.stringify(record)}
|
|
5126
|
+
`, "utf8");
|
|
5127
|
+
} catch {
|
|
5128
|
+
}
|
|
5129
|
+
return record;
|
|
5130
|
+
}
|
|
5131
|
+
function readUsageLog(path = defaultUsageLogPath()) {
|
|
5132
|
+
if (!existsSync10(path)) return [];
|
|
5133
|
+
let raw;
|
|
5134
|
+
try {
|
|
5135
|
+
raw = readFileSync12(path, "utf8");
|
|
5136
|
+
} catch {
|
|
5137
|
+
return [];
|
|
5138
|
+
}
|
|
5139
|
+
const out = [];
|
|
5140
|
+
for (const line of raw.split(/\r?\n/)) {
|
|
5141
|
+
if (!line.trim()) continue;
|
|
5142
|
+
try {
|
|
5143
|
+
const rec = JSON.parse(line);
|
|
5144
|
+
if (isValidRecord(rec)) out.push(rec);
|
|
5145
|
+
} catch {
|
|
5146
|
+
}
|
|
5147
|
+
}
|
|
5148
|
+
return out;
|
|
5149
|
+
}
|
|
5150
|
+
function isValidRecord(rec) {
|
|
5151
|
+
if (!rec || typeof rec !== "object") return false;
|
|
5152
|
+
const r = rec;
|
|
5153
|
+
return typeof r.ts === "number" && typeof r.model === "string" && typeof r.promptTokens === "number" && typeof r.completionTokens === "number" && typeof r.cacheHitTokens === "number" && typeof r.cacheMissTokens === "number" && typeof r.costUsd === "number" && typeof r.claudeEquivUsd === "number";
|
|
5154
|
+
}
|
|
5155
|
+
function bucketCacheHitRatio(b) {
|
|
5156
|
+
const denom = b.cacheHitTokens + b.cacheMissTokens;
|
|
5157
|
+
return denom > 0 ? b.cacheHitTokens / denom : 0;
|
|
5158
|
+
}
|
|
5159
|
+
function bucketSavingsFraction(b) {
|
|
5160
|
+
return b.claudeEquivUsd > 0 ? 1 - b.costUsd / b.claudeEquivUsd : 0;
|
|
5161
|
+
}
|
|
5162
|
+
function emptyBucket(label, since) {
|
|
5163
|
+
return {
|
|
5164
|
+
label,
|
|
5165
|
+
since,
|
|
5166
|
+
turns: 0,
|
|
5167
|
+
promptTokens: 0,
|
|
5168
|
+
completionTokens: 0,
|
|
5169
|
+
cacheHitTokens: 0,
|
|
5170
|
+
cacheMissTokens: 0,
|
|
5171
|
+
costUsd: 0,
|
|
5172
|
+
claudeEquivUsd: 0
|
|
5173
|
+
};
|
|
5174
|
+
}
|
|
5175
|
+
function addToBucket(b, r) {
|
|
5176
|
+
b.turns += 1;
|
|
5177
|
+
b.promptTokens += r.promptTokens;
|
|
5178
|
+
b.completionTokens += r.completionTokens;
|
|
5179
|
+
b.cacheHitTokens += r.cacheHitTokens;
|
|
5180
|
+
b.cacheMissTokens += r.cacheMissTokens;
|
|
5181
|
+
b.costUsd += r.costUsd;
|
|
5182
|
+
b.claudeEquivUsd += r.claudeEquivUsd;
|
|
5183
|
+
}
|
|
5184
|
+
function aggregateUsage(records, opts = {}) {
|
|
5185
|
+
const now = opts.now ?? Date.now();
|
|
5186
|
+
const day = 24 * 60 * 60 * 1e3;
|
|
5187
|
+
const today = emptyBucket("today", now - day);
|
|
5188
|
+
const week = emptyBucket("week", now - 7 * day);
|
|
5189
|
+
const month = emptyBucket("month", now - 30 * day);
|
|
5190
|
+
const all = emptyBucket("all-time", 0);
|
|
5191
|
+
const modelCounts = /* @__PURE__ */ new Map();
|
|
5192
|
+
const sessionCounts = /* @__PURE__ */ new Map();
|
|
5193
|
+
let firstSeen = null;
|
|
5194
|
+
let lastSeen = null;
|
|
5195
|
+
for (const r of records) {
|
|
5196
|
+
addToBucket(all, r);
|
|
5197
|
+
if (r.ts >= today.since) addToBucket(today, r);
|
|
5198
|
+
if (r.ts >= week.since) addToBucket(week, r);
|
|
5199
|
+
if (r.ts >= month.since) addToBucket(month, r);
|
|
5200
|
+
modelCounts.set(r.model, (modelCounts.get(r.model) ?? 0) + 1);
|
|
5201
|
+
const sessKey = r.session ?? "(ephemeral)";
|
|
5202
|
+
sessionCounts.set(sessKey, (sessionCounts.get(sessKey) ?? 0) + 1);
|
|
5203
|
+
if (firstSeen === null || r.ts < firstSeen) firstSeen = r.ts;
|
|
5204
|
+
if (lastSeen === null || r.ts > lastSeen) lastSeen = r.ts;
|
|
5205
|
+
}
|
|
5206
|
+
const byModel = Array.from(modelCounts.entries()).map(([model, turns]) => ({ model, turns })).sort((a, b) => b.turns - a.turns);
|
|
5207
|
+
const bySession = Array.from(sessionCounts.entries()).map(([session, turns]) => ({ session, turns })).sort((a, b) => b.turns - a.turns);
|
|
5208
|
+
return {
|
|
5209
|
+
buckets: [today, week, month, all],
|
|
5210
|
+
byModel,
|
|
5211
|
+
bySession,
|
|
5212
|
+
firstSeen,
|
|
5213
|
+
lastSeen
|
|
5214
|
+
};
|
|
5215
|
+
}
|
|
5216
|
+
function formatLogSize(path = defaultUsageLogPath()) {
|
|
5217
|
+
if (!existsSync10(path)) return "";
|
|
5218
|
+
try {
|
|
5219
|
+
const s = statSync4(path);
|
|
5220
|
+
const bytes = s.size;
|
|
5221
|
+
if (bytes < 1024) return `${bytes} B`;
|
|
5222
|
+
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
|
|
5223
|
+
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
5224
|
+
} catch {
|
|
5225
|
+
return "";
|
|
5226
|
+
}
|
|
5227
|
+
}
|
|
4881
5228
|
export {
|
|
4882
5229
|
AppendOnlyLog,
|
|
4883
5230
|
CODE_SYSTEM_PROMPT,
|
|
4884
5231
|
CacheFirstLoop,
|
|
4885
5232
|
DEFAULT_MAX_RESULT_CHARS,
|
|
4886
5233
|
DeepSeekClient,
|
|
5234
|
+
HOOK_EVENTS,
|
|
5235
|
+
HOOK_SETTINGS_DIRNAME,
|
|
5236
|
+
HOOK_SETTINGS_FILENAME,
|
|
4887
5237
|
ImmutablePrefix,
|
|
4888
5238
|
LATEST_CACHE_TTL_MS,
|
|
4889
5239
|
LATEST_FETCH_TIMEOUT_MS,
|
|
@@ -4907,21 +5257,27 @@ export {
|
|
|
4907
5257
|
VERSION,
|
|
4908
5258
|
VolatileScratch,
|
|
4909
5259
|
aggregateBranchUsage,
|
|
5260
|
+
aggregateUsage,
|
|
4910
5261
|
analyzeSchema,
|
|
4911
5262
|
appendSessionMessage,
|
|
5263
|
+
appendUsage,
|
|
4912
5264
|
applyEditBlock,
|
|
4913
5265
|
applyEditBlocks,
|
|
4914
5266
|
applyMemoryStack,
|
|
4915
5267
|
applyProjectMemory,
|
|
4916
5268
|
applyUserMemory,
|
|
4917
5269
|
bridgeMcpTools,
|
|
5270
|
+
bucketCacheHitRatio,
|
|
5271
|
+
bucketSavingsFraction,
|
|
4918
5272
|
claudeEquivalentCost,
|
|
4919
5273
|
codeSystemPrompt,
|
|
4920
5274
|
compareVersions,
|
|
4921
5275
|
computeReplayStats,
|
|
4922
5276
|
costUsd,
|
|
5277
|
+
decideOutcome,
|
|
4923
5278
|
defaultConfigPath,
|
|
4924
5279
|
defaultSelector,
|
|
5280
|
+
defaultUsageLogPath,
|
|
4925
5281
|
deleteSession,
|
|
4926
5282
|
diffTranscripts,
|
|
4927
5283
|
emptyPlanState,
|
|
@@ -4929,9 +5285,12 @@ export {
|
|
|
4929
5285
|
flattenMcpResult,
|
|
4930
5286
|
flattenSchema,
|
|
4931
5287
|
formatCommandResult,
|
|
5288
|
+
formatHookOutcomeMessage,
|
|
5289
|
+
formatLogSize,
|
|
4932
5290
|
formatLoopError,
|
|
4933
5291
|
formatSearchResults,
|
|
4934
5292
|
getLatestVersion,
|
|
5293
|
+
globalSettingsPath,
|
|
4935
5294
|
harvest,
|
|
4936
5295
|
healLoadedMessages,
|
|
4937
5296
|
htmlToText,
|
|
@@ -4945,7 +5304,9 @@ export {
|
|
|
4945
5304
|
listSessions,
|
|
4946
5305
|
loadApiKey,
|
|
4947
5306
|
loadDotenv,
|
|
5307
|
+
loadHooks,
|
|
4948
5308
|
loadSessionMessages,
|
|
5309
|
+
matchesTool,
|
|
4949
5310
|
memoryEnabled,
|
|
4950
5311
|
nestArguments,
|
|
4951
5312
|
openTranscriptFile,
|
|
@@ -4956,10 +5317,12 @@ export {
|
|
|
4956
5317
|
parseTranscript,
|
|
4957
5318
|
prepareSpawn,
|
|
4958
5319
|
projectHash,
|
|
5320
|
+
projectSettingsPath,
|
|
4959
5321
|
quoteForCmdExe,
|
|
4960
5322
|
readConfig,
|
|
4961
5323
|
readProjectMemory,
|
|
4962
5324
|
readTranscript,
|
|
5325
|
+
readUsageLog,
|
|
4963
5326
|
recordFromLoopEvent,
|
|
4964
5327
|
redactKey,
|
|
4965
5328
|
registerFilesystemTools,
|
|
@@ -4975,6 +5338,7 @@ export {
|
|
|
4975
5338
|
restoreSnapshots,
|
|
4976
5339
|
runBranches,
|
|
4977
5340
|
runCommand,
|
|
5341
|
+
runHooks,
|
|
4978
5342
|
sanitizeMemoryName,
|
|
4979
5343
|
sanitizeName as sanitizeSessionName,
|
|
4980
5344
|
saveApiKey,
|