reasonix 0.8.0 → 0.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -0
- package/README.zh-CN.md +721 -0
- package/dist/cli/{chunk-DVBNMXA6.js → chunk-GXABXQMU.js} +143 -4
- package/dist/cli/chunk-GXABXQMU.js.map +1 -0
- package/dist/cli/index.js +2223 -503
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/{prompt-POARCKKR.js → prompt-FMYQ7IDW.js} +2 -2
- package/dist/index.d.ts +40 -23
- package/dist/index.js +158 -9
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/dist/cli/chunk-DVBNMXA6.js.map +0 -1
- /package/dist/cli/{prompt-POARCKKR.js.map → prompt-FMYQ7IDW.js.map} +0 -0
package/dist/cli/index.js
CHANGED
|
@@ -10,7 +10,7 @@ import {
|
|
|
10
10
|
memoryEnabled,
|
|
11
11
|
readProjectMemory,
|
|
12
12
|
sanitizeMemoryName
|
|
13
|
-
} from "./chunk-
|
|
13
|
+
} from "./chunk-GXABXQMU.js";
|
|
14
14
|
|
|
15
15
|
// src/cli/index.ts
|
|
16
16
|
import { Command } from "commander";
|
|
@@ -22,80 +22,80 @@ import { dirname, join } from "path";
|
|
|
22
22
|
function defaultConfigPath() {
|
|
23
23
|
return join(homedir(), ".reasonix", "config.json");
|
|
24
24
|
}
|
|
25
|
-
function readConfig(
|
|
25
|
+
function readConfig(path5 = defaultConfigPath()) {
|
|
26
26
|
try {
|
|
27
|
-
const raw = readFileSync(
|
|
27
|
+
const raw = readFileSync(path5, "utf8");
|
|
28
28
|
const parsed = JSON.parse(raw);
|
|
29
29
|
if (parsed && typeof parsed === "object") return parsed;
|
|
30
30
|
} catch {
|
|
31
31
|
}
|
|
32
32
|
return {};
|
|
33
33
|
}
|
|
34
|
-
function writeConfig(cfg,
|
|
35
|
-
mkdirSync(dirname(
|
|
36
|
-
writeFileSync(
|
|
34
|
+
function writeConfig(cfg, path5 = defaultConfigPath()) {
|
|
35
|
+
mkdirSync(dirname(path5), { recursive: true });
|
|
36
|
+
writeFileSync(path5, JSON.stringify(cfg, null, 2), "utf8");
|
|
37
37
|
try {
|
|
38
|
-
chmodSync(
|
|
38
|
+
chmodSync(path5, 384);
|
|
39
39
|
} catch {
|
|
40
40
|
}
|
|
41
41
|
}
|
|
42
|
-
function loadApiKey(
|
|
42
|
+
function loadApiKey(path5 = defaultConfigPath()) {
|
|
43
43
|
if (process.env.DEEPSEEK_API_KEY) return process.env.DEEPSEEK_API_KEY;
|
|
44
|
-
return readConfig(
|
|
44
|
+
return readConfig(path5).apiKey;
|
|
45
45
|
}
|
|
46
|
-
function searchEnabled(
|
|
46
|
+
function searchEnabled(path5 = defaultConfigPath()) {
|
|
47
47
|
const env = process.env.REASONIX_SEARCH;
|
|
48
48
|
if (env === "off" || env === "false" || env === "0") return false;
|
|
49
|
-
const cfg = readConfig(
|
|
49
|
+
const cfg = readConfig(path5).search;
|
|
50
50
|
if (cfg === false) return false;
|
|
51
51
|
return true;
|
|
52
52
|
}
|
|
53
|
-
function saveApiKey(key,
|
|
54
|
-
const cfg = readConfig(
|
|
53
|
+
function saveApiKey(key, path5 = defaultConfigPath()) {
|
|
54
|
+
const cfg = readConfig(path5);
|
|
55
55
|
cfg.apiKey = key.trim();
|
|
56
|
-
writeConfig(cfg,
|
|
56
|
+
writeConfig(cfg, path5);
|
|
57
57
|
}
|
|
58
|
-
function loadProjectShellAllowed(rootDir,
|
|
59
|
-
const cfg = readConfig(
|
|
58
|
+
function loadProjectShellAllowed(rootDir, path5 = defaultConfigPath()) {
|
|
59
|
+
const cfg = readConfig(path5);
|
|
60
60
|
return cfg.projects?.[rootDir]?.shellAllowed ?? [];
|
|
61
61
|
}
|
|
62
|
-
function addProjectShellAllowed(rootDir, prefix,
|
|
62
|
+
function addProjectShellAllowed(rootDir, prefix, path5 = defaultConfigPath()) {
|
|
63
63
|
const trimmed = prefix.trim();
|
|
64
64
|
if (!trimmed) return;
|
|
65
|
-
const cfg = readConfig(
|
|
65
|
+
const cfg = readConfig(path5);
|
|
66
66
|
if (!cfg.projects) cfg.projects = {};
|
|
67
67
|
if (!cfg.projects[rootDir]) cfg.projects[rootDir] = {};
|
|
68
68
|
const existing = cfg.projects[rootDir].shellAllowed ?? [];
|
|
69
69
|
if (existing.includes(trimmed)) return;
|
|
70
70
|
cfg.projects[rootDir].shellAllowed = [...existing, trimmed];
|
|
71
|
-
writeConfig(cfg,
|
|
71
|
+
writeConfig(cfg, path5);
|
|
72
72
|
}
|
|
73
|
-
function loadEditMode(
|
|
74
|
-
const v = readConfig(
|
|
73
|
+
function loadEditMode(path5 = defaultConfigPath()) {
|
|
74
|
+
const v = readConfig(path5).editMode;
|
|
75
75
|
return v === "auto" ? "auto" : "review";
|
|
76
76
|
}
|
|
77
|
-
function saveEditMode(mode2,
|
|
78
|
-
const cfg = readConfig(
|
|
77
|
+
function saveEditMode(mode2, path5 = defaultConfigPath()) {
|
|
78
|
+
const cfg = readConfig(path5);
|
|
79
79
|
cfg.editMode = mode2;
|
|
80
|
-
writeConfig(cfg,
|
|
80
|
+
writeConfig(cfg, path5);
|
|
81
81
|
}
|
|
82
|
-
function editModeHintShown(
|
|
83
|
-
return readConfig(
|
|
82
|
+
function editModeHintShown(path5 = defaultConfigPath()) {
|
|
83
|
+
return readConfig(path5).editModeHintShown === true;
|
|
84
84
|
}
|
|
85
|
-
function loadReasoningEffort(
|
|
86
|
-
const v = readConfig(
|
|
85
|
+
function loadReasoningEffort(path5 = defaultConfigPath()) {
|
|
86
|
+
const v = readConfig(path5).reasoningEffort;
|
|
87
87
|
return v === "high" ? "high" : "max";
|
|
88
88
|
}
|
|
89
|
-
function saveReasoningEffort(effort2,
|
|
90
|
-
const cfg = readConfig(
|
|
89
|
+
function saveReasoningEffort(effort2, path5 = defaultConfigPath()) {
|
|
90
|
+
const cfg = readConfig(path5);
|
|
91
91
|
cfg.reasoningEffort = effort2;
|
|
92
|
-
writeConfig(cfg,
|
|
92
|
+
writeConfig(cfg, path5);
|
|
93
93
|
}
|
|
94
|
-
function markEditModeHintShown(
|
|
95
|
-
const cfg = readConfig(
|
|
94
|
+
function markEditModeHintShown(path5 = defaultConfigPath()) {
|
|
95
|
+
const cfg = readConfig(path5);
|
|
96
96
|
if (cfg.editModeHintShown === true) return;
|
|
97
97
|
cfg.editModeHintShown = true;
|
|
98
|
-
writeConfig(cfg,
|
|
98
|
+
writeConfig(cfg, path5);
|
|
99
99
|
}
|
|
100
100
|
function isPlausibleKey(key) {
|
|
101
101
|
const trimmed = key.trim();
|
|
@@ -156,8 +156,8 @@ function computeWait(attempt, initial, cap, retryAfter) {
|
|
|
156
156
|
}
|
|
157
157
|
function sleep(ms, signal) {
|
|
158
158
|
if (ms <= 0) return Promise.resolve();
|
|
159
|
-
return new Promise((
|
|
160
|
-
const timer = setTimeout(
|
|
159
|
+
return new Promise((resolve9, reject) => {
|
|
160
|
+
const timer = setTimeout(resolve9, ms);
|
|
161
161
|
if (signal) {
|
|
162
162
|
const onAbort = () => {
|
|
163
163
|
clearTimeout(timer);
|
|
@@ -597,10 +597,10 @@ function globalSettingsPath(homeDirOverride) {
|
|
|
597
597
|
function projectSettingsPath(projectRoot) {
|
|
598
598
|
return join2(projectRoot, HOOK_SETTINGS_DIRNAME, HOOK_SETTINGS_FILENAME);
|
|
599
599
|
}
|
|
600
|
-
function readSettingsFile(
|
|
601
|
-
if (!existsSync(
|
|
600
|
+
function readSettingsFile(path5) {
|
|
601
|
+
if (!existsSync(path5)) return null;
|
|
602
602
|
try {
|
|
603
|
-
const raw = readFileSync2(
|
|
603
|
+
const raw = readFileSync2(path5, "utf8");
|
|
604
604
|
const parsed = JSON.parse(raw);
|
|
605
605
|
if (parsed && typeof parsed === "object") return parsed;
|
|
606
606
|
} catch {
|
|
@@ -642,13 +642,13 @@ function matchesTool(hook, toolName) {
|
|
|
642
642
|
}
|
|
643
643
|
}
|
|
644
644
|
function defaultSpawner(input) {
|
|
645
|
-
return new Promise((
|
|
645
|
+
return new Promise((resolve9) => {
|
|
646
646
|
const child = spawn(input.command, {
|
|
647
647
|
cwd: input.cwd,
|
|
648
648
|
shell: true,
|
|
649
649
|
stdio: ["pipe", "pipe", "pipe"]
|
|
650
650
|
});
|
|
651
|
-
let
|
|
651
|
+
let stdout3 = "";
|
|
652
652
|
let stderr = "";
|
|
653
653
|
let timedOut = false;
|
|
654
654
|
const timer = setTimeout(() => {
|
|
@@ -662,16 +662,16 @@ function defaultSpawner(input) {
|
|
|
662
662
|
}, 500);
|
|
663
663
|
}, input.timeoutMs);
|
|
664
664
|
child.stdout.on("data", (chunk) => {
|
|
665
|
-
|
|
665
|
+
stdout3 += chunk.toString("utf8");
|
|
666
666
|
});
|
|
667
667
|
child.stderr.on("data", (chunk) => {
|
|
668
668
|
stderr += chunk.toString("utf8");
|
|
669
669
|
});
|
|
670
670
|
child.once("error", (err) => {
|
|
671
671
|
clearTimeout(timer);
|
|
672
|
-
|
|
672
|
+
resolve9({
|
|
673
673
|
exitCode: null,
|
|
674
|
-
stdout:
|
|
674
|
+
stdout: stdout3,
|
|
675
675
|
stderr,
|
|
676
676
|
timedOut: false,
|
|
677
677
|
spawnError: err
|
|
@@ -679,9 +679,9 @@ function defaultSpawner(input) {
|
|
|
679
679
|
});
|
|
680
680
|
child.once("close", (code) => {
|
|
681
681
|
clearTimeout(timer);
|
|
682
|
-
|
|
682
|
+
resolve9({
|
|
683
683
|
exitCode: code,
|
|
684
|
-
stdout:
|
|
684
|
+
stdout: stdout3.trim(),
|
|
685
685
|
stderr: stderr.trim(),
|
|
686
686
|
timedOut
|
|
687
687
|
});
|
|
@@ -715,13 +715,13 @@ async function runHooks(opts) {
|
|
|
715
715
|
const matching = opts.hooks.filter((h) => h.event === event && matchesTool(h, toolName));
|
|
716
716
|
const outcomes = [];
|
|
717
717
|
let blocked = false;
|
|
718
|
-
const
|
|
718
|
+
const stdin4 = `${JSON.stringify(opts.payload)}
|
|
719
719
|
`;
|
|
720
720
|
for (const hook of matching) {
|
|
721
721
|
const start = Date.now();
|
|
722
722
|
const timeoutMs = hook.timeout ?? DEFAULT_TIMEOUTS_MS[event];
|
|
723
723
|
const cwd = hook.cwd ?? opts.payload.cwd;
|
|
724
|
-
const raw = await spawner({ command: hook.command, cwd, stdin:
|
|
724
|
+
const raw = await spawner({ command: hook.command, cwd, stdin: stdin4, timeoutMs });
|
|
725
725
|
const decision = decideOutcome(event, raw);
|
|
726
726
|
outcomes.push({
|
|
727
727
|
hook,
|
|
@@ -804,10 +804,10 @@ function loadTokenizer() {
|
|
|
804
804
|
}
|
|
805
805
|
const addedMap = /* @__PURE__ */ new Map();
|
|
806
806
|
const addedContents = [];
|
|
807
|
-
for (const
|
|
808
|
-
if (!
|
|
809
|
-
addedMap.set(
|
|
810
|
-
addedContents.push(
|
|
807
|
+
for (const t2 of data.added_tokens) {
|
|
808
|
+
if (!t2.special) {
|
|
809
|
+
addedMap.set(t2.content, t2.id);
|
|
810
|
+
addedContents.push(t2.content);
|
|
811
811
|
}
|
|
812
812
|
}
|
|
813
813
|
addedContents.sort((a, b) => b.length - a.length);
|
|
@@ -874,29 +874,29 @@ function bpeEncode(piece, mergeRank) {
|
|
|
874
874
|
}
|
|
875
875
|
function encode(text) {
|
|
876
876
|
if (!text) return [];
|
|
877
|
-
const
|
|
877
|
+
const t2 = loadTokenizer();
|
|
878
878
|
const ids = [];
|
|
879
879
|
const process2 = (segment) => {
|
|
880
880
|
if (!segment) return;
|
|
881
881
|
let chunks = [segment];
|
|
882
|
-
for (const re of
|
|
882
|
+
for (const re of t2.splitRegexes) chunks = applySplit(chunks, re);
|
|
883
883
|
for (const chunk of chunks) {
|
|
884
884
|
if (!chunk) continue;
|
|
885
|
-
const byteLevel = byteLevelEncode(chunk,
|
|
886
|
-
const pieces = bpeEncode(byteLevel,
|
|
885
|
+
const byteLevel = byteLevelEncode(chunk, t2.byteToChar);
|
|
886
|
+
const pieces = bpeEncode(byteLevel, t2.mergeRank);
|
|
887
887
|
for (const p of pieces) {
|
|
888
|
-
const id =
|
|
888
|
+
const id = t2.vocab[p];
|
|
889
889
|
if (id !== void 0) ids.push(id);
|
|
890
890
|
}
|
|
891
891
|
}
|
|
892
892
|
};
|
|
893
|
-
if (
|
|
894
|
-
|
|
893
|
+
if (t2.addedPattern) {
|
|
894
|
+
t2.addedPattern.lastIndex = 0;
|
|
895
895
|
let last = 0;
|
|
896
|
-
for (const m of text.matchAll(
|
|
896
|
+
for (const m of text.matchAll(t2.addedPattern)) {
|
|
897
897
|
const idx = m.index ?? 0;
|
|
898
898
|
if (idx > last) process2(text.slice(last, idx));
|
|
899
|
-
const id =
|
|
899
|
+
const id = t2.addedMap.get(m[0]);
|
|
900
900
|
if (id !== void 0) ids.push(id);
|
|
901
901
|
last = idx + m[0].length;
|
|
902
902
|
}
|
|
@@ -987,14 +987,14 @@ function collect(prefix, schema, out, required, isRootRequired) {
|
|
|
987
987
|
out[prefix] = schema;
|
|
988
988
|
if (isRootRequired) required.push(prefix);
|
|
989
989
|
}
|
|
990
|
-
function setByPath(target,
|
|
990
|
+
function setByPath(target, path5, value) {
|
|
991
991
|
let cur = target;
|
|
992
|
-
for (let i = 0; i <
|
|
993
|
-
const key =
|
|
992
|
+
for (let i = 0; i < path5.length - 1; i++) {
|
|
993
|
+
const key = path5[i];
|
|
994
994
|
if (typeof cur[key] !== "object" || cur[key] === null) cur[key] = {};
|
|
995
995
|
cur = cur[key];
|
|
996
996
|
}
|
|
997
|
-
cur[
|
|
997
|
+
cur[path5[path5.length - 1]] = value;
|
|
998
998
|
}
|
|
999
999
|
|
|
1000
1000
|
// src/tools.ts
|
|
@@ -1060,12 +1060,12 @@ var ToolRegistry = class {
|
|
|
1060
1060
|
return Boolean(this._tools.get(name)?.flatSchema);
|
|
1061
1061
|
}
|
|
1062
1062
|
specs() {
|
|
1063
|
-
return [...this._tools.values()].map((
|
|
1063
|
+
return [...this._tools.values()].map((t2) => ({
|
|
1064
1064
|
type: "function",
|
|
1065
1065
|
function: {
|
|
1066
|
-
name:
|
|
1067
|
-
description:
|
|
1068
|
-
parameters:
|
|
1066
|
+
name: t2.name,
|
|
1067
|
+
description: t2.description ?? "",
|
|
1068
|
+
parameters: t2.flatSchema ?? t2.parameters ?? { type: "object", properties: {} }
|
|
1069
1069
|
}
|
|
1070
1070
|
}));
|
|
1071
1071
|
}
|
|
@@ -1277,7 +1277,7 @@ var ImmutablePrefix = class {
|
|
|
1277
1277
|
return [{ role: "system", content: this.system }, ...this.fewShots.map((m) => ({ ...m }))];
|
|
1278
1278
|
}
|
|
1279
1279
|
tools() {
|
|
1280
|
-
return this.toolSpecs.map((
|
|
1280
|
+
return this.toolSpecs.map((t2) => structuredClone(t2));
|
|
1281
1281
|
}
|
|
1282
1282
|
get fingerprint() {
|
|
1283
1283
|
const blob = JSON.stringify({
|
|
@@ -1660,10 +1660,10 @@ function sanitizeName(name) {
|
|
|
1660
1660
|
return cleaned || "default";
|
|
1661
1661
|
}
|
|
1662
1662
|
function loadSessionMessages(name) {
|
|
1663
|
-
const
|
|
1664
|
-
if (!existsSync3(
|
|
1663
|
+
const path5 = sessionPath(name);
|
|
1664
|
+
if (!existsSync3(path5)) return [];
|
|
1665
1665
|
try {
|
|
1666
|
-
const raw = readFileSync4(
|
|
1666
|
+
const raw = readFileSync4(path5, "utf8");
|
|
1667
1667
|
const out = [];
|
|
1668
1668
|
for (const line of raw.split(/\r?\n/)) {
|
|
1669
1669
|
const trimmed = line.trim();
|
|
@@ -1680,12 +1680,12 @@ function loadSessionMessages(name) {
|
|
|
1680
1680
|
}
|
|
1681
1681
|
}
|
|
1682
1682
|
function appendSessionMessage(name, message) {
|
|
1683
|
-
const
|
|
1684
|
-
mkdirSync2(dirname3(
|
|
1685
|
-
appendFileSync(
|
|
1683
|
+
const path5 = sessionPath(name);
|
|
1684
|
+
mkdirSync2(dirname3(path5), { recursive: true });
|
|
1685
|
+
appendFileSync(path5, `${JSON.stringify(message)}
|
|
1686
1686
|
`, "utf8");
|
|
1687
1687
|
try {
|
|
1688
|
-
chmodSync2(
|
|
1688
|
+
chmodSync2(path5, 384);
|
|
1689
1689
|
} catch {
|
|
1690
1690
|
}
|
|
1691
1691
|
}
|
|
@@ -1695,21 +1695,21 @@ function listSessions() {
|
|
|
1695
1695
|
try {
|
|
1696
1696
|
const files = readdirSync(dir).filter((f) => f.endsWith(".jsonl"));
|
|
1697
1697
|
return files.map((file) => {
|
|
1698
|
-
const
|
|
1699
|
-
const stat = statSync(
|
|
1698
|
+
const path5 = join4(dir, file);
|
|
1699
|
+
const stat = statSync(path5);
|
|
1700
1700
|
const name = file.replace(/\.jsonl$/, "");
|
|
1701
|
-
const messageCount = countLines(
|
|
1702
|
-
return { name, path, size: stat.size, messageCount, mtime: stat.mtime };
|
|
1701
|
+
const messageCount = countLines(path5);
|
|
1702
|
+
return { name, path: path5, size: stat.size, messageCount, mtime: stat.mtime };
|
|
1703
1703
|
}).sort((a, b) => b.mtime.getTime() - a.mtime.getTime());
|
|
1704
1704
|
} catch {
|
|
1705
1705
|
return [];
|
|
1706
1706
|
}
|
|
1707
1707
|
}
|
|
1708
1708
|
function deleteSession(name) {
|
|
1709
|
-
const
|
|
1709
|
+
const path5 = sessionPath(name);
|
|
1710
1710
|
try {
|
|
1711
|
-
unlinkSync(
|
|
1712
|
-
const sidecar =
|
|
1711
|
+
unlinkSync(path5);
|
|
1712
|
+
const sidecar = path5.replace(/\.jsonl$/, ".pending.json");
|
|
1713
1713
|
try {
|
|
1714
1714
|
unlinkSync(sidecar);
|
|
1715
1715
|
} catch {
|
|
@@ -1720,19 +1720,19 @@ function deleteSession(name) {
|
|
|
1720
1720
|
}
|
|
1721
1721
|
}
|
|
1722
1722
|
function rewriteSession(name, messages) {
|
|
1723
|
-
const
|
|
1724
|
-
mkdirSync2(dirname3(
|
|
1723
|
+
const path5 = sessionPath(name);
|
|
1724
|
+
mkdirSync2(dirname3(path5), { recursive: true });
|
|
1725
1725
|
const body = messages.map((m) => JSON.stringify(m)).join("\n");
|
|
1726
|
-
writeFileSync2(
|
|
1726
|
+
writeFileSync2(path5, body ? `${body}
|
|
1727
1727
|
` : "", "utf8");
|
|
1728
1728
|
try {
|
|
1729
|
-
chmodSync2(
|
|
1729
|
+
chmodSync2(path5, 384);
|
|
1730
1730
|
} catch {
|
|
1731
1731
|
}
|
|
1732
1732
|
}
|
|
1733
|
-
function countLines(
|
|
1733
|
+
function countLines(path5) {
|
|
1734
1734
|
try {
|
|
1735
|
-
const raw = readFileSync4(
|
|
1735
|
+
const raw = readFileSync4(path5, "utf8");
|
|
1736
1736
|
return raw.split(/\r?\n/).filter((l) => l.trim()).length;
|
|
1737
1737
|
} catch {
|
|
1738
1738
|
return 0;
|
|
@@ -1788,27 +1788,27 @@ var SessionStats = class {
|
|
|
1788
1788
|
return stats2;
|
|
1789
1789
|
}
|
|
1790
1790
|
get totalCost() {
|
|
1791
|
-
return this.turns.reduce((sum,
|
|
1791
|
+
return this.turns.reduce((sum, t2) => sum + t2.cost, 0);
|
|
1792
1792
|
}
|
|
1793
1793
|
get totalClaudeEquivalent() {
|
|
1794
|
-
return this.turns.reduce((sum,
|
|
1794
|
+
return this.turns.reduce((sum, t2) => sum + claudeEquivalentCost(t2.usage), 0);
|
|
1795
1795
|
}
|
|
1796
1796
|
get savingsVsClaude() {
|
|
1797
1797
|
const c = this.totalClaudeEquivalent;
|
|
1798
1798
|
return c > 0 ? 1 - this.totalCost / c : 0;
|
|
1799
1799
|
}
|
|
1800
1800
|
get totalInputCost() {
|
|
1801
|
-
return this.turns.reduce((sum,
|
|
1801
|
+
return this.turns.reduce((sum, t2) => sum + inputCostUsd(t2.model, t2.usage), 0);
|
|
1802
1802
|
}
|
|
1803
1803
|
get totalOutputCost() {
|
|
1804
|
-
return this.turns.reduce((sum,
|
|
1804
|
+
return this.turns.reduce((sum, t2) => sum + outputCostUsd(t2.model, t2.usage), 0);
|
|
1805
1805
|
}
|
|
1806
1806
|
get aggregateCacheHitRatio() {
|
|
1807
1807
|
let hit = 0;
|
|
1808
1808
|
let miss = 0;
|
|
1809
|
-
for (const
|
|
1810
|
-
hit +=
|
|
1811
|
-
miss +=
|
|
1809
|
+
for (const t2 of this.turns) {
|
|
1810
|
+
hit += t2.usage.promptCacheHitTokens;
|
|
1811
|
+
miss += t2.usage.promptCacheMissTokens;
|
|
1812
1812
|
}
|
|
1813
1813
|
const denom = hit + miss;
|
|
1814
1814
|
return denom > 0 ? hit / denom : 0;
|
|
@@ -2198,13 +2198,13 @@ var CacheFirstLoop = class {
|
|
|
2198
2198
|
* with `<`.
|
|
2199
2199
|
*/
|
|
2200
2200
|
looksLikePartialEscalationMarker(buf) {
|
|
2201
|
-
const
|
|
2202
|
-
if (
|
|
2203
|
-
if (
|
|
2204
|
-
return NEEDS_PRO_MARKER_PREFIX.startsWith(
|
|
2201
|
+
const t2 = buf.trimStart();
|
|
2202
|
+
if (t2.length === 0) return true;
|
|
2203
|
+
if (t2.length <= NEEDS_PRO_MARKER_PREFIX.length) {
|
|
2204
|
+
return NEEDS_PRO_MARKER_PREFIX.startsWith(t2);
|
|
2205
2205
|
}
|
|
2206
|
-
if (!
|
|
2207
|
-
const rest =
|
|
2206
|
+
if (!t2.startsWith(NEEDS_PRO_MARKER_PREFIX)) return false;
|
|
2207
|
+
const rest = t2.slice(NEEDS_PRO_MARKER_PREFIX.length);
|
|
2208
2208
|
if (rest[0] !== ">" && rest[0] !== ":") return false;
|
|
2209
2209
|
return true;
|
|
2210
2210
|
}
|
|
@@ -2312,7 +2312,9 @@ var CacheFirstLoop = class {
|
|
|
2312
2312
|
this._proArmedForNextTurn = false;
|
|
2313
2313
|
armedConsumed = true;
|
|
2314
2314
|
}
|
|
2315
|
+
const carryAbort = this._turnAbort.signal.aborted;
|
|
2315
2316
|
this._turnAbort = new AbortController();
|
|
2317
|
+
if (carryAbort) this._turnAbort.abort();
|
|
2316
2318
|
const signal = this._turnAbort.signal;
|
|
2317
2319
|
if (armedConsumed) {
|
|
2318
2320
|
yield {
|
|
@@ -2435,8 +2437,8 @@ var CacheFirstLoop = class {
|
|
|
2435
2437
|
}
|
|
2436
2438
|
);
|
|
2437
2439
|
for (let k = 0; k < budget; k++) {
|
|
2438
|
-
const sample = queue.shift() ?? await new Promise((
|
|
2439
|
-
waiter =
|
|
2440
|
+
const sample = queue.shift() ?? await new Promise((resolve9) => {
|
|
2441
|
+
waiter = resolve9;
|
|
2440
2442
|
});
|
|
2441
2443
|
yield {
|
|
2442
2444
|
turn: this._turn,
|
|
@@ -3174,7 +3176,7 @@ function listFilesWithStatsSync(root, opts = {}) {
|
|
|
3174
3176
|
const ignore = new Set(opts.ignoreDirs ?? DEFAULT_PICKER_IGNORE_DIRS);
|
|
3175
3177
|
const rootAbs = resolve(root);
|
|
3176
3178
|
const out = [];
|
|
3177
|
-
const
|
|
3179
|
+
const walk3 = (dirAbs, dirRel) => {
|
|
3178
3180
|
if (out.length >= maxResults) return;
|
|
3179
3181
|
let entries;
|
|
3180
3182
|
try {
|
|
@@ -3188,7 +3190,7 @@ function listFilesWithStatsSync(root, opts = {}) {
|
|
|
3188
3190
|
const relPath = dirRel ? `${dirRel}/${ent.name}` : ent.name;
|
|
3189
3191
|
if (ent.isDirectory()) {
|
|
3190
3192
|
if (ent.name.startsWith(".") || ignore.has(ent.name)) continue;
|
|
3191
|
-
|
|
3193
|
+
walk3(join5(dirAbs, ent.name), relPath);
|
|
3192
3194
|
} else if (ent.isFile()) {
|
|
3193
3195
|
let mtimeMs = 0;
|
|
3194
3196
|
try {
|
|
@@ -3199,7 +3201,7 @@ function listFilesWithStatsSync(root, opts = {}) {
|
|
|
3199
3201
|
}
|
|
3200
3202
|
}
|
|
3201
3203
|
};
|
|
3202
|
-
|
|
3204
|
+
walk3(rootAbs, "");
|
|
3203
3205
|
return out;
|
|
3204
3206
|
}
|
|
3205
3207
|
var AT_PICKER_PREFIX = /(?:^|\s)@([a-zA-Z0-9_./\\-]*)$/;
|
|
@@ -3259,7 +3261,7 @@ function rankPickerCandidates(files, query, limitOrOpts) {
|
|
|
3259
3261
|
var AT_MENTION_PATTERN = /(?<=^|\s)@([a-zA-Z0-9_./\\-]+)/g;
|
|
3260
3262
|
function expandAtMentions(text, rootDir, opts = {}) {
|
|
3261
3263
|
const maxBytes = opts.maxBytes ?? DEFAULT_AT_MENTION_MAX_BYTES;
|
|
3262
|
-
const
|
|
3264
|
+
const fs6 = opts.fs ?? defaultFs;
|
|
3263
3265
|
const root = resolve(rootDir);
|
|
3264
3266
|
const seen = /* @__PURE__ */ new Map();
|
|
3265
3267
|
const expansions = [];
|
|
@@ -3269,7 +3271,7 @@ function expandAtMentions(text, rootDir, opts = {}) {
|
|
|
3269
3271
|
if (!cleaned) continue;
|
|
3270
3272
|
const token = `@${cleaned}`;
|
|
3271
3273
|
if (seen.has(token)) continue;
|
|
3272
|
-
const expansion = resolveMention(cleaned, root, maxBytes,
|
|
3274
|
+
const expansion = resolveMention(cleaned, root, maxBytes, fs6);
|
|
3273
3275
|
seen.set(token, expansion);
|
|
3274
3276
|
expansions.push(expansion);
|
|
3275
3277
|
}
|
|
@@ -3277,7 +3279,7 @@ function expandAtMentions(text, rootDir, opts = {}) {
|
|
|
3277
3279
|
const blocks = [];
|
|
3278
3280
|
for (const ex of expansions) {
|
|
3279
3281
|
if (ex.ok) {
|
|
3280
|
-
const content = readSafe(root, ex.path,
|
|
3282
|
+
const content = readSafe(root, ex.path, fs6);
|
|
3281
3283
|
blocks.push(`<file path="${ex.path}">
|
|
3282
3284
|
${content}
|
|
3283
3285
|
</file>`);
|
|
@@ -3291,7 +3293,7 @@ ${content}
|
|
|
3291
3293
|
${blocks.join("\n\n")}`;
|
|
3292
3294
|
return { text: augmented, expansions };
|
|
3293
3295
|
}
|
|
3294
|
-
function resolveMention(rawPath, root, maxBytes,
|
|
3296
|
+
function resolveMention(rawPath, root, maxBytes, fs6) {
|
|
3295
3297
|
if (isAbsolute(rawPath)) {
|
|
3296
3298
|
return { token: `@${rawPath}`, path: rawPath, ok: false, skip: "escape" };
|
|
3297
3299
|
}
|
|
@@ -3300,22 +3302,22 @@ function resolveMention(rawPath, root, maxBytes, fs2) {
|
|
|
3300
3302
|
if (rel.startsWith("..") || isAbsolute(rel)) {
|
|
3301
3303
|
return { token: `@${rawPath}`, path: rawPath, ok: false, skip: "escape" };
|
|
3302
3304
|
}
|
|
3303
|
-
if (!
|
|
3305
|
+
if (!fs6.exists(resolved)) {
|
|
3304
3306
|
return { token: `@${rawPath}`, path: rawPath, ok: false, skip: "missing" };
|
|
3305
3307
|
}
|
|
3306
|
-
if (!
|
|
3308
|
+
if (!fs6.isFile(resolved)) {
|
|
3307
3309
|
return { token: `@${rawPath}`, path: rawPath, ok: false, skip: "not-file" };
|
|
3308
3310
|
}
|
|
3309
|
-
const size =
|
|
3311
|
+
const size = fs6.size(resolved);
|
|
3310
3312
|
if (size > maxBytes) {
|
|
3311
3313
|
return { token: `@${rawPath}`, path: rawPath, ok: false, skip: "too-large", bytes: size };
|
|
3312
3314
|
}
|
|
3313
3315
|
return { token: `@${rawPath}`, path: rawPath, ok: true, bytes: size };
|
|
3314
3316
|
}
|
|
3315
|
-
function readSafe(root, rawPath,
|
|
3317
|
+
function readSafe(root, rawPath, fs6) {
|
|
3316
3318
|
const resolved = resolve(root, rawPath);
|
|
3317
3319
|
try {
|
|
3318
|
-
return
|
|
3320
|
+
return fs6.read(resolved);
|
|
3319
3321
|
} catch {
|
|
3320
3322
|
return "(read failed)";
|
|
3321
3323
|
}
|
|
@@ -3338,6 +3340,108 @@ var defaultFs = {
|
|
|
3338
3340
|
},
|
|
3339
3341
|
read: (p) => readFileSync5(p, "utf8")
|
|
3340
3342
|
};
|
|
3343
|
+
var AT_URL_PATTERN = /(?<=^|\s)@(https?:\/\/\S+)/g;
|
|
3344
|
+
var DEFAULT_AT_URL_MAX_CHARS = 32e3;
|
|
3345
|
+
async function expandAtUrls(text, opts = {}) {
|
|
3346
|
+
const maxChars = opts.maxChars ?? DEFAULT_AT_URL_MAX_CHARS;
|
|
3347
|
+
const fetcher = opts.fetcher;
|
|
3348
|
+
if (!fetcher) {
|
|
3349
|
+
throw new Error("expandAtUrls: fetcher option is required (wire src/tools/web.ts:webFetch)");
|
|
3350
|
+
}
|
|
3351
|
+
const seen = /* @__PURE__ */ new Map();
|
|
3352
|
+
const bodies = /* @__PURE__ */ new Map();
|
|
3353
|
+
const order = [];
|
|
3354
|
+
for (const match of text.matchAll(AT_URL_PATTERN)) {
|
|
3355
|
+
const rawUrl = match[1] ?? "";
|
|
3356
|
+
const url = stripUrlTail(rawUrl);
|
|
3357
|
+
if (!url) continue;
|
|
3358
|
+
if (seen.has(url)) continue;
|
|
3359
|
+
const cached2 = opts.cache?.get(url);
|
|
3360
|
+
if (cached2) {
|
|
3361
|
+
seen.set(url, cached2);
|
|
3362
|
+
if (cached2.body) bodies.set(url, cached2.body);
|
|
3363
|
+
order.push(url);
|
|
3364
|
+
continue;
|
|
3365
|
+
}
|
|
3366
|
+
let expansion;
|
|
3367
|
+
let body = "";
|
|
3368
|
+
try {
|
|
3369
|
+
const page = await fetcher(url, {
|
|
3370
|
+
maxChars,
|
|
3371
|
+
timeoutMs: opts.timeoutMs,
|
|
3372
|
+
signal: opts.signal
|
|
3373
|
+
});
|
|
3374
|
+
body = page.text;
|
|
3375
|
+
expansion = {
|
|
3376
|
+
token: `@${url}`,
|
|
3377
|
+
url,
|
|
3378
|
+
ok: true,
|
|
3379
|
+
title: page.title,
|
|
3380
|
+
chars: body.length,
|
|
3381
|
+
truncated: page.truncated
|
|
3382
|
+
};
|
|
3383
|
+
} catch (err) {
|
|
3384
|
+
const message = err.message ?? String(err);
|
|
3385
|
+
let skip = "fetch-error";
|
|
3386
|
+
if (/aborted|timeout/i.test(message)) skip = "timeout";
|
|
3387
|
+
else if (/40\d|forbidden|access denied|captcha/i.test(message)) skip = "blocked";
|
|
3388
|
+
expansion = {
|
|
3389
|
+
token: `@${url}`,
|
|
3390
|
+
url,
|
|
3391
|
+
ok: false,
|
|
3392
|
+
skip,
|
|
3393
|
+
error: message
|
|
3394
|
+
};
|
|
3395
|
+
}
|
|
3396
|
+
seen.set(url, expansion);
|
|
3397
|
+
if (body) bodies.set(url, body);
|
|
3398
|
+
if (opts.cache) opts.cache.set(url, { ...expansion, body });
|
|
3399
|
+
order.push(url);
|
|
3400
|
+
}
|
|
3401
|
+
if (seen.size === 0) return { text, expansions: [] };
|
|
3402
|
+
const expansions = order.map((u) => seen.get(u)).filter(Boolean);
|
|
3403
|
+
const blocks = [];
|
|
3404
|
+
for (const ex of expansions) {
|
|
3405
|
+
if (ex.ok) {
|
|
3406
|
+
const titleAttr = ex.title ? ` title="${escapeAttr(ex.title)}"` : "";
|
|
3407
|
+
const truncTag = ex.truncated ? ' truncated="true"' : "";
|
|
3408
|
+
const body = bodies.get(ex.url) ?? "";
|
|
3409
|
+
blocks.push(`<url href="${ex.url}"${titleAttr}${truncTag}>
|
|
3410
|
+
${body}
|
|
3411
|
+
</url>`);
|
|
3412
|
+
} else {
|
|
3413
|
+
const reasonAttr = ex.skip ?? "fetch-error";
|
|
3414
|
+
blocks.push(`<url href="${ex.url}" skipped="${reasonAttr}" />`);
|
|
3415
|
+
}
|
|
3416
|
+
}
|
|
3417
|
+
const augmented = `${text}
|
|
3418
|
+
|
|
3419
|
+
[Referenced URLs]
|
|
3420
|
+
${blocks.join("\n\n")}`;
|
|
3421
|
+
return { text: augmented, expansions };
|
|
3422
|
+
}
|
|
3423
|
+
function stripUrlTail(raw) {
|
|
3424
|
+
let s = raw;
|
|
3425
|
+
while (s.length > 0) {
|
|
3426
|
+
const last = s[s.length - 1];
|
|
3427
|
+
if (".,;:!?".includes(last)) {
|
|
3428
|
+
s = s.slice(0, -1);
|
|
3429
|
+
continue;
|
|
3430
|
+
}
|
|
3431
|
+
if (")]}>".includes(last)) {
|
|
3432
|
+
const open = { ")": "(", "]": "[", "}": "{", ">": "<" }[last];
|
|
3433
|
+
if (!s.includes(open)) {
|
|
3434
|
+
s = s.slice(0, -1);
|
|
3435
|
+
continue;
|
|
3436
|
+
}
|
|
3437
|
+
}
|
|
3438
|
+
break;
|
|
3439
|
+
}
|
|
3440
|
+
return s;
|
|
3441
|
+
}
|
|
3442
|
+
function escapeAttr(s) {
|
|
3443
|
+
return s.replace(/"/g, """).replace(/[\r\n]+/g, " ").trim();
|
|
3444
|
+
}
|
|
3341
3445
|
|
|
3342
3446
|
// src/tools/filesystem.ts
|
|
3343
3447
|
import { promises as fs } from "fs";
|
|
@@ -3411,9 +3515,9 @@ var BINARY_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
|
3411
3515
|
".pyo"
|
|
3412
3516
|
]);
|
|
3413
3517
|
function isLikelyBinaryByName(name) {
|
|
3414
|
-
const
|
|
3415
|
-
if (
|
|
3416
|
-
return BINARY_EXTENSIONS.has(name.slice(
|
|
3518
|
+
const dot2 = name.lastIndexOf(".");
|
|
3519
|
+
if (dot2 < 0) return false;
|
|
3520
|
+
return BINARY_EXTENSIONS.has(name.slice(dot2).toLowerCase());
|
|
3417
3521
|
}
|
|
3418
3522
|
function registerFilesystemTools(registry, opts) {
|
|
3419
3523
|
const rootDir = pathMod.resolve(opts.rootDir);
|
|
@@ -3564,7 +3668,7 @@ Prefer \`list_directory\` for a single-level view, \`search_files\` to find spec
|
|
|
3564
3668
|
let totalBytes = 0;
|
|
3565
3669
|
let truncated = false;
|
|
3566
3670
|
const PER_DIR_CHILD_CAP = 50;
|
|
3567
|
-
const
|
|
3671
|
+
const walk3 = async (dir, depth) => {
|
|
3568
3672
|
if (truncated) return;
|
|
3569
3673
|
if (depth > maxDepth) return;
|
|
3570
3674
|
let entries;
|
|
@@ -3604,11 +3708,11 @@ Prefer \`list_directory\` for a single-level view, \`search_files\` to find spec
|
|
|
3604
3708
|
lines.push(line);
|
|
3605
3709
|
emitted++;
|
|
3606
3710
|
if (e.isDirectory() && !skip) {
|
|
3607
|
-
await
|
|
3711
|
+
await walk3(pathMod.join(dir, e.name), depth + 1);
|
|
3608
3712
|
}
|
|
3609
3713
|
}
|
|
3610
3714
|
};
|
|
3611
|
-
await
|
|
3715
|
+
await walk3(startAbs, 0);
|
|
3612
3716
|
return lines.join("\n") || "(empty tree)";
|
|
3613
3717
|
}
|
|
3614
3718
|
});
|
|
@@ -3638,7 +3742,7 @@ Prefer \`list_directory\` for a single-level view, \`search_files\` to find spec
|
|
|
3638
3742
|
}
|
|
3639
3743
|
const matches = [];
|
|
3640
3744
|
let totalBytes = 0;
|
|
3641
|
-
const
|
|
3745
|
+
const walk3 = async (dir) => {
|
|
3642
3746
|
let entries;
|
|
3643
3747
|
try {
|
|
3644
3748
|
entries = await fs.readdir(dir, { withFileTypes: true });
|
|
@@ -3658,10 +3762,10 @@ Prefer \`list_directory\` for a single-level view, \`search_files\` to find spec
|
|
|
3658
3762
|
matches.push(rel);
|
|
3659
3763
|
totalBytes += rel.length + 1;
|
|
3660
3764
|
}
|
|
3661
|
-
if (e.isDirectory()) await
|
|
3765
|
+
if (e.isDirectory()) await walk3(full);
|
|
3662
3766
|
}
|
|
3663
3767
|
};
|
|
3664
|
-
await
|
|
3768
|
+
await walk3(startAbs);
|
|
3665
3769
|
return matches.length === 0 ? "(no matches)" : matches.join("\n");
|
|
3666
3770
|
}
|
|
3667
3771
|
});
|
|
@@ -3711,7 +3815,7 @@ Prefer \`list_directory\` for a single-level view, \`search_files\` to find spec
|
|
|
3711
3815
|
let totalBytes = 0;
|
|
3712
3816
|
let scanned = 0;
|
|
3713
3817
|
let truncated = false;
|
|
3714
|
-
const
|
|
3818
|
+
const walk3 = async (dir) => {
|
|
3715
3819
|
if (truncated) return;
|
|
3716
3820
|
let entries;
|
|
3717
3821
|
try {
|
|
@@ -3723,7 +3827,7 @@ Prefer \`list_directory\` for a single-level view, \`search_files\` to find spec
|
|
|
3723
3827
|
if (truncated) return;
|
|
3724
3828
|
if (e.isDirectory()) {
|
|
3725
3829
|
if (!includeDeps && SKIP_DIR_NAMES.has(e.name)) continue;
|
|
3726
|
-
await
|
|
3830
|
+
await walk3(pathMod.join(dir, e.name));
|
|
3727
3831
|
continue;
|
|
3728
3832
|
}
|
|
3729
3833
|
if (!e.isFile()) continue;
|
|
@@ -3766,7 +3870,7 @@ Prefer \`list_directory\` for a single-level view, \`search_files\` to find spec
|
|
|
3766
3870
|
scanned++;
|
|
3767
3871
|
}
|
|
3768
3872
|
};
|
|
3769
|
-
await
|
|
3873
|
+
await walk3(startAbs);
|
|
3770
3874
|
if (matches.length === 0) {
|
|
3771
3875
|
return scanned === 0 ? "(no files scanned \u2014 path empty or all files filtered out)" : `(no matches across ${scanned} file${scanned === 1 ? "" : "s"})`;
|
|
3772
3876
|
}
|
|
@@ -3975,7 +4079,7 @@ function registerMemoryTools(registry, opts = {}) {
|
|
|
3975
4079
|
});
|
|
3976
4080
|
}
|
|
3977
4081
|
try {
|
|
3978
|
-
const
|
|
4082
|
+
const path5 = store.write({
|
|
3979
4083
|
name: args.name,
|
|
3980
4084
|
type: args.type,
|
|
3981
4085
|
scope: args.scope,
|
|
@@ -3988,7 +4092,7 @@ function registerMemoryTools(registry, opts = {}) {
|
|
|
3988
4092
|
"",
|
|
3989
4093
|
"TREAT THIS AS ESTABLISHED FACT for the rest of this session.",
|
|
3990
4094
|
"The user just told you \u2014 don't re-explore the filesystem to re-derive it.",
|
|
3991
|
-
`(Saved to ${
|
|
4095
|
+
`(Saved to ${path5}; pins into the system prompt on next /new or launch.)`
|
|
3992
4096
|
].join("\n");
|
|
3993
4097
|
} catch (err) {
|
|
3994
4098
|
return JSON.stringify({ error: `remember failed: ${err.message}` });
|
|
@@ -4466,7 +4570,11 @@ async function spawnSubagent(opts) {
|
|
|
4466
4570
|
stream: false
|
|
4467
4571
|
});
|
|
4468
4572
|
const onParentAbort = () => childLoop.abort();
|
|
4469
|
-
opts.parentSignal?.
|
|
4573
|
+
if (opts.parentSignal?.aborted) {
|
|
4574
|
+
childLoop.abort();
|
|
4575
|
+
} else {
|
|
4576
|
+
opts.parentSignal?.addEventListener("abort", onParentAbort, { once: true });
|
|
4577
|
+
}
|
|
4470
4578
|
let final = "";
|
|
4471
4579
|
let errorMessage;
|
|
4472
4580
|
let toolIter = 0;
|
|
@@ -4484,7 +4592,11 @@ async function spawnSubagent(opts) {
|
|
|
4484
4592
|
});
|
|
4485
4593
|
}
|
|
4486
4594
|
if (ev.role === "assistant_final") {
|
|
4487
|
-
|
|
4595
|
+
if (ev.forcedSummary) {
|
|
4596
|
+
errorMessage = ev.content?.trim() || "subagent ended without producing an answer";
|
|
4597
|
+
} else {
|
|
4598
|
+
final = ev.content ?? "";
|
|
4599
|
+
}
|
|
4488
4600
|
}
|
|
4489
4601
|
if (ev.role === "error") {
|
|
4490
4602
|
errorMessage = ev.error ?? "subagent error";
|
|
@@ -4531,14 +4643,14 @@ async function spawnSubagent(opts) {
|
|
|
4531
4643
|
usage
|
|
4532
4644
|
};
|
|
4533
4645
|
}
|
|
4534
|
-
function aggregateChildUsage(
|
|
4646
|
+
function aggregateChildUsage(loop2) {
|
|
4535
4647
|
const agg = new Usage();
|
|
4536
|
-
for (const
|
|
4537
|
-
agg.promptTokens +=
|
|
4538
|
-
agg.completionTokens +=
|
|
4539
|
-
agg.totalTokens +=
|
|
4540
|
-
agg.promptCacheHitTokens +=
|
|
4541
|
-
agg.promptCacheMissTokens +=
|
|
4648
|
+
for (const t2 of loop2.stats.turns) {
|
|
4649
|
+
agg.promptTokens += t2.usage.promptTokens;
|
|
4650
|
+
agg.completionTokens += t2.usage.completionTokens;
|
|
4651
|
+
agg.totalTokens += t2.usage.totalTokens;
|
|
4652
|
+
agg.promptCacheHitTokens += t2.usage.promptCacheHitTokens;
|
|
4653
|
+
agg.promptCacheMissTokens += t2.usage.promptCacheMissTokens;
|
|
4542
4654
|
}
|
|
4543
4655
|
return agg;
|
|
4544
4656
|
}
|
|
@@ -5056,7 +5168,7 @@ async function runCommand(cmd, opts) {
|
|
|
5056
5168
|
};
|
|
5057
5169
|
const { bin, args, spawnOverrides } = prepareSpawn(argv);
|
|
5058
5170
|
const effectiveSpawnOpts = { ...spawnOpts, ...spawnOverrides };
|
|
5059
|
-
return await new Promise((
|
|
5171
|
+
return await new Promise((resolve9, reject) => {
|
|
5060
5172
|
let child;
|
|
5061
5173
|
try {
|
|
5062
5174
|
child = spawn3(bin, args, effectiveSpawnOpts);
|
|
@@ -5089,7 +5201,7 @@ async function runCommand(cmd, opts) {
|
|
|
5089
5201
|
const output = buf.length > maxChars ? `${buf.slice(0, maxChars)}
|
|
5090
5202
|
|
|
5091
5203
|
[\u2026 truncated ${buf.length - maxChars} chars \u2026]` : buf;
|
|
5092
|
-
|
|
5204
|
+
resolve9({ exitCode: code, output, timedOut });
|
|
5093
5205
|
});
|
|
5094
5206
|
});
|
|
5095
5207
|
}
|
|
@@ -5204,7 +5316,7 @@ function registerShellTools(registry, opts) {
|
|
|
5204
5316
|
const snapshot2 = opts.extraAllowed ?? [];
|
|
5205
5317
|
return () => snapshot2;
|
|
5206
5318
|
})();
|
|
5207
|
-
const
|
|
5319
|
+
const isAllowAll = typeof opts.allowAll === "function" ? opts.allowAll : () => opts.allowAll === true;
|
|
5208
5320
|
registry.register({
|
|
5209
5321
|
name: "run_command",
|
|
5210
5322
|
description: "Run a shell command in the project root and return its combined stdout+stderr.\n\nConstraints (read these before the first call):\n\u2022 ONE process per call, NO shell expansion. `&&`, `||`, `|`, `;`, `>`, `<`, `2>&1` are all rejected up-front \u2014 split into separate calls and combine results in reasoning. Example: instead of `grep foo *.ts | wc -l`, use `grep -c foo *.ts`; instead of `cd sub && npm test`, use `npm test --prefix sub` (or whatever --cwd flag the binary accepts).\n\u2022 `cd` DOES NOT PERSIST between calls \u2014 each call spawns a fresh process rooted at the project. If a tool needs a subdirectory, pass it via the tool's own flag (`npm --prefix`, `cargo -C`, `git -C`, `pytest tests/\u2026`), NOT via a preceding `cd`.\n\u2022 Avoid commands with unbounded output (`netstat -ano`, `find /`, etc.) \u2014 they waste tokens. Filter at source: `netstat -ano -p TCP`, `find src -name '*.ts'`, `grep -c`, `wc -l`.\n\nCommon read-only inspection and test/lint/typecheck commands run immediately; anything that could mutate state, install dependencies, or touch the network is refused until the user confirms it in the TUI. Prefer this over asking the user to run a command manually \u2014 after edits, run the project's tests to verify.",
|
|
@@ -5213,7 +5325,7 @@ function registerShellTools(registry, opts) {
|
|
|
5213
5325
|
// during planning. Anything that would otherwise trigger a
|
|
5214
5326
|
// confirmation prompt is treated as "not read-only" and bounced.
|
|
5215
5327
|
readOnlyCheck: (args) => {
|
|
5216
|
-
if (
|
|
5328
|
+
if (isAllowAll()) return true;
|
|
5217
5329
|
const cmd = typeof args?.command === "string" ? args.command.trim() : "";
|
|
5218
5330
|
if (!cmd) return false;
|
|
5219
5331
|
return isAllowed(cmd, getExtraAllowed());
|
|
@@ -5235,7 +5347,7 @@ function registerShellTools(registry, opts) {
|
|
|
5235
5347
|
fn: async (args, ctx) => {
|
|
5236
5348
|
const cmd = args.command.trim();
|
|
5237
5349
|
if (!cmd) throw new Error("run_command: empty command");
|
|
5238
|
-
if (!
|
|
5350
|
+
if (!isAllowAll() && !isAllowed(cmd, getExtraAllowed())) {
|
|
5239
5351
|
throw new NeedsConfirmationError(cmd);
|
|
5240
5352
|
}
|
|
5241
5353
|
const effectiveTimeout = Math.max(1, Math.min(600, args.timeoutSec ?? timeoutSec));
|
|
@@ -5268,7 +5380,7 @@ function registerShellTools(registry, opts) {
|
|
|
5268
5380
|
fn: async (args, ctx) => {
|
|
5269
5381
|
const cmd = args.command.trim();
|
|
5270
5382
|
if (!cmd) throw new Error("run_background: empty command");
|
|
5271
|
-
if (!
|
|
5383
|
+
if (!isAllowAll() && !isAllowed(cmd, getExtraAllowed())) {
|
|
5272
5384
|
throw new NeedsConfirmationError(cmd);
|
|
5273
5385
|
}
|
|
5274
5386
|
const result = await jobs2.start(cmd, {
|
|
@@ -5562,10 +5674,10 @@ ${i + 1}. ${r.title}`);
|
|
|
5562
5674
|
// src/env.ts
|
|
5563
5675
|
import { readFileSync as readFileSync6 } from "fs";
|
|
5564
5676
|
import { resolve as resolve5 } from "path";
|
|
5565
|
-
function loadDotenv(
|
|
5677
|
+
function loadDotenv(path5 = ".env") {
|
|
5566
5678
|
let raw;
|
|
5567
5679
|
try {
|
|
5568
|
-
raw = readFileSync6(resolve5(process.cwd(),
|
|
5680
|
+
raw = readFileSync6(resolve5(process.cwd(), path5), "utf8");
|
|
5569
5681
|
} catch {
|
|
5570
5682
|
return;
|
|
5571
5683
|
}
|
|
@@ -5629,13 +5741,13 @@ function writeMeta(stream, meta) {
|
|
|
5629
5741
|
stream.write(`${JSON.stringify(line)}
|
|
5630
5742
|
`);
|
|
5631
5743
|
}
|
|
5632
|
-
function openTranscriptFile(
|
|
5633
|
-
const stream = createWriteStream(
|
|
5744
|
+
function openTranscriptFile(path5, meta) {
|
|
5745
|
+
const stream = createWriteStream(path5, { flags: "a" });
|
|
5634
5746
|
writeMeta(stream, meta);
|
|
5635
5747
|
return stream;
|
|
5636
5748
|
}
|
|
5637
|
-
function readTranscript(
|
|
5638
|
-
const raw = readFileSync7(
|
|
5749
|
+
function readTranscript(path5) {
|
|
5750
|
+
const raw = readFileSync7(path5, "utf8");
|
|
5639
5751
|
return parseTranscript(raw);
|
|
5640
5752
|
}
|
|
5641
5753
|
function isPlanStateEmptyShape(s) {
|
|
@@ -5684,8 +5796,8 @@ function computeCumulativeStats(pages, upToIdx) {
|
|
|
5684
5796
|
}
|
|
5685
5797
|
return computeReplayStats(flat);
|
|
5686
5798
|
}
|
|
5687
|
-
function replayFromFile(
|
|
5688
|
-
const parsed = readTranscript(
|
|
5799
|
+
function replayFromFile(path5) {
|
|
5800
|
+
const parsed = readTranscript(path5);
|
|
5689
5801
|
return { parsed, stats: computeReplayStats(parsed.records) };
|
|
5690
5802
|
}
|
|
5691
5803
|
function computeReplayStats(records) {
|
|
@@ -5742,15 +5854,15 @@ function computeReplayStats(records) {
|
|
|
5742
5854
|
};
|
|
5743
5855
|
}
|
|
5744
5856
|
function summarizeTurns(turns) {
|
|
5745
|
-
const totalCost = turns.reduce((s,
|
|
5746
|
-
const totalInput = turns.reduce((s,
|
|
5747
|
-
const totalOutput = turns.reduce((s,
|
|
5748
|
-
const totalClaude = turns.reduce((s,
|
|
5857
|
+
const totalCost = turns.reduce((s, t2) => s + t2.cost, 0);
|
|
5858
|
+
const totalInput = turns.reduce((s, t2) => s + inputCostUsd(t2.model, t2.usage), 0);
|
|
5859
|
+
const totalOutput = turns.reduce((s, t2) => s + outputCostUsd(t2.model, t2.usage), 0);
|
|
5860
|
+
const totalClaude = turns.reduce((s, t2) => s + claudeEquivalentCost(t2.usage), 0);
|
|
5749
5861
|
let hit = 0;
|
|
5750
5862
|
let miss = 0;
|
|
5751
|
-
for (const
|
|
5752
|
-
hit +=
|
|
5753
|
-
miss +=
|
|
5863
|
+
for (const t2 of turns) {
|
|
5864
|
+
hit += t2.usage.promptCacheHitTokens;
|
|
5865
|
+
miss += t2.usage.promptCacheMissTokens;
|
|
5754
5866
|
}
|
|
5755
5867
|
const cacheHitRatio = hit + miss > 0 ? hit / (hit + miss) : 0;
|
|
5756
5868
|
const savingsVsClaude = totalClaude > 0 ? 1 - totalCost / totalClaude : 0;
|
|
@@ -5827,8 +5939,8 @@ function diffTranscripts(a, b) {
|
|
|
5827
5939
|
return { a: aSide, b: bSide, pairs, firstDivergenceTurn };
|
|
5828
5940
|
}
|
|
5829
5941
|
function classifyDivergence(a, b, aTools, bTools) {
|
|
5830
|
-
const aNames = aTools.map((
|
|
5831
|
-
const bNames = bTools.map((
|
|
5942
|
+
const aNames = aTools.map((t2) => t2.tool ?? "").sort();
|
|
5943
|
+
const bNames = bTools.map((t2) => t2.tool ?? "").sort();
|
|
5832
5944
|
if (aNames.join(",") !== bNames.join(",")) {
|
|
5833
5945
|
return `tool calls differ: A=[${aNames.join(",") || "\u2014"}] B=[${bNames.join(",") || "\u2014"}]`;
|
|
5834
5946
|
}
|
|
@@ -5858,7 +5970,7 @@ function tokenOverlap(a, b) {
|
|
|
5858
5970
|
const tb = new Set(b.toLowerCase().split(/\s+/).filter(Boolean));
|
|
5859
5971
|
if (ta.size === 0 && tb.size === 0) return 1;
|
|
5860
5972
|
let shared = 0;
|
|
5861
|
-
for (const
|
|
5973
|
+
for (const t2 of ta) if (tb.has(t2)) shared++;
|
|
5862
5974
|
return 2 * shared / (ta.size + tb.size);
|
|
5863
5975
|
}
|
|
5864
5976
|
function levenshtein(a, b) {
|
|
@@ -6051,8 +6163,8 @@ function renderMarkdown(report) {
|
|
|
6051
6163
|
out.push(`| turn | kind | ${a.label} tool calls | ${b.label} tool calls | note |`);
|
|
6052
6164
|
out.push("|---:|:---:|---|---|---|");
|
|
6053
6165
|
for (const p of report.pairs) {
|
|
6054
|
-
const aTools = p.aTools.map((
|
|
6055
|
-
const bTools = p.bTools.map((
|
|
6166
|
+
const aTools = p.aTools.map((t2) => t2.tool).filter(Boolean).join(", ") || "\u2014";
|
|
6167
|
+
const bTools = p.bTools.map((t2) => t2.tool).filter(Boolean).join(", ") || "\u2014";
|
|
6056
6168
|
out.push(`| ${p.turn} | ${p.kind} | ${aTools} | ${bTools} | ${p.divergenceNote ?? ""} |`);
|
|
6057
6169
|
}
|
|
6058
6170
|
out.push("");
|
|
@@ -6279,7 +6391,7 @@ var McpClient = class {
|
|
|
6279
6391
|
const id = this.nextId++;
|
|
6280
6392
|
const frame = { jsonrpc: "2.0", id, method, params };
|
|
6281
6393
|
let abortHandler = null;
|
|
6282
|
-
const promise = new Promise((
|
|
6394
|
+
const promise = new Promise((resolve9, reject) => {
|
|
6283
6395
|
const timeout = setTimeout(() => {
|
|
6284
6396
|
this.pending.delete(id);
|
|
6285
6397
|
if (abortHandler && signal) signal.removeEventListener("abort", abortHandler);
|
|
@@ -6288,7 +6400,7 @@ var McpClient = class {
|
|
|
6288
6400
|
);
|
|
6289
6401
|
}, this.requestTimeoutMs);
|
|
6290
6402
|
this.pending.set(id, {
|
|
6291
|
-
resolve:
|
|
6403
|
+
resolve: resolve9,
|
|
6292
6404
|
reject,
|
|
6293
6405
|
timeout
|
|
6294
6406
|
});
|
|
@@ -6411,12 +6523,12 @@ var StdioTransport = class {
|
|
|
6411
6523
|
}
|
|
6412
6524
|
async send(message) {
|
|
6413
6525
|
if (this.closed) throw new Error("MCP transport is closed");
|
|
6414
|
-
return new Promise((
|
|
6526
|
+
return new Promise((resolve9, reject) => {
|
|
6415
6527
|
const line = `${JSON.stringify(message)}
|
|
6416
6528
|
`;
|
|
6417
6529
|
this.child.stdin.write(line, "utf8", (err) => {
|
|
6418
6530
|
if (err) reject(err);
|
|
6419
|
-
else
|
|
6531
|
+
else resolve9();
|
|
6420
6532
|
});
|
|
6421
6533
|
});
|
|
6422
6534
|
}
|
|
@@ -6427,8 +6539,8 @@ var StdioTransport = class {
|
|
|
6427
6539
|
continue;
|
|
6428
6540
|
}
|
|
6429
6541
|
if (this.closed) return;
|
|
6430
|
-
const next = await new Promise((
|
|
6431
|
-
this.waiters.push(
|
|
6542
|
+
const next = await new Promise((resolve9) => {
|
|
6543
|
+
this.waiters.push(resolve9);
|
|
6432
6544
|
});
|
|
6433
6545
|
if (next === null) return;
|
|
6434
6546
|
yield next;
|
|
@@ -6494,8 +6606,8 @@ var SseTransport = class {
|
|
|
6494
6606
|
constructor(opts) {
|
|
6495
6607
|
this.url = opts.url;
|
|
6496
6608
|
this.headers = opts.headers ?? {};
|
|
6497
|
-
this.endpointReady = new Promise((
|
|
6498
|
-
this.resolveEndpoint =
|
|
6609
|
+
this.endpointReady = new Promise((resolve9, reject) => {
|
|
6610
|
+
this.resolveEndpoint = resolve9;
|
|
6499
6611
|
this.rejectEndpoint = reject;
|
|
6500
6612
|
});
|
|
6501
6613
|
this.endpointReady.catch(() => void 0);
|
|
@@ -6522,8 +6634,8 @@ var SseTransport = class {
|
|
|
6522
6634
|
continue;
|
|
6523
6635
|
}
|
|
6524
6636
|
if (this.closed) return;
|
|
6525
|
-
const next = await new Promise((
|
|
6526
|
-
this.waiters.push(
|
|
6637
|
+
const next = await new Promise((resolve9) => {
|
|
6638
|
+
this.waiters.push(resolve9);
|
|
6527
6639
|
});
|
|
6528
6640
|
if (next === null) return;
|
|
6529
6641
|
yield next;
|
|
@@ -6791,8 +6903,8 @@ function applyEditBlock(block, rootDir) {
|
|
|
6791
6903
|
function applyEditBlocks(blocks, rootDir) {
|
|
6792
6904
|
return blocks.map((b) => applyEditBlock(b, rootDir));
|
|
6793
6905
|
}
|
|
6794
|
-
function toWholeFileEditBlock(
|
|
6795
|
-
const abs = resolve6(rootDir,
|
|
6906
|
+
function toWholeFileEditBlock(path5, content, rootDir) {
|
|
6907
|
+
const abs = resolve6(rootDir, path5);
|
|
6796
6908
|
let search = "";
|
|
6797
6909
|
if (existsSync6(abs)) {
|
|
6798
6910
|
try {
|
|
@@ -6801,7 +6913,7 @@ function toWholeFileEditBlock(path, content, rootDir) {
|
|
|
6801
6913
|
search = "";
|
|
6802
6914
|
}
|
|
6803
6915
|
}
|
|
6804
|
-
return { path, search, replace: content, offset: 0 };
|
|
6916
|
+
return { path: path5, search, replace: content, offset: 0 };
|
|
6805
6917
|
}
|
|
6806
6918
|
function snapshotBeforeEdits(blocks, rootDir) {
|
|
6807
6919
|
const absRoot = resolve6(rootDir);
|
|
@@ -6980,20 +7092,20 @@ function appendUsage(input) {
|
|
|
6980
7092
|
};
|
|
6981
7093
|
if (input.kind === "subagent") record.kind = "subagent";
|
|
6982
7094
|
if (input.subagent) record.subagent = input.subagent;
|
|
6983
|
-
const
|
|
7095
|
+
const path5 = input.path ?? defaultUsageLogPath();
|
|
6984
7096
|
try {
|
|
6985
|
-
mkdirSync5(dirname7(
|
|
6986
|
-
appendFileSync2(
|
|
7097
|
+
mkdirSync5(dirname7(path5), { recursive: true });
|
|
7098
|
+
appendFileSync2(path5, `${JSON.stringify(record)}
|
|
6987
7099
|
`, "utf8");
|
|
6988
7100
|
} catch {
|
|
6989
7101
|
}
|
|
6990
7102
|
return record;
|
|
6991
7103
|
}
|
|
6992
|
-
function readUsageLog(
|
|
6993
|
-
if (!existsSync8(
|
|
7104
|
+
function readUsageLog(path5 = defaultUsageLogPath()) {
|
|
7105
|
+
if (!existsSync8(path5)) return [];
|
|
6994
7106
|
let raw;
|
|
6995
7107
|
try {
|
|
6996
|
-
raw = readFileSync10(
|
|
7108
|
+
raw = readFileSync10(path5, "utf8");
|
|
6997
7109
|
} catch {
|
|
6998
7110
|
return [];
|
|
6999
7111
|
}
|
|
@@ -7097,10 +7209,10 @@ function aggregateUsage(records, opts = {}) {
|
|
|
7097
7209
|
subagents
|
|
7098
7210
|
};
|
|
7099
7211
|
}
|
|
7100
|
-
function formatLogSize(
|
|
7101
|
-
if (!existsSync8(
|
|
7212
|
+
function formatLogSize(path5 = defaultUsageLogPath()) {
|
|
7213
|
+
if (!existsSync8(path5)) return "";
|
|
7102
7214
|
try {
|
|
7103
|
-
const s = statSync4(
|
|
7215
|
+
const s = statSync4(path5);
|
|
7104
7216
|
const bytes = s.size;
|
|
7105
7217
|
if (bytes < 1024) return `${bytes} B`;
|
|
7106
7218
|
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
|
|
@@ -7116,7 +7228,7 @@ import { render } from "ink";
|
|
|
7116
7228
|
import React26, { useState as useState12 } from "react";
|
|
7117
7229
|
|
|
7118
7230
|
// src/cli/ui/App.tsx
|
|
7119
|
-
import { Box as Box21, Static, useApp, useStdout as useStdout8 } from "ink";
|
|
7231
|
+
import { Box as Box21, Static, Text as Text19, useApp, useStdout as useStdout8 } from "ink";
|
|
7120
7232
|
import React23, { useCallback as useCallback4, useEffect as useEffect6, useMemo as useMemo3, useRef as useRef6, useState as useState10 } from "react";
|
|
7121
7233
|
|
|
7122
7234
|
// src/code/pending-edits.ts
|
|
@@ -7127,24 +7239,24 @@ function pendingEditsPath(sessionName) {
|
|
|
7127
7239
|
}
|
|
7128
7240
|
function savePendingEdits(sessionName, blocks) {
|
|
7129
7241
|
if (!sessionName) return;
|
|
7130
|
-
const
|
|
7242
|
+
const path5 = pendingEditsPath(sessionName);
|
|
7131
7243
|
try {
|
|
7132
7244
|
if (blocks.length === 0) {
|
|
7133
|
-
if (existsSync9(
|
|
7245
|
+
if (existsSync9(path5)) unlinkSync3(path5);
|
|
7134
7246
|
return;
|
|
7135
7247
|
}
|
|
7136
|
-
mkdirSync6(dirname8(
|
|
7137
|
-
writeFileSync5(
|
|
7248
|
+
mkdirSync6(dirname8(path5), { recursive: true });
|
|
7249
|
+
writeFileSync5(path5, JSON.stringify(blocks, null, 2), "utf8");
|
|
7138
7250
|
} catch {
|
|
7139
7251
|
}
|
|
7140
7252
|
}
|
|
7141
7253
|
function loadPendingEdits(sessionName) {
|
|
7142
7254
|
if (!sessionName) return null;
|
|
7143
|
-
const
|
|
7144
|
-
if (!existsSync9(
|
|
7255
|
+
const path5 = pendingEditsPath(sessionName);
|
|
7256
|
+
if (!existsSync9(path5)) return null;
|
|
7145
7257
|
let raw;
|
|
7146
7258
|
try {
|
|
7147
|
-
raw = readFileSync11(
|
|
7259
|
+
raw = readFileSync11(path5, "utf8");
|
|
7148
7260
|
} catch {
|
|
7149
7261
|
return null;
|
|
7150
7262
|
}
|
|
@@ -7164,9 +7276,9 @@ function loadPendingEdits(sessionName) {
|
|
|
7164
7276
|
}
|
|
7165
7277
|
function clearPendingEdits(sessionName) {
|
|
7166
7278
|
if (!sessionName) return;
|
|
7167
|
-
const
|
|
7279
|
+
const path5 = pendingEditsPath(sessionName);
|
|
7168
7280
|
try {
|
|
7169
|
-
if (existsSync9(
|
|
7281
|
+
if (existsSync9(path5)) unlinkSync3(path5);
|
|
7170
7282
|
} catch {
|
|
7171
7283
|
}
|
|
7172
7284
|
}
|
|
@@ -7187,10 +7299,10 @@ function planStatePath(sessionName) {
|
|
|
7187
7299
|
return join10(sessionsDir(), `${sanitizeName(sessionName)}.plan.json`);
|
|
7188
7300
|
}
|
|
7189
7301
|
function loadPlanState(sessionName) {
|
|
7190
|
-
const
|
|
7191
|
-
if (!existsSync10(
|
|
7302
|
+
const path5 = planStatePath(sessionName);
|
|
7303
|
+
if (!existsSync10(path5)) return null;
|
|
7192
7304
|
try {
|
|
7193
|
-
const raw = readFileSync12(
|
|
7305
|
+
const raw = readFileSync12(path5, "utf8");
|
|
7194
7306
|
const parsed = JSON.parse(raw);
|
|
7195
7307
|
if (!parsed || typeof parsed !== "object") return null;
|
|
7196
7308
|
if (parsed.version !== 1) return null;
|
|
@@ -7226,9 +7338,9 @@ function loadPlanState(sessionName) {
|
|
|
7226
7338
|
}
|
|
7227
7339
|
}
|
|
7228
7340
|
function savePlanState(sessionName, steps, completedStepIds, extras) {
|
|
7229
|
-
const
|
|
7341
|
+
const path5 = planStatePath(sessionName);
|
|
7230
7342
|
try {
|
|
7231
|
-
mkdirSync7(dirname9(
|
|
7343
|
+
mkdirSync7(dirname9(path5), { recursive: true });
|
|
7232
7344
|
const state = {
|
|
7233
7345
|
version: 1,
|
|
7234
7346
|
steps,
|
|
@@ -7237,7 +7349,7 @@ function savePlanState(sessionName, steps, completedStepIds, extras) {
|
|
|
7237
7349
|
};
|
|
7238
7350
|
if (extras?.body) state.body = extras.body;
|
|
7239
7351
|
if (extras?.summary) state.summary = extras.summary;
|
|
7240
|
-
writeFileSync6(
|
|
7352
|
+
writeFileSync6(path5, `${JSON.stringify(state, null, 2)}
|
|
7241
7353
|
`, "utf8");
|
|
7242
7354
|
} catch (err) {
|
|
7243
7355
|
process.stderr.write(
|
|
@@ -7247,9 +7359,9 @@ function savePlanState(sessionName, steps, completedStepIds, extras) {
|
|
|
7247
7359
|
}
|
|
7248
7360
|
}
|
|
7249
7361
|
function clearPlanState(sessionName) {
|
|
7250
|
-
const
|
|
7362
|
+
const path5 = planStatePath(sessionName);
|
|
7251
7363
|
try {
|
|
7252
|
-
if (existsSync10(
|
|
7364
|
+
if (existsSync10(path5)) unlinkSync4(path5);
|
|
7253
7365
|
} catch {
|
|
7254
7366
|
}
|
|
7255
7367
|
}
|
|
@@ -7317,9 +7429,9 @@ function listPlanArchives(sessionName) {
|
|
|
7317
7429
|
return summaries;
|
|
7318
7430
|
}
|
|
7319
7431
|
function relativeTime(updatedAt, now = Date.now()) {
|
|
7320
|
-
const
|
|
7321
|
-
if (Number.isNaN(
|
|
7322
|
-
const diffMs = Math.max(0, now -
|
|
7432
|
+
const t2 = Date.parse(updatedAt);
|
|
7433
|
+
if (Number.isNaN(t2)) return updatedAt;
|
|
7434
|
+
const diffMs = Math.max(0, now - t2);
|
|
7323
7435
|
const sec = Math.floor(diffMs / 1e3);
|
|
7324
7436
|
if (sec < 60) return `${sec}s ago`;
|
|
7325
7437
|
const min = Math.floor(sec / 60);
|
|
@@ -7364,7 +7476,7 @@ function registerSkillTools(registry, opts = {}) {
|
|
|
7364
7476
|
}
|
|
7365
7477
|
const stripped = raw.replace(/\[[^\]]*\]/g, " ").trim();
|
|
7366
7478
|
const tokens = stripped.split(/\s+/).filter(Boolean);
|
|
7367
|
-
const name = tokens.find((
|
|
7479
|
+
const name = tokens.find((t2) => /^[a-zA-Z0-9]/.test(t2)) ?? "";
|
|
7368
7480
|
if (!name) {
|
|
7369
7481
|
return JSON.stringify({
|
|
7370
7482
|
error: "run_skill requires a 'name' argument",
|
|
@@ -7427,12 +7539,12 @@ function AtMentionSuggestions({
|
|
|
7427
7539
|
const shown = matches.slice(windowStart, windowStart + MAX);
|
|
7428
7540
|
const hiddenAbove = windowStart;
|
|
7429
7541
|
const hiddenBelow = total - windowStart - shown.length;
|
|
7430
|
-
return /* @__PURE__ */ React.createElement(Box, { flexDirection: "column", paddingX: 1, marginTop: 1 }, hiddenAbove > 0 ? /* @__PURE__ */ React.createElement(Text, { dimColor: true }, " \u2191 ", hiddenAbove, " more above") : null, shown.map((
|
|
7542
|
+
return /* @__PURE__ */ React.createElement(Box, { flexDirection: "column", paddingX: 1, marginTop: 1 }, hiddenAbove > 0 ? /* @__PURE__ */ React.createElement(Text, { dimColor: true }, " \u2191 ", hiddenAbove, " more above") : null, shown.map((path5, i) => /* @__PURE__ */ React.createElement(FileRow, { key: path5, path: path5, isSelected: windowStart + i === selectedIndex })), hiddenBelow > 0 ? /* @__PURE__ */ React.createElement(Text, { dimColor: true }, " \u2193 ", hiddenBelow, " more below") : null, /* @__PURE__ */ React.createElement(Text, { dimColor: true }, " [\u2191\u2193] navigate \xB7 [Tab]/[Enter] pick \xB7 file content inlined on send"));
|
|
7431
7543
|
}
|
|
7432
|
-
function FileRow({ path, isSelected }) {
|
|
7433
|
-
const slash =
|
|
7434
|
-
const dir = slash >= 0 ? `${
|
|
7435
|
-
const base = slash >= 0 ?
|
|
7544
|
+
function FileRow({ path: path5, isSelected }) {
|
|
7545
|
+
const slash = path5.lastIndexOf("/");
|
|
7546
|
+
const dir = slash >= 0 ? `${path5.slice(0, slash)}/` : "";
|
|
7547
|
+
const base = slash >= 0 ? path5.slice(slash + 1) : path5;
|
|
7436
7548
|
if (isSelected) {
|
|
7437
7549
|
return /* @__PURE__ */ React.createElement(Box, null, /* @__PURE__ */ React.createElement(Text, { backgroundColor: "#67e8f9", color: "black", bold: true }, ` \u25B8 ${base}${dir ? ` ${dir}` : ""} `));
|
|
7438
7550
|
}
|
|
@@ -7453,8 +7565,8 @@ function ModalCard({
|
|
|
7453
7565
|
icon,
|
|
7454
7566
|
children
|
|
7455
7567
|
}) {
|
|
7456
|
-
const { stdout:
|
|
7457
|
-
const cols =
|
|
7568
|
+
const { stdout: stdout3 } = useStdout();
|
|
7569
|
+
const cols = stdout3?.columns ?? 80;
|
|
7458
7570
|
const ruleWidth = Math.min(80, Math.max(28, cols - 4));
|
|
7459
7571
|
const titleText = icon ? ` ${icon} ${title} ` : ` ${title} `;
|
|
7460
7572
|
return /* @__PURE__ */ React2.createElement(Box2, { flexDirection: "column", paddingX: 1, marginY: 1 }, /* @__PURE__ */ React2.createElement(Box2, null, /* @__PURE__ */ React2.createElement(Text2, { color: accent }, "\u2594".repeat(ruleWidth))), /* @__PURE__ */ React2.createElement(Box2, { marginTop: 1 }, /* @__PURE__ */ React2.createElement(Text2, { backgroundColor: accent, color: "black", bold: true }, titleText), subtitle ? /* @__PURE__ */ React2.createElement(Text2, { dimColor: true }, ` ${subtitle}`) : null), /* @__PURE__ */ React2.createElement(Box2, { marginTop: 1, flexDirection: "column" }, children), /* @__PURE__ */ React2.createElement(Box2, { marginTop: 1 }, /* @__PURE__ */ React2.createElement(Text2, { color: accent, dimColor: true }, "\u2581".repeat(ruleWidth))));
|
|
@@ -8017,8 +8129,8 @@ function capLines(lines, maxLines, indent) {
|
|
|
8017
8129
|
var MODAL_OVERHEAD_ROWS = 18;
|
|
8018
8130
|
var MIN_DIFF_ROWS = 8;
|
|
8019
8131
|
function EditConfirm({ block, onChoose }) {
|
|
8020
|
-
const { stdout:
|
|
8021
|
-
const rows =
|
|
8132
|
+
const { stdout: stdout3 } = useStdout2();
|
|
8133
|
+
const rows = stdout3?.rows ?? 40;
|
|
8022
8134
|
const budget = Math.max(MIN_DIFF_ROWS, rows - MODAL_OVERHEAD_ROWS);
|
|
8023
8135
|
const allLines = useMemo(
|
|
8024
8136
|
() => formatEditBlockDiff(block, { contextLines: 2, maxLines: 1e5, indent: " " }),
|
|
@@ -9008,10 +9120,10 @@ function gradientCells(width, glyph = GLYPH.block) {
|
|
|
9008
9120
|
if (width <= 0) return cells;
|
|
9009
9121
|
const last = GRADIENT.length - 1;
|
|
9010
9122
|
for (let i = 0; i < width; i++) {
|
|
9011
|
-
const
|
|
9012
|
-
const lo = Math.floor(
|
|
9123
|
+
const t2 = width === 1 ? 0 : i * last / (width - 1);
|
|
9124
|
+
const lo = Math.floor(t2);
|
|
9013
9125
|
const hi = Math.min(last, lo + 1);
|
|
9014
|
-
const color =
|
|
9126
|
+
const color = t2 - lo < 0.5 ? GRADIENT[lo] : GRADIENT[hi];
|
|
9015
9127
|
cells.push({ ch: glyph, color });
|
|
9016
9128
|
}
|
|
9017
9129
|
return cells;
|
|
@@ -9025,7 +9137,7 @@ function TickerProvider({ children, disabled }) {
|
|
|
9025
9137
|
const [tick, setTick] = useState3(0);
|
|
9026
9138
|
useEffect2(() => {
|
|
9027
9139
|
if (disabled) return;
|
|
9028
|
-
const id = setInterval(() => setTick((
|
|
9140
|
+
const id = setInterval(() => setTick((t2) => t2 + 1), TICK_MS);
|
|
9029
9141
|
return () => clearInterval(id);
|
|
9030
9142
|
}, [disabled]);
|
|
9031
9143
|
return /* @__PURE__ */ React10.createElement(TickContext.Provider, { value: tick }, children);
|
|
@@ -9314,8 +9426,8 @@ var EventRow = React11.memo(function EventRow2({
|
|
|
9314
9426
|
return /* @__PURE__ */ React11.createElement(Box9, null, /* @__PURE__ */ React11.createElement(Text8, null, event.text));
|
|
9315
9427
|
});
|
|
9316
9428
|
function TurnSeparator() {
|
|
9317
|
-
const { stdout:
|
|
9318
|
-
const cols =
|
|
9429
|
+
const { stdout: stdout3 } = useStdout3();
|
|
9430
|
+
const cols = stdout3?.columns ?? 80;
|
|
9319
9431
|
const width = Math.max(16, cols - 2);
|
|
9320
9432
|
const sideWidth = Math.max(2, Math.floor((width - 5) / 2));
|
|
9321
9433
|
const leftCells = gradientCells(sideWidth, "\u2500");
|
|
@@ -9346,10 +9458,10 @@ function EditFileDiff({ text }) {
|
|
|
9346
9458
|
function BranchBlock({ branch: branch2 }) {
|
|
9347
9459
|
return /* @__PURE__ */ React11.createElement(Box9, { flexDirection: "column", marginBottom: 1 }, /* @__PURE__ */ React11.createElement(Box9, null, /* @__PURE__ */ React11.createElement(Text8, { backgroundColor: "#93c5fd", color: "black", bold: true }, ` \u2387 BRANCH \xD7${branch2.budget} `), /* @__PURE__ */ React11.createElement(Text8, null, " "), /* @__PURE__ */ React11.createElement(Text8, { color: "#93c5fd" }, "picked "), /* @__PURE__ */ React11.createElement(Text8, { color: "#93c5fd", bold: true }, "#", branch2.chosenIndex)), /* @__PURE__ */ React11.createElement(Box9, { paddingLeft: 2, marginTop: 1 }, branch2.uncertainties.map((u, i) => {
|
|
9348
9460
|
const chosen = i === branch2.chosenIndex;
|
|
9349
|
-
const
|
|
9461
|
+
const t2 = (branch2.temperatures[i] ?? 0).toFixed(1);
|
|
9350
9462
|
return (
|
|
9351
9463
|
// biome-ignore lint/suspicious/noArrayIndexKey: branch index is positional and stable
|
|
9352
|
-
/* @__PURE__ */ React11.createElement(Text8, { key: `b-${i}` }, /* @__PURE__ */ React11.createElement(Text8, { color: chosen ? "#93c5fd" : "#475569", bold: chosen }, chosen ? "\u25B8 " : " "), /* @__PURE__ */ React11.createElement(Text8, { color: chosen ? "#93c5fd" : "#94a3b8", bold: chosen }, `#${i}`), /* @__PURE__ */ React11.createElement(Text8, { dimColor: true }, ` T=${
|
|
9464
|
+
/* @__PURE__ */ React11.createElement(Text8, { key: `b-${i}` }, /* @__PURE__ */ React11.createElement(Text8, { color: chosen ? "#93c5fd" : "#475569", bold: chosen }, chosen ? "\u25B8 " : " "), /* @__PURE__ */ React11.createElement(Text8, { color: chosen ? "#93c5fd" : "#94a3b8", bold: chosen }, `#${i}`), /* @__PURE__ */ React11.createElement(Text8, { dimColor: true }, ` T=${t2} u=${u} `))
|
|
9353
9465
|
);
|
|
9354
9466
|
})));
|
|
9355
9467
|
}
|
|
@@ -9467,15 +9579,14 @@ function ModeStatusBar({
|
|
|
9467
9579
|
if (planMode) {
|
|
9468
9580
|
return /* @__PURE__ */ React12.createElement(ModeBarFrame, null, /* @__PURE__ */ React12.createElement(ModePill, { label: "PLAN MODE", bg: "red", flash }), /* @__PURE__ */ React12.createElement(Text9, { dimColor: true }, " writes gated \xB7 /plan off to leave"), jobsTag);
|
|
9469
9581
|
}
|
|
9470
|
-
const
|
|
9471
|
-
const
|
|
9472
|
-
const
|
|
9473
|
-
const mid = isAuto ? "edits land now \xB7 u to undo" : pendingCount > 0 ? `${pendingCount} queued \xB7 y apply \xB7 n discard` : "edits queued \xB7 y apply \xB7 n discard";
|
|
9582
|
+
const label = editMode === "yolo" ? "YOLO" : editMode === "auto" ? "AUTO" : "REVIEW";
|
|
9583
|
+
const bg = editMode === "yolo" ? "red" : editMode === "auto" ? "magenta" : "cyan";
|
|
9584
|
+
const mid = editMode === "yolo" ? "edits + shell auto \xB7 /undo to roll back" : editMode === "auto" ? "edits land now \xB7 u to undo" : pendingCount > 0 ? `${pendingCount} queued \xB7 y apply \xB7 n discard` : "edits queued \xB7 y apply \xB7 n discard";
|
|
9474
9585
|
return /* @__PURE__ */ React12.createElement(ModeBarFrame, null, /* @__PURE__ */ React12.createElement(ModePill, { label, bg, flash }), /* @__PURE__ */ React12.createElement(Text9, { dimColor: true }, ` ${mid} \xB7 Shift+Tab to flip`), jobsTag);
|
|
9475
9586
|
}
|
|
9476
9587
|
function ModeBarFrame({ children }) {
|
|
9477
|
-
const { stdout:
|
|
9478
|
-
const cols =
|
|
9588
|
+
const { stdout: stdout3 } = useStdout4();
|
|
9589
|
+
const cols = stdout3?.columns ?? 80;
|
|
9479
9590
|
const ruleWidth = Math.max(20, cols - 2);
|
|
9480
9591
|
return /* @__PURE__ */ React12.createElement(Box10, { flexDirection: "column" }, /* @__PURE__ */ React12.createElement(Box10, { paddingX: 1 }, /* @__PURE__ */ React12.createElement(Text9, { color: "#475569", dimColor: true }, "\u254C".repeat(ruleWidth))), /* @__PURE__ */ React12.createElement(Box10, { paddingX: 1 }, children));
|
|
9481
9592
|
}
|
|
@@ -9534,26 +9645,26 @@ function summarizeToolArgs(name, args) {
|
|
|
9534
9645
|
return args.length > 80 ? `${args.slice(0, 80)}\u2026` : args;
|
|
9535
9646
|
}
|
|
9536
9647
|
const hasSuffix = (s) => name === s || name.endsWith(`_${s}`);
|
|
9537
|
-
const
|
|
9648
|
+
const path5 = typeof parsed.path === "string" ? parsed.path : void 0;
|
|
9538
9649
|
if (hasSuffix("read_file")) {
|
|
9539
9650
|
const head = typeof parsed.head === "number" ? `, head=${parsed.head}` : "";
|
|
9540
9651
|
const tail = typeof parsed.tail === "number" ? `, tail=${parsed.tail}` : "";
|
|
9541
|
-
return `path: ${
|
|
9652
|
+
return `path: ${path5 ?? "?"}${head}${tail}`;
|
|
9542
9653
|
}
|
|
9543
9654
|
if (hasSuffix("write_file")) {
|
|
9544
9655
|
const content = typeof parsed.content === "string" ? parsed.content : "";
|
|
9545
|
-
return `path: ${
|
|
9656
|
+
return `path: ${path5 ?? "?"} (${content.length} chars)`;
|
|
9546
9657
|
}
|
|
9547
9658
|
if (hasSuffix("edit_file")) {
|
|
9548
9659
|
const edits = Array.isArray(parsed.edits) ? parsed.edits.length : 0;
|
|
9549
|
-
return `path: ${
|
|
9660
|
+
return `path: ${path5 ?? "?"} (${edits} edit${edits === 1 ? "" : "s"})`;
|
|
9550
9661
|
}
|
|
9551
9662
|
if (hasSuffix("list_directory") || hasSuffix("directory_tree")) {
|
|
9552
|
-
return `path: ${
|
|
9663
|
+
return `path: ${path5 ?? "?"}`;
|
|
9553
9664
|
}
|
|
9554
9665
|
if (hasSuffix("search_files")) {
|
|
9555
9666
|
const pattern = typeof parsed.pattern === "string" ? parsed.pattern : "?";
|
|
9556
|
-
return `path: ${
|
|
9667
|
+
return `path: ${path5 ?? "?"} \xB7 pattern: ${pattern}`;
|
|
9557
9668
|
}
|
|
9558
9669
|
if (hasSuffix("move_file")) {
|
|
9559
9670
|
const src = typeof parsed.source === "string" ? parsed.source : "?";
|
|
@@ -9561,7 +9672,7 @@ function summarizeToolArgs(name, args) {
|
|
|
9561
9672
|
return `${src} \u2192 ${dst}`;
|
|
9562
9673
|
}
|
|
9563
9674
|
if (hasSuffix("get_file_info")) {
|
|
9564
|
-
return `path: ${
|
|
9675
|
+
return `path: ${path5 ?? "?"}`;
|
|
9565
9676
|
}
|
|
9566
9677
|
return args.length > 80 ? `${args.slice(0, 80)}\u2026` : args;
|
|
9567
9678
|
}
|
|
@@ -10276,8 +10387,8 @@ function PromptInput({
|
|
|
10276
10387
|
if (action.historyHandoff === "prev") onHistoryPrev?.();
|
|
10277
10388
|
if (action.historyHandoff === "next") onHistoryNext?.();
|
|
10278
10389
|
}, !disabled);
|
|
10279
|
-
const { stdout:
|
|
10280
|
-
const cols =
|
|
10390
|
+
const { stdout: stdout3 } = useStdout5();
|
|
10391
|
+
const cols = stdout3?.columns ?? 80;
|
|
10281
10392
|
const narrow = cols <= 90;
|
|
10282
10393
|
const promptBody = narrow ? "\u203A " : "you \u203A ";
|
|
10283
10394
|
const promptPrefix = BAR + promptBody;
|
|
@@ -10648,8 +10759,8 @@ function StatsPanel({
|
|
|
10648
10759
|
const branchOn = (branchBudget ?? 1) > 1;
|
|
10649
10760
|
const ctxMax = DEEPSEEK_CONTEXT_TOKENS[model2] ?? DEFAULT_CONTEXT_TOKENS;
|
|
10650
10761
|
const ctxRatio = summary.lastPromptTokens / ctxMax;
|
|
10651
|
-
const { stdout:
|
|
10652
|
-
const columns =
|
|
10762
|
+
const { stdout: stdout3 } = useStdout6();
|
|
10763
|
+
const columns = stdout3?.columns ?? 80;
|
|
10653
10764
|
const narrow = columns < NARROW_BREAKPOINT;
|
|
10654
10765
|
const coldStart = summary.turns <= COLD_START_TURNS;
|
|
10655
10766
|
return /* @__PURE__ */ React21.createElement(Box19, { flexDirection: "column", paddingX: 1, marginBottom: 1 }, /* @__PURE__ */ React21.createElement(
|
|
@@ -10815,8 +10926,8 @@ function formatTokens(n) {
|
|
|
10815
10926
|
import { Box as Box20, Text as Text18, useStdout as useStdout7 } from "ink";
|
|
10816
10927
|
import React22 from "react";
|
|
10817
10928
|
function WelcomeBanner({ inCodeMode }) {
|
|
10818
|
-
const { stdout:
|
|
10819
|
-
const cols =
|
|
10929
|
+
const { stdout: stdout3 } = useStdout7();
|
|
10930
|
+
const cols = stdout3?.columns ?? 80;
|
|
10820
10931
|
const ruleWidth = Math.min(60, Math.max(28, cols - 4));
|
|
10821
10932
|
return /* @__PURE__ */ React22.createElement(Box20, { flexDirection: "column", paddingX: 1, marginY: 1 }, /* @__PURE__ */ React22.createElement(GradientRule, { width: ruleWidth }), /* @__PURE__ */ React22.createElement(BarRow, null, /* @__PURE__ */ React22.createElement(Text18, { bold: true, color: COLOR.brand }, "\u25C8 welcome"), /* @__PURE__ */ React22.createElement(Text18, { dimColor: true }, " \xB7 type a message to start")), /* @__PURE__ */ React22.createElement(BarRow, null), /* @__PURE__ */ React22.createElement(BarRow, null, /* @__PURE__ */ React22.createElement(Text18, { bold: true, color: COLOR.primary }, "quick start")), /* @__PURE__ */ React22.createElement(Hint, { cmd: "/help", desc: "every command + keyboard shortcut" }), /* @__PURE__ */ React22.createElement(Hint, { cmd: "/skill", desc: "invoke a stored playbook" }), inCodeMode ? /* @__PURE__ */ React22.createElement(React22.Fragment, null, /* @__PURE__ */ React22.createElement(Hint, { cmd: "@path", desc: "inline a file in your message" }), /* @__PURE__ */ React22.createElement(Hint, { cmd: "!cmd", desc: "run a shell command, output goes to context" })) : null, /* @__PURE__ */ React22.createElement(Hint, { cmd: "/exit", desc: "quit (Ctrl+C also works)" }), /* @__PURE__ */ React22.createElement(BarRow, null), /* @__PURE__ */ React22.createElement(BarRow, null, /* @__PURE__ */ React22.createElement(Text18, { dimColor: true, italic: true }, "tip:"), /* @__PURE__ */ React22.createElement(Text18, { dimColor: true }, " Ctrl+J inserts a newline \xB7 trailing \\ also continues")), /* @__PURE__ */ React22.createElement(Box20, { marginTop: 1 }, /* @__PURE__ */ React22.createElement(GradientRule, { width: ruleWidth, thin: true })));
|
|
10822
10933
|
}
|
|
@@ -10877,7 +10988,7 @@ function parseEditIndices(raw, max) {
|
|
|
10877
10988
|
if (!trimmed) return { ok: [] };
|
|
10878
10989
|
if (max <= 0) return { error: "no pending edits to address" };
|
|
10879
10990
|
const seen = /* @__PURE__ */ new Set();
|
|
10880
|
-
const tokens = trimmed.split(",").map((
|
|
10991
|
+
const tokens = trimmed.split(",").map((t2) => t2.trim()).filter((t2) => t2.length > 0);
|
|
10881
10992
|
if (tokens.length === 0) return { ok: [] };
|
|
10882
10993
|
for (const tok of tokens) {
|
|
10883
10994
|
const range = tok.match(/^(\d+)-(\d+)$/);
|
|
@@ -10927,13 +11038,21 @@ function describeRepair(repair) {
|
|
|
10927
11038
|
}
|
|
10928
11039
|
|
|
10929
11040
|
// src/cli/ui/hash-memory.ts
|
|
10930
|
-
import { appendFileSync as appendFileSync3, existsSync as existsSync11, readFileSync as readFileSync14, writeFileSync as writeFileSync7 } from "fs";
|
|
10931
|
-
import {
|
|
10932
|
-
|
|
11041
|
+
import { appendFileSync as appendFileSync3, existsSync as existsSync11, mkdirSync as mkdirSync8, readFileSync as readFileSync14, writeFileSync as writeFileSync7 } from "fs";
|
|
11042
|
+
import { homedir as homedir6 } from "os";
|
|
11043
|
+
import { dirname as dirname10, join as join12 } from "path";
|
|
11044
|
+
var PROJECT_HEADER = `# Reasonix project memory
|
|
10933
11045
|
|
|
10934
11046
|
Notes the user pinned via the \`#\` prompt prefix. The whole file is
|
|
10935
11047
|
loaded into the immutable system prefix every session \u2014 keep it terse.
|
|
10936
11048
|
|
|
11049
|
+
`;
|
|
11050
|
+
var GLOBAL_HEADER = `# Reasonix global memory
|
|
11051
|
+
|
|
11052
|
+
Cross-project notes the user pinned via the \`#g\` prompt prefix. Loaded
|
|
11053
|
+
into every Reasonix session's prefix regardless of working directory.
|
|
11054
|
+
Private to this machine \u2014 not committed anywhere.
|
|
11055
|
+
|
|
10937
11056
|
`;
|
|
10938
11057
|
function detectHashMemory(text) {
|
|
10939
11058
|
if (text.startsWith("\\#")) {
|
|
@@ -10941,28 +11060,105 @@ function detectHashMemory(text) {
|
|
|
10941
11060
|
}
|
|
10942
11061
|
if (!text.startsWith("#")) return null;
|
|
10943
11062
|
if (text.startsWith("##")) return null;
|
|
11063
|
+
if (/^#g\s*$/.test(text)) return null;
|
|
11064
|
+
const globalMatch = /^#g\s+(.+)$/s.exec(text);
|
|
11065
|
+
if (globalMatch) {
|
|
11066
|
+
const body2 = globalMatch[1].trim();
|
|
11067
|
+
if (!body2) return null;
|
|
11068
|
+
return { kind: "memory-global", note: body2 };
|
|
11069
|
+
}
|
|
10944
11070
|
const body = text.slice(1).trim();
|
|
10945
11071
|
if (!body) return null;
|
|
10946
11072
|
return { kind: "memory", note: body };
|
|
10947
11073
|
}
|
|
10948
11074
|
function appendProjectMemory(rootDir, note) {
|
|
10949
|
-
|
|
11075
|
+
return appendBulletToFile(join12(rootDir, PROJECT_MEMORY_FILE), note, PROJECT_HEADER);
|
|
11076
|
+
}
|
|
11077
|
+
var GLOBAL_MEMORY_DIR = ".reasonix";
|
|
11078
|
+
var GLOBAL_MEMORY_FILE = "REASONIX.md";
|
|
11079
|
+
function globalMemoryPath(homeDir = homedir6()) {
|
|
11080
|
+
return join12(homeDir, GLOBAL_MEMORY_DIR, GLOBAL_MEMORY_FILE);
|
|
11081
|
+
}
|
|
11082
|
+
function appendGlobalMemory(note, homeDir) {
|
|
11083
|
+
return appendBulletToFile(globalMemoryPath(homeDir), note, GLOBAL_HEADER);
|
|
11084
|
+
}
|
|
11085
|
+
function appendBulletToFile(path5, note, newFileHeader) {
|
|
10950
11086
|
const trimmed = note.trim();
|
|
10951
11087
|
if (!trimmed) throw new Error("note body cannot be empty");
|
|
10952
11088
|
const bullet = `- ${trimmed}
|
|
10953
11089
|
`;
|
|
10954
|
-
if (!existsSync11(
|
|
10955
|
-
|
|
10956
|
-
|
|
11090
|
+
if (!existsSync11(path5)) {
|
|
11091
|
+
mkdirSync8(dirname10(path5), { recursive: true });
|
|
11092
|
+
writeFileSync7(path5, `${newFileHeader}${bullet}`, "utf8");
|
|
11093
|
+
return { path: path5, created: true };
|
|
10957
11094
|
}
|
|
10958
11095
|
let prefix = "";
|
|
10959
11096
|
try {
|
|
10960
|
-
const existing = readFileSync14(
|
|
11097
|
+
const existing = readFileSync14(path5, "utf8");
|
|
10961
11098
|
if (existing.length > 0 && !existing.endsWith("\n")) prefix = "\n";
|
|
10962
11099
|
} catch {
|
|
10963
11100
|
}
|
|
10964
|
-
appendFileSync3(
|
|
10965
|
-
return { path, created: false };
|
|
11101
|
+
appendFileSync3(path5, `${prefix}${bullet}`, "utf8");
|
|
11102
|
+
return { path: path5, created: false };
|
|
11103
|
+
}
|
|
11104
|
+
|
|
11105
|
+
// src/cli/ui/loop.ts
|
|
11106
|
+
var MIN_LOOP_INTERVAL_MS = 5e3;
|
|
11107
|
+
var MAX_LOOP_INTERVAL_MS = 6 * 60 * 6e4;
|
|
11108
|
+
function parseLoopInterval(raw) {
|
|
11109
|
+
const s = raw.trim().toLowerCase();
|
|
11110
|
+
if (!s) return null;
|
|
11111
|
+
const m = /^([0-9]+(?:\.[0-9]+)?)(s|sec|secs|m|min|mins|h|hr|hrs)?$/.exec(s);
|
|
11112
|
+
if (!m) return null;
|
|
11113
|
+
const n = Number.parseFloat(m[1] ?? "");
|
|
11114
|
+
if (!Number.isFinite(n) || n <= 0) return null;
|
|
11115
|
+
const unit = m[2] ?? "s";
|
|
11116
|
+
let ms;
|
|
11117
|
+
if (unit === "s" || unit === "sec" || unit === "secs") ms = Math.round(n * 1e3);
|
|
11118
|
+
else if (unit === "m" || unit === "min" || unit === "mins") ms = Math.round(n * 6e4);
|
|
11119
|
+
else if (unit === "h" || unit === "hr" || unit === "hrs") ms = Math.round(n * 60 * 6e4);
|
|
11120
|
+
else return null;
|
|
11121
|
+
if (ms < MIN_LOOP_INTERVAL_MS) return null;
|
|
11122
|
+
if (ms > MAX_LOOP_INTERVAL_MS) return null;
|
|
11123
|
+
return { ms };
|
|
11124
|
+
}
|
|
11125
|
+
function parseLoopCommand(args) {
|
|
11126
|
+
if (args.length === 0) return { kind: "status" };
|
|
11127
|
+
const first = (args[0] ?? "").toLowerCase();
|
|
11128
|
+
if (args.length === 1 && (first === "stop" || first === "off" || first === "cancel")) {
|
|
11129
|
+
return { kind: "stop" };
|
|
11130
|
+
}
|
|
11131
|
+
const interval = parseLoopInterval(args[0] ?? "");
|
|
11132
|
+
if (!interval) {
|
|
11133
|
+
return {
|
|
11134
|
+
kind: "error",
|
|
11135
|
+
message: "usage: /loop <interval> <prompt> (interval = 5s..6h, e.g. 30s, 5m, 1h)\n /loop stop (cancel an active loop)\n /loop (show active-loop status)"
|
|
11136
|
+
};
|
|
11137
|
+
}
|
|
11138
|
+
const prompt = args.slice(1).join(" ").trim();
|
|
11139
|
+
if (!prompt) {
|
|
11140
|
+
return {
|
|
11141
|
+
kind: "error",
|
|
11142
|
+
message: `usage: /loop ${args[0]} <prompt> \u2014 interval is fine but the prompt is missing.`
|
|
11143
|
+
};
|
|
11144
|
+
}
|
|
11145
|
+
return { kind: "start", intervalMs: interval.ms, prompt };
|
|
11146
|
+
}
|
|
11147
|
+
function formatLoopStatus(prompt, nextFireMs, iter) {
|
|
11148
|
+
const preview = prompt.length > 36 ? `${prompt.slice(0, 33)}\u2026` : prompt;
|
|
11149
|
+
const when = nextFireMs <= 0 ? "firing now" : `next in ${formatDuration2(nextFireMs)}`;
|
|
11150
|
+
return `loop: \`${preview}\` \xB7 ${when} \xB7 iter ${iter}`;
|
|
11151
|
+
}
|
|
11152
|
+
function formatDuration2(ms) {
|
|
11153
|
+
if (ms < 1e3) return `${ms}ms`;
|
|
11154
|
+
const totalSec = Math.round(ms / 1e3);
|
|
11155
|
+
if (totalSec < 60) return `${totalSec}s`;
|
|
11156
|
+
const m = Math.floor(totalSec / 60);
|
|
11157
|
+
const s = totalSec % 60;
|
|
11158
|
+
if (m < 60) return s === 0 ? `${m}m` : `${m}m${s}s`;
|
|
11159
|
+
const h = Math.floor(m / 60);
|
|
11160
|
+
const mm = m % 60;
|
|
11161
|
+
return mm === 0 ? `${h}h` : `${h}h${mm}m`;
|
|
10966
11162
|
}
|
|
10967
11163
|
|
|
10968
11164
|
// src/cli/ui/mcp-browse.ts
|
|
@@ -11268,8 +11464,17 @@ var SLASH_COMMANDS = [
|
|
|
11268
11464
|
{ cmd: "sessions", summary: "list saved sessions (current marked with \u25B8)" },
|
|
11269
11465
|
{ cmd: "forget", summary: "delete the current session from disk" },
|
|
11270
11466
|
{ cmd: "setup", summary: "reminds you to exit and run `reasonix setup`" },
|
|
11467
|
+
{
|
|
11468
|
+
cmd: "semantic",
|
|
11469
|
+
summary: "show semantic_search status \u2014 built? Ollama installed? how to enable"
|
|
11470
|
+
},
|
|
11271
11471
|
{ cmd: "clear", summary: "clear visible scrollback only (log/context kept)" },
|
|
11272
11472
|
{ cmd: "new", summary: "start a fresh conversation (clear context + scrollback)" },
|
|
11473
|
+
{
|
|
11474
|
+
cmd: "loop",
|
|
11475
|
+
argsHint: "<5s..6h> <prompt> \xB7 stop \xB7 (no args = status)",
|
|
11476
|
+
summary: "auto-resubmit <prompt> every <interval> until you type something / Esc / /loop stop"
|
|
11477
|
+
},
|
|
11273
11478
|
{ cmd: "exit", summary: "quit the TUI" },
|
|
11274
11479
|
// Code-mode only
|
|
11275
11480
|
{
|
|
@@ -11284,6 +11489,11 @@ var SLASH_COMMANDS = [
|
|
|
11284
11489
|
summary: "drop pending edit blocks without writing (no arg \u2192 all; indices \u2192 that subset)",
|
|
11285
11490
|
contextual: "code"
|
|
11286
11491
|
},
|
|
11492
|
+
{
|
|
11493
|
+
cmd: "walk",
|
|
11494
|
+
summary: "step through pending edits one block at a time (git-add-p style: y/n per block, a apply rest, A flip AUTO)",
|
|
11495
|
+
contextual: "code"
|
|
11496
|
+
},
|
|
11287
11497
|
{ cmd: "undo", summary: "roll back the last applied edit batch", contextual: "code" },
|
|
11288
11498
|
{
|
|
11289
11499
|
cmd: "history",
|
|
@@ -11316,10 +11526,10 @@ var SLASH_COMMANDS = [
|
|
|
11316
11526
|
},
|
|
11317
11527
|
{
|
|
11318
11528
|
cmd: "mode",
|
|
11319
|
-
argsHint: "[review|auto]",
|
|
11320
|
-
summary: "edit-gate: review (queue
|
|
11529
|
+
argsHint: "[review|auto|yolo]",
|
|
11530
|
+
summary: "edit-gate: review (queue) \xB7 auto (apply+undo) \xB7 yolo (apply+auto-shell). Shift+Tab cycles.",
|
|
11321
11531
|
contextual: "code",
|
|
11322
|
-
argCompleter: ["review", "auto"]
|
|
11532
|
+
argCompleter: ["review", "auto", "yolo"]
|
|
11323
11533
|
},
|
|
11324
11534
|
{ cmd: "jobs", summary: "list background jobs started by run_background", contextual: "code" },
|
|
11325
11535
|
{
|
|
@@ -11380,12 +11590,12 @@ function statsCommand(opts) {
|
|
|
11380
11590
|
}
|
|
11381
11591
|
dashboard(opts);
|
|
11382
11592
|
}
|
|
11383
|
-
function transcriptSummary(
|
|
11384
|
-
if (!existsSync12(
|
|
11385
|
-
console.error(`no such transcript: ${
|
|
11593
|
+
function transcriptSummary(path5) {
|
|
11594
|
+
if (!existsSync12(path5)) {
|
|
11595
|
+
console.error(`no such transcript: ${path5}`);
|
|
11386
11596
|
process.exit(1);
|
|
11387
11597
|
}
|
|
11388
|
-
const lines = readFileSync15(
|
|
11598
|
+
const lines = readFileSync15(path5, "utf8").split(/\r?\n/).filter(Boolean);
|
|
11389
11599
|
let assistantTurns = 0;
|
|
11390
11600
|
let toolCalls = 0;
|
|
11391
11601
|
let lastTurn = 0;
|
|
@@ -11398,25 +11608,25 @@ function transcriptSummary(path) {
|
|
|
11398
11608
|
} catch {
|
|
11399
11609
|
}
|
|
11400
11610
|
}
|
|
11401
|
-
console.log(`transcript: ${
|
|
11611
|
+
console.log(`transcript: ${path5}`);
|
|
11402
11612
|
console.log(`assistant turns: ${assistantTurns}`);
|
|
11403
11613
|
console.log(`tool invocations: ${toolCalls}`);
|
|
11404
11614
|
console.log(`last turn index: ${lastTurn}`);
|
|
11405
11615
|
}
|
|
11406
11616
|
function dashboard(opts) {
|
|
11407
|
-
const
|
|
11408
|
-
const records = readUsageLog(
|
|
11617
|
+
const path5 = opts.logPath ?? defaultUsageLogPath();
|
|
11618
|
+
const records = readUsageLog(path5);
|
|
11409
11619
|
if (records.length === 0) {
|
|
11410
11620
|
console.log("no usage data yet.");
|
|
11411
11621
|
console.log("");
|
|
11412
|
-
console.log(` ${
|
|
11622
|
+
console.log(` ${path5}`);
|
|
11413
11623
|
console.log("");
|
|
11414
11624
|
console.log("run `reasonix chat`, `reasonix code`, or `reasonix run <task>` \u2014 every turn");
|
|
11415
11625
|
console.log("appends one line to the log and `reasonix stats` will roll it up.");
|
|
11416
11626
|
return;
|
|
11417
11627
|
}
|
|
11418
11628
|
const agg = aggregateUsage(records, { now: opts.now });
|
|
11419
|
-
console.log(renderDashboard(agg,
|
|
11629
|
+
console.log(renderDashboard(agg, path5));
|
|
11420
11630
|
}
|
|
11421
11631
|
function renderDashboard(agg, logPath) {
|
|
11422
11632
|
const lines = [];
|
|
@@ -11500,7 +11710,7 @@ function pad(s, width, align = "left") {
|
|
|
11500
11710
|
}
|
|
11501
11711
|
|
|
11502
11712
|
// src/cli/ui/slash/handlers/admin.ts
|
|
11503
|
-
var hooks = (args,
|
|
11713
|
+
var hooks = (args, loop2, ctx) => {
|
|
11504
11714
|
const sub = (args[0] ?? "").toLowerCase();
|
|
11505
11715
|
if (sub === "reload") {
|
|
11506
11716
|
if (!ctx.reloadHooks) {
|
|
@@ -11516,7 +11726,7 @@ var hooks = (args, loop, ctx) => {
|
|
|
11516
11726
|
info: "usage: /hooks list active hooks\n /hooks reload re-read settings.json files"
|
|
11517
11727
|
};
|
|
11518
11728
|
}
|
|
11519
|
-
const all =
|
|
11729
|
+
const all = loop2.hooks;
|
|
11520
11730
|
const projPath = ctx.codeRoot ? projectSettingsPath(ctx.codeRoot) : void 0;
|
|
11521
11731
|
const globPath = globalSettingsPath();
|
|
11522
11732
|
if (all.length === 0) {
|
|
@@ -11588,14 +11798,14 @@ var update = (_args, _loop, ctx) => {
|
|
|
11588
11798
|
return { info: lines.join("\n") };
|
|
11589
11799
|
};
|
|
11590
11800
|
var stats = () => {
|
|
11591
|
-
const
|
|
11592
|
-
const records = readUsageLog(
|
|
11801
|
+
const path5 = defaultUsageLogPath();
|
|
11802
|
+
const records = readUsageLog(path5);
|
|
11593
11803
|
if (records.length === 0) {
|
|
11594
11804
|
return {
|
|
11595
11805
|
info: [
|
|
11596
11806
|
"no usage data yet.",
|
|
11597
11807
|
"",
|
|
11598
|
-
` ${
|
|
11808
|
+
` ${path5}`,
|
|
11599
11809
|
"",
|
|
11600
11810
|
"every turn you run here appends one record \u2014 this session's turns",
|
|
11601
11811
|
"will show up in the dashboard once you send a message."
|
|
@@ -11603,7 +11813,7 @@ var stats = () => {
|
|
|
11603
11813
|
};
|
|
11604
11814
|
}
|
|
11605
11815
|
const agg = aggregateUsage(records);
|
|
11606
|
-
return { info: renderDashboard(agg,
|
|
11816
|
+
return { info: renderDashboard(agg, path5) };
|
|
11607
11817
|
};
|
|
11608
11818
|
var handlers = {
|
|
11609
11819
|
hook: hooks,
|
|
@@ -11618,8 +11828,8 @@ var clear = () => ({
|
|
|
11618
11828
|
clear: true,
|
|
11619
11829
|
info: "\u25B8 terminal cleared (viewport + scrollback). Context (message log) is intact \u2014 next turn still sees everything. Use /new to start fresh, or /forget to delete the session entirely."
|
|
11620
11830
|
});
|
|
11621
|
-
var resetLog = (_args,
|
|
11622
|
-
const { dropped } =
|
|
11831
|
+
var resetLog = (_args, loop2) => {
|
|
11832
|
+
const { dropped } = loop2.clearLog();
|
|
11623
11833
|
return {
|
|
11624
11834
|
clear: true,
|
|
11625
11835
|
info: `\u25B8 new conversation \u2014 dropped ${dropped} message(s) from context. Same session, fresh slate.`
|
|
@@ -11647,9 +11857,13 @@ var keys = () => ({
|
|
|
11647
11857
|
" /<name> slash command; Tab/Enter picks from the suggestion list",
|
|
11648
11858
|
" @<path> inline a file under [Referenced files] (code mode).",
|
|
11649
11859
|
" Trailing `@\u2026` opens a file picker; \u2191/\u2193 navigate, Tab/Enter pick.",
|
|
11860
|
+
" @https://... fetch the URL, strip HTML, inline under [Referenced URLs].",
|
|
11861
|
+
" Cached per session \u2014 same URL twice fetches once.",
|
|
11650
11862
|
" !<cmd> run <cmd> as shell in the sandbox root; output goes into context",
|
|
11651
11863
|
" so the model sees it next turn. No allowlist gate.",
|
|
11652
|
-
" #<note> append <note> to REASONIX.md
|
|
11864
|
+
" #<note> append <note> to <project>/REASONIX.md (committable, team-shared).",
|
|
11865
|
+
" #g <note> append <note> to ~/.reasonix/REASONIX.md (global, never committed).",
|
|
11866
|
+
" Both pin into the immutable prefix every future session.",
|
|
11653
11867
|
" Use `\\#literal` if you actually want a `#` heading sent to the model.",
|
|
11654
11868
|
"",
|
|
11655
11869
|
"Pickers (slash + @-mention):",
|
|
@@ -11691,13 +11905,14 @@ var help = () => ({
|
|
|
11691
11905
|
" /retry truncate & resend your last message (fresh sample from the model)",
|
|
11692
11906
|
" /apply [N|1,3|1-4] (code mode) commit pending edit blocks (no arg \u2192 all; index \u2192 subset)",
|
|
11693
11907
|
" /discard [N|1,3|1-4] (code mode) drop pending edits (no arg \u2192 all; index \u2192 subset)",
|
|
11908
|
+
" /walk (code mode) step through pending edits one block at a time (y/n per block, a apply rest, A flip AUTO)",
|
|
11694
11909
|
" /undo (code mode) roll back the latest non-undone edit batch",
|
|
11695
11910
|
" /history (code mode) list every edit batch this session",
|
|
11696
11911
|
" /show [id] (code mode) dump a stored edit diff (newest when id omitted)",
|
|
11697
11912
|
' /commit "msg" (code mode) git add -A && git commit -m "msg"',
|
|
11698
11913
|
" /plan [on|off] (code mode) toggle read-only plan mode; writes gated behind submit_plan + your approval",
|
|
11699
11914
|
" /apply-plan (code mode) force-approve pending/in-text plan (fallback)",
|
|
11700
|
-
" /mode [review|auto]
|
|
11915
|
+
" /mode [review|auto|yolo] (code mode) review = queue \xB7 auto = apply+undo banner \xB7 yolo = apply+auto-shell. Shift+Tab cycles all three.",
|
|
11701
11916
|
" /jobs (code mode) list background processes (run_background) \u2014 running and exited",
|
|
11702
11917
|
" /kill <id> (code mode) stop a background job by id (SIGTERM \u2192 SIGKILL)",
|
|
11703
11918
|
" /logs <id> [lines] (code mode) tail a background job's output (default 80 lines)",
|
|
@@ -11705,6 +11920,7 @@ var help = () => ({
|
|
|
11705
11920
|
" /forget delete the current session from disk",
|
|
11706
11921
|
" /new start fresh: drop all context + clear scrollback",
|
|
11707
11922
|
" /clear clear displayed scrollback only (context kept \u2014 model still sees it)",
|
|
11923
|
+
" /loop <interval> <prompt> auto-resubmit <prompt> every <interval> (5s..6h). /loop stop \xB7 type anything to cancel.",
|
|
11708
11924
|
" /exit quit",
|
|
11709
11925
|
"",
|
|
11710
11926
|
"Shell shortcut:",
|
|
@@ -11714,15 +11930,22 @@ var help = () => ({
|
|
|
11714
11930
|
" Example: !git status !ls src/ !npm test",
|
|
11715
11931
|
"",
|
|
11716
11932
|
"Quick memory:",
|
|
11717
|
-
" #<note> append <note> to REASONIX.md (
|
|
11718
|
-
"
|
|
11719
|
-
"
|
|
11933
|
+
" #<note> append <note> to <project>/REASONIX.md (committable).",
|
|
11934
|
+
" Example: #findByEmail must be case-insensitive",
|
|
11935
|
+
" #g <note> append <note> to ~/.reasonix/REASONIX.md (global, never committed).",
|
|
11936
|
+
" Example: #g always run pnpm not npm",
|
|
11937
|
+
" Both pin into every future session's prefix. Faster than /memory.",
|
|
11720
11938
|
" Use `\\#text` to send a literal `#text` to the model.",
|
|
11721
11939
|
"",
|
|
11722
11940
|
"File references (code mode):",
|
|
11723
11941
|
" @path/to/file inline file content under [Referenced files] on send.",
|
|
11724
11942
|
" Type `@` to open the picker (\u2191\u2193 navigate, Tab/Enter pick).",
|
|
11725
11943
|
"",
|
|
11944
|
+
"URL references:",
|
|
11945
|
+
" @https://example.com fetch the URL, strip HTML, inline under [Referenced URLs].",
|
|
11946
|
+
" Same URL twice in one session fetches once (in-mem cache).",
|
|
11947
|
+
" Trailing sentence punctuation (./,/)) is stripped automatically.",
|
|
11948
|
+
"",
|
|
11726
11949
|
"Presets (branch + harvest are NEVER auto-enabled \u2014 opt-in only):",
|
|
11727
11950
|
" fast v4-flash \xB7 effort=high cheapest \xB7 quick Q&A, one-line edits",
|
|
11728
11951
|
" smart v4-flash \xB7 effort=max \u2190 default \xB7 day-to-day coding",
|
|
@@ -11742,8 +11965,8 @@ var help = () => ({
|
|
|
11742
11965
|
var setup = () => ({
|
|
11743
11966
|
info: "To reconfigure (preset, MCP servers, API key), exit this chat and run `reasonix setup`. Changes take effect on next launch."
|
|
11744
11967
|
});
|
|
11745
|
-
var retry = (_args,
|
|
11746
|
-
const prev =
|
|
11968
|
+
var retry = (_args, loop2) => {
|
|
11969
|
+
const prev = loop2.retryLastUser();
|
|
11747
11970
|
if (!prev) {
|
|
11748
11971
|
return {
|
|
11749
11972
|
info: "nothing to retry \u2014 no prior user message in this session's log."
|
|
@@ -11755,6 +11978,37 @@ var retry = (_args, loop) => {
|
|
|
11755
11978
|
resubmit: prev
|
|
11756
11979
|
};
|
|
11757
11980
|
};
|
|
11981
|
+
var loop = (args, _loop, ctx) => {
|
|
11982
|
+
if (!ctx.startLoop || !ctx.stopLoop || !ctx.getLoopStatus) {
|
|
11983
|
+
return {
|
|
11984
|
+
info: "/loop is only available in the interactive TUI (not in run/replay)."
|
|
11985
|
+
};
|
|
11986
|
+
}
|
|
11987
|
+
const cmd = parseLoopCommand(args);
|
|
11988
|
+
if (cmd.kind === "error") return { info: cmd.message };
|
|
11989
|
+
if (cmd.kind === "stop") {
|
|
11990
|
+
const wasActive = ctx.getLoopStatus() !== null;
|
|
11991
|
+
ctx.stopLoop();
|
|
11992
|
+
return {
|
|
11993
|
+
info: wasActive ? "\u25B8 loop stopped." : "no active loop to stop."
|
|
11994
|
+
};
|
|
11995
|
+
}
|
|
11996
|
+
if (cmd.kind === "status") {
|
|
11997
|
+
const status2 = ctx.getLoopStatus();
|
|
11998
|
+
if (!status2) {
|
|
11999
|
+
return {
|
|
12000
|
+
info: "no active loop. Start one with `/loop <interval> <prompt>` (e.g. /loop 30s npm test).\nCancels on: /loop stop \xB7 Esc \xB7 /clear \xB7 /new \xB7 any user-typed prompt."
|
|
12001
|
+
};
|
|
12002
|
+
}
|
|
12003
|
+
return { info: `\u25B8 ${formatLoopStatus(status2.prompt, status2.nextFireMs, status2.iter)}` };
|
|
12004
|
+
}
|
|
12005
|
+
ctx.startLoop(cmd.intervalMs, cmd.prompt);
|
|
12006
|
+
return {
|
|
12007
|
+
info: `\u25B8 loop started \u2014 re-submitting "${cmd.prompt}" every ${formatDuration2(
|
|
12008
|
+
cmd.intervalMs
|
|
12009
|
+
)}. Type anything (or /loop stop) to cancel.`
|
|
12010
|
+
};
|
|
12011
|
+
};
|
|
11758
12012
|
var handlers2 = {
|
|
11759
12013
|
exit,
|
|
11760
12014
|
quit: exit,
|
|
@@ -11765,7 +12019,8 @@ var handlers2 = {
|
|
|
11765
12019
|
help,
|
|
11766
12020
|
"?": help,
|
|
11767
12021
|
setup,
|
|
11768
|
-
retry
|
|
12022
|
+
retry,
|
|
12023
|
+
loop
|
|
11769
12024
|
};
|
|
11770
12025
|
|
|
11771
12026
|
// src/cli/ui/slash/helpers.ts
|
|
@@ -11860,8 +12115,8 @@ ${gitTail(commit2)}` };
|
|
|
11860
12115
|
}
|
|
11861
12116
|
function gitTail(res) {
|
|
11862
12117
|
const stderr = res.stderr ?? "";
|
|
11863
|
-
const
|
|
11864
|
-
const body = stderr.trim() ||
|
|
12118
|
+
const stdout3 = res.stdout ?? "";
|
|
12119
|
+
const body = stderr.trim() || stdout3.trim();
|
|
11865
12120
|
if (body) return body;
|
|
11866
12121
|
if (res.error) return res.error.message;
|
|
11867
12122
|
return "(no output from git)";
|
|
@@ -11961,15 +12216,17 @@ var mode = (args, _loop, ctx) => {
|
|
|
11961
12216
|
let target;
|
|
11962
12217
|
if (raw === "review") target = "review";
|
|
11963
12218
|
else if (raw === "auto") target = "auto";
|
|
12219
|
+
else if (raw === "yolo") target = "yolo";
|
|
11964
12220
|
else if (raw === "") {
|
|
11965
|
-
target = current === "
|
|
12221
|
+
target = current === "review" ? "auto" : current === "auto" ? "yolo" : "review";
|
|
11966
12222
|
} else {
|
|
11967
|
-
return {
|
|
12223
|
+
return {
|
|
12224
|
+
info: "usage: /mode <review|auto|yolo> (Shift+Tab also cycles)"
|
|
12225
|
+
};
|
|
11968
12226
|
}
|
|
11969
12227
|
ctx.setEditMode(target);
|
|
11970
|
-
|
|
11971
|
-
|
|
11972
|
-
};
|
|
12228
|
+
const banner = target === "yolo" ? "\u25B8 edit mode: YOLO \u2014 edits AND shell commands auto-run with no prompt. /undo still rolls back edits. Use carefully." : target === "auto" ? "\u25B8 edit mode: AUTO \u2014 edits apply immediately; press u within 5s to undo, or /undo later. Shell commands still ask." : "\u25B8 edit mode: review \u2014 edits queue for /apply (or y) / /discard (or n)";
|
|
12229
|
+
return { info: banner };
|
|
11973
12230
|
};
|
|
11974
12231
|
var commit = (args, _loop, ctx) => {
|
|
11975
12232
|
if (!ctx.codeRoot) {
|
|
@@ -11986,6 +12243,14 @@ var commit = (args, _loop, ctx) => {
|
|
|
11986
12243
|
}
|
|
11987
12244
|
return runGitCommit(ctx.codeRoot, message);
|
|
11988
12245
|
};
|
|
12246
|
+
var walk2 = (_args, _loop, ctx) => {
|
|
12247
|
+
if (!ctx.startWalkthrough) {
|
|
12248
|
+
return {
|
|
12249
|
+
info: "/walk is only available inside `reasonix code`."
|
|
12250
|
+
};
|
|
12251
|
+
}
|
|
12252
|
+
return { info: ctx.startWalkthrough() };
|
|
12253
|
+
};
|
|
11989
12254
|
var handlers3 = {
|
|
11990
12255
|
undo,
|
|
11991
12256
|
history,
|
|
@@ -11996,7 +12261,8 @@ var handlers3 = {
|
|
|
11996
12261
|
"apply-plan": applyPlan,
|
|
11997
12262
|
applyplan: applyPlan,
|
|
11998
12263
|
mode,
|
|
11999
|
-
commit
|
|
12264
|
+
commit,
|
|
12265
|
+
walk: walk2
|
|
12000
12266
|
};
|
|
12001
12267
|
|
|
12002
12268
|
// src/cli/ui/slash/handlers/jobs.ts
|
|
@@ -12061,10 +12327,10 @@ var handlers4 = {
|
|
|
12061
12327
|
};
|
|
12062
12328
|
|
|
12063
12329
|
// src/cli/ui/slash/handlers/mcp.ts
|
|
12064
|
-
var mcp = (_args,
|
|
12330
|
+
var mcp = (_args, loop2, ctx) => {
|
|
12065
12331
|
const servers = ctx.mcpServers ?? [];
|
|
12066
12332
|
const specs = ctx.mcpSpecs ?? [];
|
|
12067
|
-
const toolSpecs =
|
|
12333
|
+
const toolSpecs = loop2.prefix.toolSpecs ?? [];
|
|
12068
12334
|
if (servers.length === 0 && specs.length === 0 && toolSpecs.length === 0) {
|
|
12069
12335
|
return {
|
|
12070
12336
|
info: 'no MCP servers attached. Run `reasonix setup` to pick some, or launch with --mcp "<spec>". `reasonix mcp list` shows the catalog.'
|
|
@@ -12109,7 +12375,7 @@ var mcp = (_args, loop, ctx) => {
|
|
|
12109
12375
|
}
|
|
12110
12376
|
if (toolSpecs.length > 0) {
|
|
12111
12377
|
lines.push(`Tools in registry (${toolSpecs.length}):`);
|
|
12112
|
-
for (const
|
|
12378
|
+
for (const t2 of toolSpecs) lines.push(` \xB7 ${t2.function.name}`);
|
|
12113
12379
|
}
|
|
12114
12380
|
lines.push("");
|
|
12115
12381
|
lines.push("To change this set, exit and run `reasonix setup`.");
|
|
@@ -12253,14 +12519,14 @@ var memory = (args, _loop, ctx) => {
|
|
|
12253
12519
|
var handlers6 = { memory };
|
|
12254
12520
|
|
|
12255
12521
|
// src/cli/ui/slash/handlers/model.ts
|
|
12256
|
-
var model = (args,
|
|
12522
|
+
var model = (args, loop2, ctx) => {
|
|
12257
12523
|
const id = args[0];
|
|
12258
12524
|
const known = ctx.models ?? null;
|
|
12259
12525
|
if (!id) {
|
|
12260
12526
|
const hint = known && known.length > 0 ? known.join(" | ") : "try deepseek-v4-flash or deepseek-v4-pro \u2014 run /models to fetch the live list";
|
|
12261
12527
|
return { info: `usage: /model <id> (${hint})` };
|
|
12262
12528
|
}
|
|
12263
|
-
|
|
12529
|
+
loop2.configure({ model: id });
|
|
12264
12530
|
if (known && known.length > 0 && !known.includes(id)) {
|
|
12265
12531
|
return {
|
|
12266
12532
|
info: `model \u2192 ${id} (\u26A0 not in the fetched catalog: ${known.join(", ")}. If this is wrong the next call will 400 \u2014 run /models to refresh.)`
|
|
@@ -12268,7 +12534,7 @@ var model = (args, loop, ctx) => {
|
|
|
12268
12534
|
}
|
|
12269
12535
|
return { info: `model \u2192 ${id}` };
|
|
12270
12536
|
};
|
|
12271
|
-
var models = (_args,
|
|
12537
|
+
var models = (_args, loop2, ctx) => {
|
|
12272
12538
|
const list = ctx.models ?? null;
|
|
12273
12539
|
if (list === null) {
|
|
12274
12540
|
ctx.refreshModels?.();
|
|
@@ -12281,7 +12547,7 @@ var models = (_args, loop, ctx) => {
|
|
|
12281
12547
|
info: "DeepSeek /models returned an empty list. Try /models again, or check your account status at api-docs.deepseek.com."
|
|
12282
12548
|
};
|
|
12283
12549
|
}
|
|
12284
|
-
const current =
|
|
12550
|
+
const current = loop2.model;
|
|
12285
12551
|
const lines = list.map((id) => id === current ? `\u25B8 ${id} (current)` : ` ${id}`);
|
|
12286
12552
|
return {
|
|
12287
12553
|
info: [
|
|
@@ -12293,18 +12559,18 @@ var models = (_args, loop, ctx) => {
|
|
|
12293
12559
|
].join("\n")
|
|
12294
12560
|
};
|
|
12295
12561
|
};
|
|
12296
|
-
var harvest2 = (args,
|
|
12562
|
+
var harvest2 = (args, loop2) => {
|
|
12297
12563
|
const arg = (args[0] ?? "").toLowerCase();
|
|
12298
|
-
const on = arg === "" ? !
|
|
12299
|
-
|
|
12300
|
-
if (
|
|
12564
|
+
const on = arg === "" ? !loop2.harvestEnabled : arg === "on" || arg === "true" || arg === "1";
|
|
12565
|
+
loop2.configure({ harvest: on });
|
|
12566
|
+
if (loop2.harvestEnabled) {
|
|
12301
12567
|
return {
|
|
12302
12568
|
info: "harvest \u2192 on (Pillar-2 plan-state extraction \xB7 +1 cheap flash call per turn \xB7 opt-in only; no preset turns it on)"
|
|
12303
12569
|
};
|
|
12304
12570
|
}
|
|
12305
12571
|
return { info: "harvest \u2192 off" };
|
|
12306
12572
|
};
|
|
12307
|
-
var preset = (args,
|
|
12573
|
+
var preset = (args, loop2) => {
|
|
12308
12574
|
const name = (args[0] ?? "").toLowerCase();
|
|
12309
12575
|
const applyAndPersist = (effort2) => {
|
|
12310
12576
|
try {
|
|
@@ -12313,7 +12579,7 @@ var preset = (args, loop) => {
|
|
|
12313
12579
|
}
|
|
12314
12580
|
};
|
|
12315
12581
|
if (name === "fast" || name === "default") {
|
|
12316
|
-
|
|
12582
|
+
loop2.configure({
|
|
12317
12583
|
model: "deepseek-v4-flash",
|
|
12318
12584
|
reasoningEffort: "high",
|
|
12319
12585
|
harvest: false,
|
|
@@ -12323,7 +12589,7 @@ var preset = (args, loop) => {
|
|
|
12323
12589
|
return { info: "preset \u2192 fast (v4-flash \xB7 effort=high \xB7 cheapest)" };
|
|
12324
12590
|
}
|
|
12325
12591
|
if (name === "smart") {
|
|
12326
|
-
|
|
12592
|
+
loop2.configure({
|
|
12327
12593
|
model: "deepseek-v4-flash",
|
|
12328
12594
|
reasoningEffort: "max",
|
|
12329
12595
|
harvest: false,
|
|
@@ -12333,7 +12599,7 @@ var preset = (args, loop) => {
|
|
|
12333
12599
|
return { info: "preset \u2192 smart (v4-flash \xB7 effort=max \xB7 default \xB7 ~1.5\xD7 fast)" };
|
|
12334
12600
|
}
|
|
12335
12601
|
if (name === "max" || name === "best") {
|
|
12336
|
-
|
|
12602
|
+
loop2.configure({
|
|
12337
12603
|
model: "deepseek-v4-pro",
|
|
12338
12604
|
reasoningEffort: "max",
|
|
12339
12605
|
harvest: false,
|
|
@@ -12346,10 +12612,10 @@ var preset = (args, loop) => {
|
|
|
12346
12612
|
}
|
|
12347
12613
|
return { info: "usage: /preset <fast|smart|max>" };
|
|
12348
12614
|
};
|
|
12349
|
-
var branch = (args,
|
|
12615
|
+
var branch = (args, loop2) => {
|
|
12350
12616
|
const raw = (args[0] ?? "").toLowerCase();
|
|
12351
12617
|
if (raw === "" || raw === "off" || raw === "0" || raw === "1") {
|
|
12352
|
-
|
|
12618
|
+
loop2.configure({ branch: 1 });
|
|
12353
12619
|
return { info: "branch \u2192 off" };
|
|
12354
12620
|
}
|
|
12355
12621
|
const n = Number.parseInt(raw, 10);
|
|
@@ -12359,36 +12625,36 @@ var branch = (args, loop) => {
|
|
|
12359
12625
|
if (n > 8) {
|
|
12360
12626
|
return { info: "branch budget capped at 8 to prevent runaway cost" };
|
|
12361
12627
|
}
|
|
12362
|
-
|
|
12628
|
+
loop2.configure({ branch: n });
|
|
12363
12629
|
return {
|
|
12364
12630
|
info: `branch \u2192 ${n} (runs ${n} parallel samples per turn \xB7 ${n}\xD7 per-turn cost \xB7 streaming disabled \xB7 manual only, no preset enables branching)`
|
|
12365
12631
|
};
|
|
12366
12632
|
};
|
|
12367
|
-
var effort = (args,
|
|
12633
|
+
var effort = (args, loop2) => {
|
|
12368
12634
|
const raw = (args[0] ?? "").toLowerCase();
|
|
12369
12635
|
if (raw === "") {
|
|
12370
12636
|
return {
|
|
12371
|
-
info: `reasoning_effort \u2192 ${
|
|
12637
|
+
info: `reasoning_effort \u2192 ${loop2.reasoningEffort} (use /effort high for cheaper/faster, /effort max for the agent-class default \xB7 persisted across relaunches)`
|
|
12372
12638
|
};
|
|
12373
12639
|
}
|
|
12374
12640
|
if (raw !== "high" && raw !== "max") {
|
|
12375
12641
|
return { info: "usage: /effort <high|max>" };
|
|
12376
12642
|
}
|
|
12377
|
-
|
|
12643
|
+
loop2.configure({ reasoningEffort: raw });
|
|
12378
12644
|
try {
|
|
12379
12645
|
saveReasoningEffort(raw);
|
|
12380
12646
|
} catch {
|
|
12381
12647
|
}
|
|
12382
12648
|
return { info: `reasoning_effort \u2192 ${raw} (persisted)` };
|
|
12383
12649
|
};
|
|
12384
|
-
var pro = (args,
|
|
12650
|
+
var pro = (args, loop2, ctx) => {
|
|
12385
12651
|
const arg = (args[0] ?? "").toLowerCase();
|
|
12386
12652
|
if (arg === "off" || arg === "cancel" || arg === "disarm") {
|
|
12387
|
-
if (!
|
|
12653
|
+
if (!loop2.proArmed) {
|
|
12388
12654
|
return { info: "nothing armed \u2014 /pro with no args will arm pro for your next turn" };
|
|
12389
12655
|
}
|
|
12390
12656
|
if (ctx.disarmPro) ctx.disarmPro();
|
|
12391
|
-
else
|
|
12657
|
+
else loop2.disarmPro();
|
|
12392
12658
|
return { info: "\u25B8 /pro disarmed \u2014 next turn falls back to the current preset" };
|
|
12393
12659
|
}
|
|
12394
12660
|
if (arg && arg !== "on" && arg !== "arm") {
|
|
@@ -12397,7 +12663,7 @@ var pro = (args, loop, ctx) => {
|
|
|
12397
12663
|
};
|
|
12398
12664
|
}
|
|
12399
12665
|
if (ctx.armPro) ctx.armPro();
|
|
12400
|
-
else
|
|
12666
|
+
else loop2.armProForNextTurn();
|
|
12401
12667
|
return {
|
|
12402
12668
|
info: `\u25B8 /pro armed \u2014 your NEXT message runs on ${ESCALATION_MODEL_ID} regardless of preset. Auto-disarms after one turn. Use /preset max for a persistent switch.`
|
|
12403
12669
|
};
|
|
@@ -12414,8 +12680,8 @@ var handlers7 = {
|
|
|
12414
12680
|
};
|
|
12415
12681
|
|
|
12416
12682
|
// src/cli/ui/slash/handlers/observability.ts
|
|
12417
|
-
var think = (_args,
|
|
12418
|
-
const raw =
|
|
12683
|
+
var think = (_args, loop2) => {
|
|
12684
|
+
const raw = loop2.scratch.reasoning;
|
|
12419
12685
|
if (!raw || !raw.trim()) {
|
|
12420
12686
|
return {
|
|
12421
12687
|
info: "no reasoning cached. `/think` shows the full thinking-mode thought for the most recent turn \u2014 only thinking-mode models (deepseek-v4-flash / -v4-pro / -reasoner) produce it, and only once the turn completes."
|
|
@@ -12457,10 +12723,10 @@ var tool = (args, _loop, ctx) => {
|
|
|
12457
12723
|
${entry.text}`
|
|
12458
12724
|
};
|
|
12459
12725
|
};
|
|
12460
|
-
var context = (_args,
|
|
12461
|
-
const systemTokens = countTokens(
|
|
12462
|
-
const toolsTokens = countTokens(JSON.stringify(
|
|
12463
|
-
const entries =
|
|
12726
|
+
var context = (_args, loop2) => {
|
|
12727
|
+
const systemTokens = countTokens(loop2.prefix.system);
|
|
12728
|
+
const toolsTokens = countTokens(JSON.stringify(loop2.prefix.toolSpecs));
|
|
12729
|
+
const entries = loop2.log.toMessages();
|
|
12464
12730
|
let userTokens = 0;
|
|
12465
12731
|
let assistantTokens = 0;
|
|
12466
12732
|
let toolResultTokens = 0;
|
|
@@ -12485,7 +12751,7 @@ var context = (_args, loop) => {
|
|
|
12485
12751
|
}
|
|
12486
12752
|
const logTokens = userTokens + assistantTokens + toolResultTokens + toolCallTokens;
|
|
12487
12753
|
const total = systemTokens + toolsTokens + logTokens;
|
|
12488
|
-
const ctxMax = DEEPSEEK_CONTEXT_TOKENS[
|
|
12754
|
+
const ctxMax = DEEPSEEK_CONTEXT_TOKENS[loop2.model] ?? DEFAULT_CONTEXT_TOKENS;
|
|
12489
12755
|
const pct2 = (n) => total > 0 ? `${Math.round(n / total * 100)}%`.padStart(4) : " 0%";
|
|
12490
12756
|
const row2 = (label, n, note = "") => ` ${label.padEnd(20)}${compactNum(n).padStart(8)} tokens ${pct2(n)}${note ? ` ${note}` : ""}`;
|
|
12491
12757
|
const lines = [
|
|
@@ -12494,7 +12760,7 @@ var context = (_args, loop) => {
|
|
|
12494
12760
|
)}% of window)`,
|
|
12495
12761
|
"",
|
|
12496
12762
|
row2("system prompt", systemTokens),
|
|
12497
|
-
row2("tool specs", toolsTokens, `(${
|
|
12763
|
+
row2("tool specs", toolsTokens, `(${loop2.prefix.toolSpecs.length} tools)`),
|
|
12498
12764
|
row2("log (all turns)", logTokens, `(${entries.length} messages)`),
|
|
12499
12765
|
` user ${compactNum(userTokens).padStart(8)} tokens`,
|
|
12500
12766
|
` assistant ${compactNum(assistantTokens).padStart(8)} tokens`,
|
|
@@ -12505,9 +12771,9 @@ var context = (_args, loop) => {
|
|
|
12505
12771
|
const top = [...toolBreakdown].sort((a, b) => b.tokens - a.tokens).slice(0, 5);
|
|
12506
12772
|
lines.push("");
|
|
12507
12773
|
lines.push(`Top tool results by cost (of ${toolBreakdown.length} total):`);
|
|
12508
|
-
for (const
|
|
12774
|
+
for (const t2 of top) {
|
|
12509
12775
|
lines.push(
|
|
12510
|
-
` turn ${String(
|
|
12776
|
+
` turn ${String(t2.turn).padStart(3)} ${t2.name.padEnd(22)} ${compactNum(t2.tokens).padStart(8)} tokens`
|
|
12511
12777
|
);
|
|
12512
12778
|
}
|
|
12513
12779
|
}
|
|
@@ -12517,23 +12783,23 @@ var context = (_args, loop) => {
|
|
|
12517
12783
|
);
|
|
12518
12784
|
return { info: lines.join("\n") };
|
|
12519
12785
|
};
|
|
12520
|
-
var status = (_args,
|
|
12521
|
-
const branchBudget =
|
|
12522
|
-
const ctxMax = DEEPSEEK_CONTEXT_TOKENS[
|
|
12523
|
-
const lastPromptTokens =
|
|
12786
|
+
var status = (_args, loop2, ctx) => {
|
|
12787
|
+
const branchBudget = loop2.branchOptions.budget ?? 1;
|
|
12788
|
+
const ctxMax = DEEPSEEK_CONTEXT_TOKENS[loop2.model] ?? DEFAULT_CONTEXT_TOKENS;
|
|
12789
|
+
const lastPromptTokens = loop2.stats.summary().lastPromptTokens;
|
|
12524
12790
|
const ctxPct = ctxMax > 0 ? Math.round(lastPromptTokens / ctxMax * 100) : 0;
|
|
12525
12791
|
const ctxLine = lastPromptTokens > 0 ? ` ctx ${compactNum(lastPromptTokens)}/${compactNum(ctxMax)} (${ctxPct}%)` : " ctx no turns yet";
|
|
12526
12792
|
const pending = ctx.pendingEditCount ?? 0;
|
|
12527
|
-
const sessionLine =
|
|
12793
|
+
const sessionLine = loop2.sessionName ? ` session "${loop2.sessionName}" \xB7 ${loop2.log.length} messages in log (resumed ${loop2.resumedMessageCount})` : " session (ephemeral \u2014 no persistence)";
|
|
12528
12794
|
const mcpCount = ctx.mcpSpecs?.length ?? 0;
|
|
12529
|
-
const toolCount =
|
|
12795
|
+
const toolCount = loop2.prefix.toolSpecs.length;
|
|
12530
12796
|
const mcpLine = ` mcp ${mcpCount} server(s), ${toolCount} tool(s) in registry`;
|
|
12531
12797
|
const pendingLine = pending > 0 ? ` edits ${pending} pending (/apply to commit, /discard to drop)` : "";
|
|
12532
12798
|
const planLine = ctx.planMode ? " plan ON \u2014 writes gated (submit_plan + approval)" : "";
|
|
12533
|
-
const modeLine = ctx.editMode === "auto" ? " mode AUTO \u2014 edits apply immediately (u to undo within 5s \xB7 Shift+Tab to flip)" : ctx.editMode === "review" ? " mode review \u2014 edits queue for /apply or y (Shift+Tab to flip)" : "";
|
|
12799
|
+
const modeLine = ctx.editMode === "yolo" ? " mode YOLO \u2014 edits + shell auto-run with no prompt (/undo still rolls back \xB7 Shift+Tab to flip)" : ctx.editMode === "auto" ? " mode AUTO \u2014 edits apply immediately (u to undo within 5s \xB7 Shift+Tab to flip)" : ctx.editMode === "review" ? " mode review \u2014 edits queue for /apply or y (Shift+Tab to flip)" : "";
|
|
12534
12800
|
const lines = [
|
|
12535
|
-
` model ${
|
|
12536
|
-
` flags harvest=${
|
|
12801
|
+
` model ${loop2.model}`,
|
|
12802
|
+
` flags harvest=${loop2.harvestEnabled ? "on" : "off"} \xB7 branch=${branchBudget > 1 ? branchBudget : "off"} \xB7 stream=${loop2.stream ? "on" : "off"} \xB7 effort=${loop2.reasoningEffort}`,
|
|
12537
12803
|
ctxLine,
|
|
12538
12804
|
mcpLine,
|
|
12539
12805
|
sessionLine
|
|
@@ -12543,10 +12809,10 @@ var status = (_args, loop, ctx) => {
|
|
|
12543
12809
|
if (modeLine) lines.push(modeLine);
|
|
12544
12810
|
return { info: lines.join("\n") };
|
|
12545
12811
|
};
|
|
12546
|
-
var compact = (args,
|
|
12812
|
+
var compact = (args, loop2) => {
|
|
12547
12813
|
const tight = Number.parseInt(args[0] ?? "", 10);
|
|
12548
12814
|
const cap = Number.isFinite(tight) && tight >= 100 ? tight : 4e3;
|
|
12549
|
-
const { healedCount, tokensSaved, charsSaved } =
|
|
12815
|
+
const { healedCount, tokensSaved, charsSaved } = loop2.compact(cap);
|
|
12550
12816
|
if (healedCount === 0) {
|
|
12551
12817
|
return {
|
|
12552
12818
|
info: `\u25B8 nothing to compact \u2014 no tool result or tool-call args in history exceed ${cap.toLocaleString()} tokens.`
|
|
@@ -12567,8 +12833,8 @@ var handlers8 = {
|
|
|
12567
12833
|
|
|
12568
12834
|
// src/cli/ui/slash/handlers/plans.ts
|
|
12569
12835
|
import { basename } from "path";
|
|
12570
|
-
var plans = (_args,
|
|
12571
|
-
const sessionName =
|
|
12836
|
+
var plans = (_args, loop2) => {
|
|
12837
|
+
const sessionName = loop2.sessionName;
|
|
12572
12838
|
if (!sessionName) {
|
|
12573
12839
|
return {
|
|
12574
12840
|
info: "no session attached \u2014 `/plans` is per-session. Run `reasonix code` in a project to get a session."
|
|
@@ -12609,8 +12875,8 @@ var plans = (_args, loop) => {
|
|
|
12609
12875
|
}
|
|
12610
12876
|
return { info: lines.join("\n") };
|
|
12611
12877
|
};
|
|
12612
|
-
var replay = (args,
|
|
12613
|
-
const sessionName =
|
|
12878
|
+
var replay = (args, loop2) => {
|
|
12879
|
+
const sessionName = loop2.sessionName;
|
|
12614
12880
|
if (!sessionName) {
|
|
12615
12881
|
return {
|
|
12616
12882
|
info: "no session attached \u2014 `/replay` is per-session. Run `reasonix code` in a project to get a session."
|
|
@@ -12649,8 +12915,380 @@ var handlers9 = {
|
|
|
12649
12915
|
replay
|
|
12650
12916
|
};
|
|
12651
12917
|
|
|
12918
|
+
// src/cli/ui/slash/handlers/semantic.ts
|
|
12919
|
+
import { promises as fs2 } from "fs";
|
|
12920
|
+
import path from "path";
|
|
12921
|
+
|
|
12922
|
+
// src/index/semantic/embedding.ts
|
|
12923
|
+
var DEFAULT_OLLAMA_URL = "http://localhost:11434";
|
|
12924
|
+
var DEFAULT_EMBED_MODEL = "nomic-embed-text";
|
|
12925
|
+
var DEFAULT_TIMEOUT_MS = 3e4;
|
|
12926
|
+
var EmbeddingError = class extends Error {
|
|
12927
|
+
constructor(message, cause) {
|
|
12928
|
+
super(message);
|
|
12929
|
+
this.cause = cause;
|
|
12930
|
+
this.name = "EmbeddingError";
|
|
12931
|
+
}
|
|
12932
|
+
cause;
|
|
12933
|
+
};
|
|
12934
|
+
async function embed(text, opts = {}) {
|
|
12935
|
+
const baseUrl = opts.baseUrl ?? process.env.OLLAMA_URL ?? DEFAULT_OLLAMA_URL;
|
|
12936
|
+
const model2 = opts.model ?? process.env.REASONIX_EMBED_MODEL ?? DEFAULT_EMBED_MODEL;
|
|
12937
|
+
const timeoutMs = opts.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
12938
|
+
const controller = new AbortController();
|
|
12939
|
+
const onCallerAbort = () => controller.abort(opts.signal?.reason);
|
|
12940
|
+
if (opts.signal) {
|
|
12941
|
+
if (opts.signal.aborted) controller.abort(opts.signal.reason);
|
|
12942
|
+
else opts.signal.addEventListener("abort", onCallerAbort, { once: true });
|
|
12943
|
+
}
|
|
12944
|
+
const timer = setTimeout(() => controller.abort(new Error("embedding timeout")), timeoutMs);
|
|
12945
|
+
let res;
|
|
12946
|
+
try {
|
|
12947
|
+
res = await fetch(`${baseUrl}/api/embeddings`, {
|
|
12948
|
+
method: "POST",
|
|
12949
|
+
headers: { "content-type": "application/json" },
|
|
12950
|
+
body: JSON.stringify({ model: model2, prompt: text }),
|
|
12951
|
+
signal: controller.signal
|
|
12952
|
+
});
|
|
12953
|
+
} catch (err) {
|
|
12954
|
+
clearTimeout(timer);
|
|
12955
|
+
if (opts.signal) opts.signal.removeEventListener("abort", onCallerAbort);
|
|
12956
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
12957
|
+
if (/ECONNREFUSED|connect ECONNREFUSED|fetch failed/i.test(msg)) {
|
|
12958
|
+
throw new EmbeddingError(
|
|
12959
|
+
`Cannot reach Ollama at ${baseUrl}. Install from https://ollama.com, then run \`ollama pull ${model2}\` and \`ollama serve\`. Override the URL via OLLAMA_URL.`,
|
|
12960
|
+
err
|
|
12961
|
+
);
|
|
12962
|
+
}
|
|
12963
|
+
throw new EmbeddingError(`embedding request failed: ${msg}`, err);
|
|
12964
|
+
} finally {
|
|
12965
|
+
clearTimeout(timer);
|
|
12966
|
+
if (opts.signal) opts.signal.removeEventListener("abort", onCallerAbort);
|
|
12967
|
+
}
|
|
12968
|
+
if (!res.ok) {
|
|
12969
|
+
const body = await res.text().catch(() => "");
|
|
12970
|
+
if (res.status === 404 && /model.*not found/i.test(body)) {
|
|
12971
|
+
throw new EmbeddingError(
|
|
12972
|
+
`Embedding model "${model2}" not pulled. Run \`ollama pull ${model2}\` once, then retry.`
|
|
12973
|
+
);
|
|
12974
|
+
}
|
|
12975
|
+
throw new EmbeddingError(`Ollama returned ${res.status}: ${body.slice(0, 200)}`);
|
|
12976
|
+
}
|
|
12977
|
+
const json = await res.json();
|
|
12978
|
+
if (!json.embedding || !Array.isArray(json.embedding)) {
|
|
12979
|
+
throw new EmbeddingError(`Ollama response missing 'embedding' array`);
|
|
12980
|
+
}
|
|
12981
|
+
const out = new Float32Array(json.embedding.length);
|
|
12982
|
+
for (let i = 0; i < json.embedding.length; i++) {
|
|
12983
|
+
const v = json.embedding[i];
|
|
12984
|
+
if (typeof v !== "number" || !Number.isFinite(v)) {
|
|
12985
|
+
throw new EmbeddingError(`embedding[${i}] is not a finite number`);
|
|
12986
|
+
}
|
|
12987
|
+
out[i] = v;
|
|
12988
|
+
}
|
|
12989
|
+
return out;
|
|
12990
|
+
}
|
|
12991
|
+
async function embedAll(texts, opts = {}) {
|
|
12992
|
+
const out = new Array(texts.length).fill(null);
|
|
12993
|
+
for (let i = 0; i < texts.length; i++) {
|
|
12994
|
+
if (opts.signal?.aborted) {
|
|
12995
|
+
throw new EmbeddingError("embedding aborted");
|
|
12996
|
+
}
|
|
12997
|
+
const text = texts[i];
|
|
12998
|
+
if (text === void 0) continue;
|
|
12999
|
+
try {
|
|
13000
|
+
out[i] = await embed(text, opts);
|
|
13001
|
+
} catch (err) {
|
|
13002
|
+
opts.onError?.(i, err);
|
|
13003
|
+
}
|
|
13004
|
+
opts.onProgress?.(i + 1, texts.length);
|
|
13005
|
+
}
|
|
13006
|
+
return out;
|
|
13007
|
+
}
|
|
13008
|
+
async function probeOllama(opts = {}) {
|
|
13009
|
+
const baseUrl = opts.baseUrl ?? process.env.OLLAMA_URL ?? DEFAULT_OLLAMA_URL;
|
|
13010
|
+
try {
|
|
13011
|
+
const res = await fetch(`${baseUrl}/api/tags`, { signal: opts.signal });
|
|
13012
|
+
if (!res.ok) return { ok: false, error: `Ollama returned ${res.status}` };
|
|
13013
|
+
const json = await res.json();
|
|
13014
|
+
const models2 = (json.models ?? []).map((m) => m.name).filter((n) => typeof n === "string");
|
|
13015
|
+
return { ok: true, models: models2 };
|
|
13016
|
+
} catch (err) {
|
|
13017
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
13018
|
+
return { ok: false, error: msg };
|
|
13019
|
+
}
|
|
13020
|
+
}
|
|
13021
|
+
|
|
13022
|
+
// src/index/semantic/i18n.ts
|
|
13023
|
+
var cachedLocale = null;
|
|
13024
|
+
function detectLocale() {
|
|
13025
|
+
if (cachedLocale) return cachedLocale;
|
|
13026
|
+
const override = (process.env.REASONIX_LANG ?? "").toLowerCase();
|
|
13027
|
+
if (override === "zh" || override === "en") {
|
|
13028
|
+
cachedLocale = override;
|
|
13029
|
+
return cachedLocale;
|
|
13030
|
+
}
|
|
13031
|
+
const env = process.env.LANG ?? process.env.LC_ALL ?? process.env.LC_MESSAGES ?? "";
|
|
13032
|
+
if (/^zh[-_]/i.test(env)) {
|
|
13033
|
+
cachedLocale = "zh";
|
|
13034
|
+
return "zh";
|
|
13035
|
+
}
|
|
13036
|
+
try {
|
|
13037
|
+
const sys = new Intl.DateTimeFormat().resolvedOptions().locale ?? "";
|
|
13038
|
+
if (/^zh[-_]/i.test(sys)) {
|
|
13039
|
+
cachedLocale = "zh";
|
|
13040
|
+
return "zh";
|
|
13041
|
+
}
|
|
13042
|
+
} catch {
|
|
13043
|
+
}
|
|
13044
|
+
cachedLocale = "en";
|
|
13045
|
+
return "en";
|
|
13046
|
+
}
|
|
13047
|
+
function t(key, vars = {}) {
|
|
13048
|
+
const loc = detectLocale();
|
|
13049
|
+
const dict = loc === "zh" ? ZH : EN;
|
|
13050
|
+
const tpl = dict[key] ?? EN[key];
|
|
13051
|
+
return tpl.replace(/\{(\w+)\}/g, (_m, name) => {
|
|
13052
|
+
const v = vars[name];
|
|
13053
|
+
return v === void 0 ? `{${name}}` : String(v);
|
|
13054
|
+
});
|
|
13055
|
+
}
|
|
13056
|
+
var EN = {
|
|
13057
|
+
// ── preflight ─────────────────────────────────────────────────────
|
|
13058
|
+
ollamaNotFound: "\u2717 `ollama` not found on PATH.\n Install from https://ollama.com (one-time, ~150 MB), then retry.\n",
|
|
13059
|
+
daemonNotReachableHint: "\u2717 Ollama daemon not reachable. Run `ollama serve` and retry, or pass --yes to start it automatically.\n",
|
|
13060
|
+
daemonStartConfirm: "Ollama daemon isn't running. Start `ollama serve` now?",
|
|
13061
|
+
daemonAbortStart: "\u2717 aborted \u2014 start `ollama serve` yourself and retry.\n",
|
|
13062
|
+
daemonStarting: "\u25B8 starting `ollama serve`\u2026\n",
|
|
13063
|
+
daemonStartTimeout: "\u2717 daemon didn't come up within 15s. Try `ollama serve` in a separate terminal and retry.\n",
|
|
13064
|
+
daemonReady: "\u2713 daemon up{pid}\n",
|
|
13065
|
+
modelNotPulledHint: '\u2717 embedding model "{model}" not pulled. Run `ollama pull {model}` and retry, or pass --yes to pull it automatically.\n',
|
|
13066
|
+
modelPullConfirm: `Embedding model "{model}" isn't pulled yet. Pull it now? (~274 MB for nomic-embed-text)`,
|
|
13067
|
+
modelAbortPull: "\u2717 aborted \u2014 pull the model yourself and retry.\n",
|
|
13068
|
+
modelPulling: "\u25B8 pulling {model}\u2026\n",
|
|
13069
|
+
modelPullFailed: "\u2717 `ollama pull {model}` failed (exit {code}).\n",
|
|
13070
|
+
modelPulled: "\u2713 {model} pulled\n",
|
|
13071
|
+
// ── progress ─────────────────────────────────────────────────────
|
|
13072
|
+
// The TTY-mode progress writer paints `<spinner> <status> <elapsed>s`
|
|
13073
|
+
// every 120ms. The status itself comes from one of these keys based
|
|
13074
|
+
// on the current phase. {files}, {done}, {total}, {pct} are
|
|
13075
|
+
// substituted by the writer.
|
|
13076
|
+
progressStarting: "starting\u2026",
|
|
13077
|
+
progressScan: "scanning project \xB7 {files} files",
|
|
13078
|
+
progressEmbed: "embedding {done}/{total} chunks \xB7 {pct}%",
|
|
13079
|
+
progressEmbedHeartbeat: " {done}/{total}\n",
|
|
13080
|
+
progressScanLine: "scanning files\u2026\n",
|
|
13081
|
+
progressEmbedLine: "embedding {total} chunks across {files} files\u2026\n",
|
|
13082
|
+
// Final result line after a successful build.
|
|
13083
|
+
indexSuccess: "\u2713 indexed {scanned} files ({changed} changed, {added} new chunks, {removed} stale removed) in {seconds}s\n",
|
|
13084
|
+
indexSuccessWithSkips: "\u2713 indexed {scanned} files ({changed} changed, {added} new chunks, {removed} stale removed, {skipped} skipped due to embed errors) in {seconds}s\n",
|
|
13085
|
+
indexNothingToDo: " (nothing to do \u2014 re-run with --rebuild to force a full rebuild)\n",
|
|
13086
|
+
indexFailed: "\u2717 index failed: {msg}\n",
|
|
13087
|
+
// ── /semantic slash ──────────────────────────────────────────────
|
|
13088
|
+
slashHeader: "semantic_search status",
|
|
13089
|
+
slashEnabled: "\u2713 enabled \u2014 index built, tool registered.",
|
|
13090
|
+
slashEnabledDetail: " index size: {chunks} chunks across {files} files",
|
|
13091
|
+
slashEnabledHowto: " the model will call semantic_search automatically when it fits.",
|
|
13092
|
+
slashIndexMissing: "\u2717 no index built yet for this project.",
|
|
13093
|
+
slashHowToBuild: " to enable, exit Reasonix and run in your shell:\n reasonix index",
|
|
13094
|
+
slashOllamaMissing: " prerequisite: install Ollama from https://ollama.com",
|
|
13095
|
+
slashDaemonDown: " Ollama is installed but the daemon isn't running. start it with: ollama serve",
|
|
13096
|
+
slashIndexInfo: " what semantic_search does: cross-language code understanding via local embeddings.\n better than grep when you describe WHAT something does, not WHICH token to find."
|
|
13097
|
+
};
|
|
13098
|
+
var ZH = {
|
|
13099
|
+
ollamaNotFound: "\u2717 \u672A\u627E\u5230 `ollama`\u3002\n \u8BF7\u8BBF\u95EE https://ollama.com \u5B89\u88C5\uFF08\u4E00\u6B21\u6027\uFF0C\u7EA6 150 MB\uFF09\uFF0C\u7136\u540E\u91CD\u8BD5\u3002\n",
|
|
13100
|
+
daemonNotReachableHint: "\u2717 Ollama \u5B88\u62A4\u8FDB\u7A0B\u672A\u542F\u52A8\u3002\u8BF7\u8FD0\u884C `ollama serve` \u540E\u91CD\u8BD5\uFF0C\u6216\u52A0 --yes \u8BA9\u6211\u81EA\u52A8\u542F\u52A8\u3002\n",
|
|
13101
|
+
daemonStartConfirm: "Ollama \u5B88\u62A4\u8FDB\u7A0B\u672A\u8FD0\u884C\u3002\u73B0\u5728\u542F\u52A8 `ollama serve` \u5417\uFF1F",
|
|
13102
|
+
daemonAbortStart: "\u2717 \u5DF2\u53D6\u6D88\u2014\u2014\u8BF7\u81EA\u884C\u8FD0\u884C `ollama serve` \u540E\u91CD\u8BD5\u3002\n",
|
|
13103
|
+
daemonStarting: "\u25B8 \u6B63\u5728\u542F\u52A8 `ollama serve`\u2026\n",
|
|
13104
|
+
daemonStartTimeout: "\u2717 15 \u79D2\u5185\u5B88\u62A4\u8FDB\u7A0B\u672A\u5C31\u7EEA\u3002\u8BF7\u5728\u53E6\u4E00\u4E2A\u7EC8\u7AEF\u8FD0\u884C `ollama serve` \u540E\u91CD\u8BD5\u3002\n",
|
|
13105
|
+
daemonReady: "\u2713 \u5B88\u62A4\u8FDB\u7A0B\u5DF2\u542F\u52A8{pid}\n",
|
|
13106
|
+
modelNotPulledHint: '\u2717 \u5D4C\u5165\u6A21\u578B "{model}" \u672A\u4E0B\u8F7D\u3002\u8BF7\u8FD0\u884C `ollama pull {model}` \u540E\u91CD\u8BD5\uFF0C\u6216\u52A0 --yes \u8BA9\u6211\u81EA\u52A8\u4E0B\u8F7D\u3002\n',
|
|
13107
|
+
modelPullConfirm: '\u5D4C\u5165\u6A21\u578B "{model}" \u8FD8\u672A\u4E0B\u8F7D\u3002\u73B0\u5728\u4E0B\u8F7D\u5417\uFF1F\uFF08nomic-embed-text \u7EA6 274 MB\uFF09',
|
|
13108
|
+
modelAbortPull: "\u2717 \u5DF2\u53D6\u6D88\u2014\u2014\u8BF7\u81EA\u884C\u4E0B\u8F7D\u6A21\u578B\u540E\u91CD\u8BD5\u3002\n",
|
|
13109
|
+
modelPulling: "\u25B8 \u6B63\u5728\u4E0B\u8F7D {model}\u2026\n",
|
|
13110
|
+
modelPullFailed: "\u2717 `ollama pull {model}` \u5931\u8D25\uFF08\u9000\u51FA\u7801 {code}\uFF09\u3002\n",
|
|
13111
|
+
modelPulled: "\u2713 {model} \u4E0B\u8F7D\u5B8C\u6210\n",
|
|
13112
|
+
progressStarting: "\u6B63\u5728\u542F\u52A8\u2026",
|
|
13113
|
+
progressScan: "\u626B\u63CF\u9879\u76EE \xB7 \u5DF2\u626B\u63CF {files} \u4E2A\u6587\u4EF6",
|
|
13114
|
+
progressEmbed: "\u6B63\u5728\u5411\u91CF\u5316 {done}/{total} \u4E2A\u7247\u6BB5 \xB7 {pct}%",
|
|
13115
|
+
progressEmbedHeartbeat: " {done}/{total}\n",
|
|
13116
|
+
progressScanLine: "\u6B63\u5728\u626B\u63CF\u6587\u4EF6\u2026\n",
|
|
13117
|
+
progressEmbedLine: "\u6B63\u5728\u5411\u91CF\u5316 {total} \u4E2A\u7247\u6BB5\uFF08\u6D89\u53CA {files} \u4E2A\u6587\u4EF6\uFF09\u2026\n",
|
|
13118
|
+
indexSuccess: "\u2713 \u5DF2\u5EFA\u7ACB\u7D22\u5F15\uFF1A\u626B\u63CF {scanned} \u4E2A\u6587\u4EF6\uFF08{changed} \u4E2A\u6709\u53D8\u5316\uFF0C\u65B0\u589E {added} \u4E2A\u7247\u6BB5\uFF0C\u79FB\u9664 {removed} \u4E2A\u8FC7\u671F\uFF09\uFF1B\u8017\u65F6 {seconds}s\n",
|
|
13119
|
+
indexSuccessWithSkips: "\u2713 \u5DF2\u5EFA\u7ACB\u7D22\u5F15\uFF1A\u626B\u63CF {scanned} \u4E2A\u6587\u4EF6\uFF08{changed} \u4E2A\u6709\u53D8\u5316\uFF0C\u65B0\u589E {added} \u4E2A\u7247\u6BB5\uFF0C\u79FB\u9664 {removed} \u4E2A\u8FC7\u671F\uFF0C\u8DF3\u8FC7 {skipped} \u4E2A\u5D4C\u5165\u5931\u8D25\u7684\u7247\u6BB5\uFF09\uFF1B\u8017\u65F6 {seconds}s\n",
|
|
13120
|
+
indexNothingToDo: " \uFF08\u6CA1\u6709\u53D8\u5316\u2014\u2014\u52A0 --rebuild \u5F3A\u5236\u91CD\u5EFA\uFF09\n",
|
|
13121
|
+
indexFailed: "\u2717 \u5EFA\u7ACB\u7D22\u5F15\u5931\u8D25\uFF1A{msg}\n",
|
|
13122
|
+
slashHeader: "semantic_search \u72B6\u6001",
|
|
13123
|
+
slashEnabled: "\u2713 \u5DF2\u542F\u7528\u2014\u2014\u7D22\u5F15\u5DF2\u5EFA\u597D\uFF0C\u5DE5\u5177\u5DF2\u6CE8\u518C\u3002",
|
|
13124
|
+
slashEnabledDetail: " \u7D22\u5F15\u89C4\u6A21\uFF1A{chunks} \u4E2A\u7247\u6BB5\uFF0C{files} \u4E2A\u6587\u4EF6",
|
|
13125
|
+
slashEnabledHowto: " \u6A21\u578B\u5728\u5408\u9002\u7684\u65F6\u5019\u4F1A\u81EA\u52A8\u8C03\u7528 semantic_search\u3002",
|
|
13126
|
+
slashIndexMissing: "\u2717 \u5F53\u524D\u9879\u76EE\u8FD8\u6CA1\u6709\u7D22\u5F15\u3002",
|
|
13127
|
+
slashHowToBuild: " \u542F\u7528\u65B9\u5F0F\uFF1A\u9000\u51FA Reasonix\uFF0C\u5728\u7EC8\u7AEF\u8FD0\u884C\uFF1A\n reasonix index",
|
|
13128
|
+
slashOllamaMissing: " \u524D\u7F6E\u4F9D\u8D56\uFF1A\u4ECE https://ollama.com \u5B89\u88C5 Ollama",
|
|
13129
|
+
slashDaemonDown: " \u5DF2\u88C5 Ollama \u4F46\u5B88\u62A4\u8FDB\u7A0B\u672A\u542F\u52A8\uFF0C\u8BF7\u8FD0\u884C\uFF1Aollama serve",
|
|
13130
|
+
slashIndexInfo: ' semantic_search \u7528\u672C\u5730 embedding \u505A\u8DE8\u8BED\u8A00\u4EE3\u7801\u7406\u89E3\u3002\n \u5F53\u4F60\u63CF\u8FF0"\u505A\u4EC0\u4E48"\u800C\u4E0D\u662F\u5177\u4F53 token \u65F6\uFF0C\u6BD4 grep \u66F4\u597D\u3002'
|
|
13131
|
+
};
|
|
13132
|
+
|
|
13133
|
+
// src/index/semantic/ollama-launcher.ts
|
|
13134
|
+
import { spawn as spawn5, spawnSync as spawnSync2 } from "child_process";
|
|
13135
|
+
import { setTimeout as sleep2 } from "timers/promises";
|
|
13136
|
+
function findOllamaBinary() {
|
|
13137
|
+
const cmd = process.platform === "win32" ? "where" : "which";
|
|
13138
|
+
const out = spawnSync2(cmd, ["ollama"], { encoding: "utf8" });
|
|
13139
|
+
if (out.status !== 0) return null;
|
|
13140
|
+
const first = out.stdout.split(/\r?\n/).find((l) => l.trim().length > 0);
|
|
13141
|
+
return first ? first.trim() : null;
|
|
13142
|
+
}
|
|
13143
|
+
async function checkOllamaStatus(modelName, baseUrl) {
|
|
13144
|
+
const binary = findOllamaBinary();
|
|
13145
|
+
const probe = await probeOllama({ baseUrl });
|
|
13146
|
+
const installedModels = probe.ok ? probe.models : [];
|
|
13147
|
+
const wanted = modelName.includes(":") ? modelName : `${modelName}:latest`;
|
|
13148
|
+
const modelPulled = installedModels.some((m) => m === modelName || m === wanted);
|
|
13149
|
+
return {
|
|
13150
|
+
binaryFound: binary !== null,
|
|
13151
|
+
daemonRunning: probe.ok,
|
|
13152
|
+
modelPulled,
|
|
13153
|
+
modelName,
|
|
13154
|
+
installedModels
|
|
13155
|
+
};
|
|
13156
|
+
}
|
|
13157
|
+
async function startOllamaDaemon(opts = {}) {
|
|
13158
|
+
const timeoutMs = opts.timeoutMs ?? 15e3;
|
|
13159
|
+
const child = spawn5("ollama", ["serve"], {
|
|
13160
|
+
detached: true,
|
|
13161
|
+
stdio: "ignore",
|
|
13162
|
+
windowsHide: true
|
|
13163
|
+
});
|
|
13164
|
+
child.unref();
|
|
13165
|
+
const pid = child.pid ?? null;
|
|
13166
|
+
const start = Date.now();
|
|
13167
|
+
while (Date.now() - start < timeoutMs) {
|
|
13168
|
+
if (opts.signal?.aborted) return { ready: false, pid };
|
|
13169
|
+
const probe = await probeOllama({ baseUrl: opts.baseUrl, signal: opts.signal });
|
|
13170
|
+
if (probe.ok) return { ready: true, pid };
|
|
13171
|
+
await sleep2(500);
|
|
13172
|
+
}
|
|
13173
|
+
return { ready: false, pid };
|
|
13174
|
+
}
|
|
13175
|
+
async function pullOllamaModel(modelName, opts = {}) {
|
|
13176
|
+
return new Promise((resolve9) => {
|
|
13177
|
+
const child = spawn5("ollama", ["pull", modelName], {
|
|
13178
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
13179
|
+
windowsHide: true
|
|
13180
|
+
});
|
|
13181
|
+
if (opts.signal) {
|
|
13182
|
+
const onAbort = () => child.kill();
|
|
13183
|
+
opts.signal.addEventListener("abort", onAbort, { once: true });
|
|
13184
|
+
child.once("exit", () => opts.signal?.removeEventListener("abort", onAbort));
|
|
13185
|
+
}
|
|
13186
|
+
streamLines(child.stdout, (l) => opts.onLine?.(l, "stdout"));
|
|
13187
|
+
streamLines(child.stderr, (l) => opts.onLine?.(l, "stderr"));
|
|
13188
|
+
child.once("exit", (code) => resolve9(code ?? -1));
|
|
13189
|
+
child.once("error", () => resolve9(-1));
|
|
13190
|
+
});
|
|
13191
|
+
}
|
|
13192
|
+
function streamLines(stream, cb) {
|
|
13193
|
+
if (!stream) return;
|
|
13194
|
+
let buf = "";
|
|
13195
|
+
stream.setEncoding("utf8");
|
|
13196
|
+
stream.on("data", (chunk) => {
|
|
13197
|
+
buf += chunk;
|
|
13198
|
+
let nl = buf.indexOf("\n");
|
|
13199
|
+
while (nl !== -1) {
|
|
13200
|
+
const line = buf.slice(0, nl).replace(/\r$/, "");
|
|
13201
|
+
buf = buf.slice(nl + 1);
|
|
13202
|
+
if (line.length > 0) cb(line);
|
|
13203
|
+
nl = buf.indexOf("\n");
|
|
13204
|
+
}
|
|
13205
|
+
});
|
|
13206
|
+
stream.on("end", () => {
|
|
13207
|
+
if (buf.length > 0) cb(buf.replace(/\r$/, ""));
|
|
13208
|
+
});
|
|
13209
|
+
}
|
|
13210
|
+
|
|
13211
|
+
// src/cli/ui/slash/handlers/semantic.ts
|
|
13212
|
+
var semantic = (_args, _loop, ctx) => {
|
|
13213
|
+
const root = ctx.codeRoot;
|
|
13214
|
+
if (!root) {
|
|
13215
|
+
return {
|
|
13216
|
+
info: "/semantic is only available inside `reasonix code` (needs a project root)."
|
|
13217
|
+
};
|
|
13218
|
+
}
|
|
13219
|
+
void (async () => {
|
|
13220
|
+
const status2 = await renderSemanticStatus(root);
|
|
13221
|
+
ctx.postInfo?.(status2);
|
|
13222
|
+
})();
|
|
13223
|
+
return { info: "\u25B8 checking semantic_search status\u2026" };
|
|
13224
|
+
};
|
|
13225
|
+
async function renderSemanticStatus(rootDir) {
|
|
13226
|
+
const lines = [t("slashHeader"), ""];
|
|
13227
|
+
const indexExists2 = await indexFileExists(rootDir);
|
|
13228
|
+
if (indexExists2) {
|
|
13229
|
+
const meta = await readIndexMeta(rootDir);
|
|
13230
|
+
lines.push(t("slashEnabled"));
|
|
13231
|
+
if (meta) {
|
|
13232
|
+
lines.push(
|
|
13233
|
+
t("slashEnabledDetail", {
|
|
13234
|
+
chunks: meta.chunks,
|
|
13235
|
+
files: meta.files
|
|
13236
|
+
})
|
|
13237
|
+
);
|
|
13238
|
+
}
|
|
13239
|
+
lines.push(t("slashEnabledHowto"));
|
|
13240
|
+
return lines.join("\n");
|
|
13241
|
+
}
|
|
13242
|
+
lines.push(t("slashIndexMissing"));
|
|
13243
|
+
lines.push(t("slashIndexInfo"));
|
|
13244
|
+
lines.push("");
|
|
13245
|
+
if (findOllamaBinary() === null) {
|
|
13246
|
+
lines.push(t("slashOllamaMissing"));
|
|
13247
|
+
} else {
|
|
13248
|
+
const probe = await probeOllama();
|
|
13249
|
+
if (!probe.ok) lines.push(t("slashDaemonDown"));
|
|
13250
|
+
}
|
|
13251
|
+
lines.push(t("slashHowToBuild"));
|
|
13252
|
+
return lines.join("\n");
|
|
13253
|
+
}
|
|
13254
|
+
async function indexFileExists(rootDir) {
|
|
13255
|
+
try {
|
|
13256
|
+
await fs2.access(path.join(rootDir, ".reasonix", "semantic", "index.meta.json"));
|
|
13257
|
+
return true;
|
|
13258
|
+
} catch {
|
|
13259
|
+
return false;
|
|
13260
|
+
}
|
|
13261
|
+
}
|
|
13262
|
+
async function readIndexMeta(rootDir) {
|
|
13263
|
+
const dataPath = path.join(rootDir, ".reasonix", "semantic", "index.jsonl");
|
|
13264
|
+
try {
|
|
13265
|
+
const stat = await fs2.stat(dataPath);
|
|
13266
|
+
if (stat.size > 10 * 1024 * 1024) {
|
|
13267
|
+
return { chunks: Math.round(stat.size / 500), files: 0 };
|
|
13268
|
+
}
|
|
13269
|
+
const raw = await fs2.readFile(dataPath, "utf8");
|
|
13270
|
+
const seenPaths = /* @__PURE__ */ new Set();
|
|
13271
|
+
let chunks = 0;
|
|
13272
|
+
for (const line of raw.split("\n")) {
|
|
13273
|
+
if (line.length === 0) continue;
|
|
13274
|
+
chunks++;
|
|
13275
|
+
try {
|
|
13276
|
+
const parsed = JSON.parse(line);
|
|
13277
|
+
if (parsed.p) seenPaths.add(parsed.p);
|
|
13278
|
+
} catch {
|
|
13279
|
+
}
|
|
13280
|
+
}
|
|
13281
|
+
return { chunks, files: seenPaths.size };
|
|
13282
|
+
} catch {
|
|
13283
|
+
return null;
|
|
13284
|
+
}
|
|
13285
|
+
}
|
|
13286
|
+
var handlers10 = {
|
|
13287
|
+
semantic
|
|
13288
|
+
};
|
|
13289
|
+
|
|
12652
13290
|
// src/cli/ui/slash/handlers/sessions.ts
|
|
12653
|
-
var sessions = (_args,
|
|
13291
|
+
var sessions = (_args, loop2) => {
|
|
12654
13292
|
const items = listSessions();
|
|
12655
13293
|
if (items.length === 0) {
|
|
12656
13294
|
return {
|
|
@@ -12661,7 +13299,7 @@ var sessions = (_args, loop) => {
|
|
|
12661
13299
|
for (const s of items) {
|
|
12662
13300
|
const sizeKb = (s.size / 1024).toFixed(1);
|
|
12663
13301
|
const when = s.mtime.toISOString().replace("T", " ").slice(0, 16);
|
|
12664
|
-
const marker = s.name ===
|
|
13302
|
+
const marker = s.name === loop2.sessionName ? "\u25B8" : " ";
|
|
12665
13303
|
lines.push(
|
|
12666
13304
|
` ${marker} ${s.name.padEnd(22)} ${String(s.messageCount).padStart(5)} msgs ${sizeKb.padStart(7)} KB ${when}`
|
|
12667
13305
|
);
|
|
@@ -12670,17 +13308,17 @@ var sessions = (_args, loop) => {
|
|
|
12670
13308
|
lines.push("Resume with: reasonix chat --session <name>");
|
|
12671
13309
|
return { info: lines.join("\n") };
|
|
12672
13310
|
};
|
|
12673
|
-
var forget = (_args,
|
|
12674
|
-
if (!
|
|
13311
|
+
var forget = (_args, loop2) => {
|
|
13312
|
+
if (!loop2.sessionName) {
|
|
12675
13313
|
return { info: "not in a session \u2014 nothing to forget" };
|
|
12676
13314
|
}
|
|
12677
|
-
const name =
|
|
13315
|
+
const name = loop2.sessionName;
|
|
12678
13316
|
const ok = deleteSession(name);
|
|
12679
13317
|
return {
|
|
12680
13318
|
info: ok ? `\u25B8 deleted session "${name}" \u2014 current screen still shows the conversation, but next launch starts fresh` : `could not delete session "${name}" (already gone?)`
|
|
12681
13319
|
};
|
|
12682
13320
|
};
|
|
12683
|
-
var
|
|
13321
|
+
var handlers11 = {
|
|
12684
13322
|
sessions,
|
|
12685
13323
|
forget
|
|
12686
13324
|
};
|
|
@@ -12756,7 +13394,7 @@ ${found.body}${argsLine}`;
|
|
|
12756
13394
|
resubmit: payload
|
|
12757
13395
|
};
|
|
12758
13396
|
};
|
|
12759
|
-
var
|
|
13397
|
+
var handlers12 = {
|
|
12760
13398
|
skill,
|
|
12761
13399
|
skills: skill
|
|
12762
13400
|
};
|
|
@@ -12773,11 +13411,12 @@ var HANDLERS = {
|
|
|
12773
13411
|
...handlers8,
|
|
12774
13412
|
...handlers9,
|
|
12775
13413
|
...handlers10,
|
|
12776
|
-
...handlers11
|
|
13414
|
+
...handlers11,
|
|
13415
|
+
...handlers12
|
|
12777
13416
|
};
|
|
12778
|
-
function handleSlash(cmd, args,
|
|
13417
|
+
function handleSlash(cmd, args, loop2, ctx = {}) {
|
|
12779
13418
|
const h = HANDLERS[cmd];
|
|
12780
|
-
if (h) return h(args,
|
|
13419
|
+
if (h) return h(args, loop2, ctx);
|
|
12781
13420
|
return { unknown: true, info: `unknown command: /${cmd} (try /help)` };
|
|
12782
13421
|
}
|
|
12783
13422
|
|
|
@@ -13087,16 +13726,16 @@ function useEditHistory(codeMode) {
|
|
|
13087
13726
|
const status2 = entryStatus(entry);
|
|
13088
13727
|
const header2 = `\u25B8 edit #${entry.id} \xB7 ${when} \xB7 ${entry.source} \xB7 ${status2} \xB7 ${files.length} file(s)`;
|
|
13089
13728
|
const countLines3 = (s) => s.length === 0 ? 0 : (s.match(/\n/g)?.length ?? 0) + 1;
|
|
13090
|
-
const fileLines = files.map((
|
|
13091
|
-
const fileBlocks = entry.blocks.filter((b) => b.path ===
|
|
13729
|
+
const fileLines = files.map((path5) => {
|
|
13730
|
+
const fileBlocks = entry.blocks.filter((b) => b.path === path5);
|
|
13092
13731
|
let removed = 0;
|
|
13093
13732
|
let added = 0;
|
|
13094
13733
|
for (const b of fileBlocks) {
|
|
13095
13734
|
removed += countLines3(b.search);
|
|
13096
13735
|
added += countLines3(b.replace);
|
|
13097
13736
|
}
|
|
13098
|
-
const state = entry.undoneFiles.has(
|
|
13099
|
-
return ` ${state.padEnd(7)} -${String(removed).padStart(3)}/+${String(added).padStart(3)} ${
|
|
13737
|
+
const state = entry.undoneFiles.has(path5) ? "UNDONE" : "applied";
|
|
13738
|
+
return ` ${state.padEnd(7)} -${String(removed).padStart(3)}/+${String(added).padStart(3)} ${path5} (${fileBlocks.length} block${fileBlocks.length === 1 ? "" : "s"})`;
|
|
13100
13739
|
});
|
|
13101
13740
|
return [
|
|
13102
13741
|
header2,
|
|
@@ -13129,14 +13768,14 @@ function useEditHistory(codeMode) {
|
|
|
13129
13768
|
|
|
13130
13769
|
// src/cli/ui/useSessionInfo.ts
|
|
13131
13770
|
import { useCallback as useCallback3, useEffect as useEffect4, useState as useState8 } from "react";
|
|
13132
|
-
function useSessionInfo(
|
|
13771
|
+
function useSessionInfo(loop2) {
|
|
13133
13772
|
const [balance, setBalance] = useState8(null);
|
|
13134
13773
|
const [models2, setModels] = useState8(null);
|
|
13135
13774
|
const [latestVersion, setLatestVersion] = useState8(null);
|
|
13136
13775
|
useEffect4(() => {
|
|
13137
13776
|
let cancelled = false;
|
|
13138
13777
|
void (async () => {
|
|
13139
|
-
const bal = await
|
|
13778
|
+
const bal = await loop2.client.getBalance().catch(() => null);
|
|
13140
13779
|
if (cancelled || !bal || !bal.balance_infos.length) return;
|
|
13141
13780
|
const primary = bal.balance_infos[0];
|
|
13142
13781
|
setBalance({ currency: primary.currency, total: Number(primary.total_balance) });
|
|
@@ -13144,18 +13783,18 @@ function useSessionInfo(loop) {
|
|
|
13144
13783
|
return () => {
|
|
13145
13784
|
cancelled = true;
|
|
13146
13785
|
};
|
|
13147
|
-
}, [
|
|
13786
|
+
}, [loop2]);
|
|
13148
13787
|
useEffect4(() => {
|
|
13149
13788
|
let cancelled = false;
|
|
13150
13789
|
void (async () => {
|
|
13151
|
-
const list = await
|
|
13790
|
+
const list = await loop2.client.listModels().catch(() => null);
|
|
13152
13791
|
if (cancelled || !list) return;
|
|
13153
13792
|
setModels(list.data.map((m) => m.id));
|
|
13154
13793
|
})();
|
|
13155
13794
|
return () => {
|
|
13156
13795
|
cancelled = true;
|
|
13157
13796
|
};
|
|
13158
|
-
}, [
|
|
13797
|
+
}, [loop2]);
|
|
13159
13798
|
useEffect4(() => {
|
|
13160
13799
|
let cancelled = false;
|
|
13161
13800
|
void (async () => {
|
|
@@ -13170,19 +13809,19 @@ function useSessionInfo(loop) {
|
|
|
13170
13809
|
const updateAvailable = latestVersion && compareVersions(VERSION, latestVersion) < 0 ? latestVersion : null;
|
|
13171
13810
|
const refreshBalance = useCallback3(() => {
|
|
13172
13811
|
void (async () => {
|
|
13173
|
-
const bal = await
|
|
13812
|
+
const bal = await loop2.client.getBalance().catch(() => null);
|
|
13174
13813
|
if (bal?.balance_infos.length) {
|
|
13175
13814
|
const p = bal.balance_infos[0];
|
|
13176
13815
|
setBalance({ currency: p.currency, total: Number(p.total_balance) });
|
|
13177
13816
|
}
|
|
13178
13817
|
})();
|
|
13179
|
-
}, [
|
|
13818
|
+
}, [loop2]);
|
|
13180
13819
|
const refreshModels = useCallback3(() => {
|
|
13181
13820
|
void (async () => {
|
|
13182
|
-
const list = await
|
|
13821
|
+
const list = await loop2.client.listModels().catch(() => null);
|
|
13183
13822
|
if (list) setModels(list.data.map((m) => m.id));
|
|
13184
13823
|
})();
|
|
13185
|
-
}, [
|
|
13824
|
+
}, [loop2]);
|
|
13186
13825
|
const refreshLatestVersion = useCallback3(() => {
|
|
13187
13826
|
void (async () => {
|
|
13188
13827
|
const fresh = await getLatestVersion({ force: true });
|
|
@@ -13260,6 +13899,17 @@ function useSubagent({ session, setHistorical }) {
|
|
|
13260
13899
|
// src/cli/ui/App.tsx
|
|
13261
13900
|
var FLUSH_INTERVAL_MS = 100;
|
|
13262
13901
|
var PLAIN_UI = process.env.REASONIX_UI === "plain";
|
|
13902
|
+
function LoopStatusRow({
|
|
13903
|
+
loop: loop2
|
|
13904
|
+
}) {
|
|
13905
|
+
const [, setTick] = React23.useState(0);
|
|
13906
|
+
React23.useEffect(() => {
|
|
13907
|
+
const id = setInterval(() => setTick((t2) => t2 + 1), 1e3);
|
|
13908
|
+
return () => clearInterval(id);
|
|
13909
|
+
}, []);
|
|
13910
|
+
const nextFireMs = Math.max(0, loop2.nextFireAt - Date.now());
|
|
13911
|
+
return /* @__PURE__ */ React23.createElement(Box21, null, /* @__PURE__ */ React23.createElement(Text19, { color: "cyan" }, `\u25B8 ${formatLoopStatus(loop2.prompt, nextFireMs, loop2.iter)} \xB7 /loop stop or type to cancel`));
|
|
13912
|
+
}
|
|
13263
13913
|
function App({
|
|
13264
13914
|
model: model2,
|
|
13265
13915
|
system,
|
|
@@ -13279,21 +13929,24 @@ function App({
|
|
|
13279
13929
|
const [input, setInput] = useState10("");
|
|
13280
13930
|
const [busy, setBusy] = useState10(false);
|
|
13281
13931
|
const abortedThisTurn = useRef6(false);
|
|
13932
|
+
useEffect6(() => {
|
|
13933
|
+
busyRef.current = busy;
|
|
13934
|
+
}, [busy]);
|
|
13282
13935
|
const [ongoingTool, setOngoingTool] = useState10(null);
|
|
13283
13936
|
const [toolProgress, setToolProgress] = useState10(null);
|
|
13284
|
-
const { stdout:
|
|
13937
|
+
const { stdout: stdout3 } = useStdout8();
|
|
13285
13938
|
useEffect6(() => {
|
|
13286
|
-
if (!
|
|
13287
|
-
|
|
13288
|
-
|
|
13939
|
+
if (!stdout3 || !stdout3.isTTY) return;
|
|
13940
|
+
stdout3.write("\x1B[?2004h");
|
|
13941
|
+
stdout3.write("\x1B[>4;2m");
|
|
13289
13942
|
return () => {
|
|
13290
|
-
|
|
13291
|
-
|
|
13943
|
+
stdout3.write("\x1B[?2004l");
|
|
13944
|
+
stdout3.write("\x1B[>4m");
|
|
13292
13945
|
};
|
|
13293
|
-
}, [
|
|
13946
|
+
}, [stdout3]);
|
|
13294
13947
|
const [isResizing, setIsResizing] = useState10(false);
|
|
13295
13948
|
useEffect6(() => {
|
|
13296
|
-
if (!
|
|
13949
|
+
if (!stdout3 || !stdout3.isTTY) return;
|
|
13297
13950
|
let timer = null;
|
|
13298
13951
|
const onResize = () => {
|
|
13299
13952
|
setIsResizing(true);
|
|
@@ -13303,12 +13956,12 @@ function App({
|
|
|
13303
13956
|
timer = null;
|
|
13304
13957
|
}, 400);
|
|
13305
13958
|
};
|
|
13306
|
-
|
|
13959
|
+
stdout3.on("resize", onResize);
|
|
13307
13960
|
return () => {
|
|
13308
|
-
|
|
13961
|
+
stdout3.off("resize", onResize);
|
|
13309
13962
|
if (timer) clearTimeout(timer);
|
|
13310
13963
|
};
|
|
13311
|
-
}, [
|
|
13964
|
+
}, [stdout3]);
|
|
13312
13965
|
const { activity: subagentActivity, sinkRef: subagentSinkRef } = useSubagent({
|
|
13313
13966
|
session,
|
|
13314
13967
|
setHistorical
|
|
@@ -13332,6 +13985,7 @@ function App({
|
|
|
13332
13985
|
const [pendingCount, setPendingCount] = useState10(0);
|
|
13333
13986
|
const syncPendingCount = useCallback4(() => {
|
|
13334
13987
|
setPendingCount(pendingEdits.current.length);
|
|
13988
|
+
setPendingTick((t2) => t2 + 1);
|
|
13335
13989
|
}, []);
|
|
13336
13990
|
const [editMode, setEditMode] = useState10(() => codeMode ? loadEditMode() : "review");
|
|
13337
13991
|
const editModeRef = useRef6(editMode);
|
|
@@ -13340,6 +13994,8 @@ function App({
|
|
|
13340
13994
|
if (codeMode) saveEditMode(editMode);
|
|
13341
13995
|
}, [editMode, codeMode]);
|
|
13342
13996
|
const [pendingEditReview, setPendingEditReview] = useState10(null);
|
|
13997
|
+
const [walkthroughActive, setWalkthroughActive] = useState10(false);
|
|
13998
|
+
const [pendingTick, setPendingTick] = useState10(0);
|
|
13343
13999
|
const editReviewResolveRef = useRef6(null);
|
|
13344
14000
|
const turnEditPolicyRef = useRef6("ask");
|
|
13345
14001
|
const [modeFlash, setModeFlash] = useState10(false);
|
|
@@ -13370,6 +14026,16 @@ function App({
|
|
|
13370
14026
|
const promptHistory = useRef6([]);
|
|
13371
14027
|
const historyCursor = useRef6(-1);
|
|
13372
14028
|
const assistantIterCounter = useRef6(0);
|
|
14029
|
+
const atUrlCache = useRef6(/* @__PURE__ */ new Map());
|
|
14030
|
+
const [activeLoop, setActiveLoop] = useState10(null);
|
|
14031
|
+
const loopTimerRef = useRef6(null);
|
|
14032
|
+
const handleSubmitRef = useRef6(null);
|
|
14033
|
+
const busyRef = useRef6(false);
|
|
14034
|
+
const activeLoopRef = useRef6(activeLoop);
|
|
14035
|
+
const loopFiringRef = useRef6(false);
|
|
14036
|
+
useEffect6(() => {
|
|
14037
|
+
activeLoopRef.current = activeLoop;
|
|
14038
|
+
}, [activeLoop]);
|
|
13373
14039
|
const toolHistoryRef = useRef6([]);
|
|
13374
14040
|
const planStepsRef = useRef6(null);
|
|
13375
14041
|
const completedStepIdsRef = useRef6(/* @__PURE__ */ new Set());
|
|
@@ -13414,7 +14080,7 @@ function App({
|
|
|
13414
14080
|
};
|
|
13415
14081
|
}, []);
|
|
13416
14082
|
const loopRef = useRef6(null);
|
|
13417
|
-
const
|
|
14083
|
+
const loop2 = useMemo3(() => {
|
|
13418
14084
|
if (loopRef.current) return loopRef.current;
|
|
13419
14085
|
const client = new DeepSeekClient();
|
|
13420
14086
|
if (tools && !tools.has("run_skill")) {
|
|
@@ -13463,8 +14129,8 @@ function App({
|
|
|
13463
14129
|
return l;
|
|
13464
14130
|
}, [model2, system, harvest3, branch2, session, tools, codeMode]);
|
|
13465
14131
|
useEffect6(() => {
|
|
13466
|
-
|
|
13467
|
-
}, [
|
|
14132
|
+
loop2.hooks = hookList;
|
|
14133
|
+
}, [loop2, hookList]);
|
|
13468
14134
|
const {
|
|
13469
14135
|
balance,
|
|
13470
14136
|
models: models2,
|
|
@@ -13473,7 +14139,7 @@ function App({
|
|
|
13473
14139
|
refreshBalance,
|
|
13474
14140
|
refreshModels,
|
|
13475
14141
|
refreshLatestVersion
|
|
13476
|
-
} = useSessionInfo(
|
|
14142
|
+
} = useSessionInfo(loop2);
|
|
13477
14143
|
const {
|
|
13478
14144
|
slashMatches,
|
|
13479
14145
|
slashSelected,
|
|
@@ -13516,13 +14182,13 @@ function App({
|
|
|
13516
14182
|
text: "\u25B8 ephemeral chat (no session persistence) \u2014 drop --no-session to enable"
|
|
13517
14183
|
}
|
|
13518
14184
|
]);
|
|
13519
|
-
} else if (
|
|
14185
|
+
} else if (loop2.resumedMessageCount > 0) {
|
|
13520
14186
|
setHistorical((prev) => [
|
|
13521
14187
|
...prev,
|
|
13522
14188
|
{
|
|
13523
14189
|
id: `sys-resume-${Date.now()}`,
|
|
13524
14190
|
role: "info",
|
|
13525
|
-
text: `\u25B8 resumed session "${session}" with ${
|
|
14191
|
+
text: `\u25B8 resumed session "${session}" with ${loop2.resumedMessageCount} prior messages \xB7 /forget to start over \xB7 /sessions to list`
|
|
13526
14192
|
}
|
|
13527
14193
|
]);
|
|
13528
14194
|
} else {
|
|
@@ -13585,7 +14251,7 @@ function App({
|
|
|
13585
14251
|
]);
|
|
13586
14252
|
markEditModeHintShown();
|
|
13587
14253
|
}
|
|
13588
|
-
}, [session,
|
|
14254
|
+
}, [session, loop2, codeMode, syncPendingCount]);
|
|
13589
14255
|
const quitProcess = useCallback4(() => {
|
|
13590
14256
|
transcriptRef.current?.end();
|
|
13591
14257
|
process.exit(0);
|
|
@@ -13609,31 +14275,46 @@ function App({
|
|
|
13609
14275
|
if (key.escape && busy) {
|
|
13610
14276
|
if (abortedThisTurn.current) return;
|
|
13611
14277
|
abortedThisTurn.current = true;
|
|
13612
|
-
const
|
|
13613
|
-
if (
|
|
14278
|
+
const resolve9 = editReviewResolveRef.current;
|
|
14279
|
+
if (resolve9) {
|
|
13614
14280
|
editReviewResolveRef.current = null;
|
|
13615
14281
|
setPendingEditReview(null);
|
|
13616
|
-
|
|
14282
|
+
resolve9("reject");
|
|
13617
14283
|
}
|
|
13618
|
-
|
|
14284
|
+
if (activeLoopRef.current) stopLoop();
|
|
14285
|
+
loop2.abort();
|
|
14286
|
+
return;
|
|
14287
|
+
}
|
|
14288
|
+
if (key.escape && !busy && activeLoopRef.current) {
|
|
14289
|
+
stopLoop();
|
|
14290
|
+
return;
|
|
14291
|
+
}
|
|
14292
|
+
if (key.escape && walkthroughActive) {
|
|
14293
|
+
setWalkthroughActive(false);
|
|
14294
|
+
const remaining = pendingEdits.current.length;
|
|
14295
|
+
setHistorical((prev) => [
|
|
14296
|
+
...prev,
|
|
14297
|
+
{
|
|
14298
|
+
id: `walk-esc-${Date.now()}`,
|
|
14299
|
+
role: "info",
|
|
14300
|
+
text: remaining > 0 ? `\u25B8 walk cancelled \u2014 ${remaining} block(s) still pending.` : "\u25B8 walk cancelled."
|
|
14301
|
+
}
|
|
14302
|
+
]);
|
|
13619
14303
|
return;
|
|
13620
14304
|
}
|
|
13621
|
-
if (codeMode && key.shift && key.tab && !pendingShell && !pendingPlan && !stagedInput && !pendingEditReview && !pendingCheckpoint && !stagedCheckpointRevise && !pendingChoice && !stagedChoiceCustom && !pendingRevision) {
|
|
14305
|
+
if (codeMode && key.shift && key.tab && !pendingShell && !pendingPlan && !stagedInput && !pendingEditReview && !walkthroughActive && !pendingCheckpoint && !stagedCheckpointRevise && !pendingChoice && !stagedChoiceCustom && !pendingRevision) {
|
|
13622
14306
|
setEditMode((m) => {
|
|
13623
|
-
const next = m === "
|
|
14307
|
+
const next = m === "review" ? "auto" : m === "auto" ? "yolo" : "review";
|
|
14308
|
+
const message = next === "yolo" ? "\u25B8 edit mode: YOLO \u2014 edits AND shell commands auto-run. /undo still rolls back edits. Use carefully." : next === "auto" ? "\u25B8 edit mode: AUTO \u2014 edits apply immediately; press u within 5s to undo. Shell commands still ask." : "\u25B8 edit mode: review \u2014 edits queue for /apply (or y) / /discard (or n)";
|
|
13624
14309
|
setHistorical((prev) => [
|
|
13625
14310
|
...prev,
|
|
13626
|
-
{
|
|
13627
|
-
id: `mode-${Date.now()}`,
|
|
13628
|
-
role: "info",
|
|
13629
|
-
text: next === "auto" ? "\u25B8 edit mode: AUTO \u2014 edits apply immediately; press u within 5s to undo" : "\u25B8 edit mode: review \u2014 edits queue for /apply (or y) / /discard (or n)"
|
|
13630
|
-
}
|
|
14311
|
+
{ id: `mode-${Date.now()}`, role: "info", text: message }
|
|
13631
14312
|
]);
|
|
13632
14313
|
return next;
|
|
13633
14314
|
});
|
|
13634
14315
|
return;
|
|
13635
14316
|
}
|
|
13636
|
-
if (codeMode && input.length === 0 && (chKey === "u" || chKey === "U") && !pendingShell && !pendingPlan && !stagedInput && !pendingEditReview && !pendingCheckpoint && !stagedCheckpointRevise && !pendingChoice && !stagedChoiceCustom && !pendingRevision && // Fire when EITHER the banner is up OR there's any non-undone
|
|
14317
|
+
if (codeMode && input.length === 0 && (chKey === "u" || chKey === "U") && !pendingShell && !pendingPlan && !stagedInput && !pendingEditReview && !walkthroughActive && !pendingCheckpoint && !stagedCheckpointRevise && !pendingChoice && !stagedChoiceCustom && !pendingRevision && // Fire when EITHER the banner is up OR there's any non-undone
|
|
13637
14318
|
// history entry — the keybind is useful long after the 5-second
|
|
13638
14319
|
// banner expires, which users rightly want.
|
|
13639
14320
|
(undoBanner || hasUndoable())) {
|
|
@@ -13734,7 +14415,7 @@ function App({
|
|
|
13734
14415
|
}
|
|
13735
14416
|
return formatEditResults(results);
|
|
13736
14417
|
};
|
|
13737
|
-
if (editModeRef.current === "auto") return applyNow();
|
|
14418
|
+
if (editModeRef.current === "auto" || editModeRef.current === "yolo") return applyNow();
|
|
13738
14419
|
if (turnEditPolicyRef.current === "apply-all") return applyNow();
|
|
13739
14420
|
const choice = await new Promise((resolveChoice) => {
|
|
13740
14421
|
editReviewResolveRef.current = resolveChoice;
|
|
@@ -13827,7 +14508,7 @@ function App({
|
|
|
13827
14508
|
},
|
|
13828
14509
|
[session, syncPendingCount]
|
|
13829
14510
|
);
|
|
13830
|
-
const prefixHash =
|
|
14511
|
+
const prefixHash = loop2.prefix.fingerprint;
|
|
13831
14512
|
const writeTranscript = useCallback4(
|
|
13832
14513
|
(ev) => {
|
|
13833
14514
|
const stream = transcriptRef.current;
|
|
@@ -13846,10 +14527,98 @@ function App({
|
|
|
13846
14527
|
const clearPendingPlan = useCallback4(() => {
|
|
13847
14528
|
setPendingPlan(null);
|
|
13848
14529
|
}, []);
|
|
14530
|
+
const stopLoop = useCallback4(() => {
|
|
14531
|
+
if (loopTimerRef.current) {
|
|
14532
|
+
clearTimeout(loopTimerRef.current);
|
|
14533
|
+
loopTimerRef.current = null;
|
|
14534
|
+
}
|
|
14535
|
+
setActiveLoop((cur) => {
|
|
14536
|
+
if (!cur) return cur;
|
|
14537
|
+
setHistorical((prev) => [
|
|
14538
|
+
...prev,
|
|
14539
|
+
{
|
|
14540
|
+
id: `loop-stop-${Date.now()}`,
|
|
14541
|
+
role: "info",
|
|
14542
|
+
text: `\u25B8 loop stopped (after ${cur.iter} iter${cur.iter === 1 ? "" : "s"}).`
|
|
14543
|
+
}
|
|
14544
|
+
]);
|
|
14545
|
+
return null;
|
|
14546
|
+
});
|
|
14547
|
+
}, []);
|
|
14548
|
+
const startLoop = useCallback4((intervalMs, prompt) => {
|
|
14549
|
+
if (loopTimerRef.current) {
|
|
14550
|
+
clearTimeout(loopTimerRef.current);
|
|
14551
|
+
loopTimerRef.current = null;
|
|
14552
|
+
}
|
|
14553
|
+
setActiveLoop({
|
|
14554
|
+
prompt,
|
|
14555
|
+
intervalMs,
|
|
14556
|
+
nextFireAt: Date.now() + intervalMs,
|
|
14557
|
+
iter: 0
|
|
14558
|
+
});
|
|
14559
|
+
}, []);
|
|
14560
|
+
const startWalkthrough = useCallback4(() => {
|
|
14561
|
+
if (!codeMode) {
|
|
14562
|
+
return "/walk is only available inside `reasonix code`.";
|
|
14563
|
+
}
|
|
14564
|
+
if (pendingEdits.current.length === 0) {
|
|
14565
|
+
return "nothing pending \u2014 nothing to walk through.";
|
|
14566
|
+
}
|
|
14567
|
+
setWalkthroughActive(true);
|
|
14568
|
+
return `\u25B8 walking ${pendingEdits.current.length} edit block(s) \u2014 y apply \xB7 n reject \xB7 a apply rest \xB7 A flip to AUTO \xB7 Esc cancels (keeps remaining queued).`;
|
|
14569
|
+
}, [codeMode]);
|
|
14570
|
+
const handleWalkChoice = useCallback4(
|
|
14571
|
+
(choice) => {
|
|
14572
|
+
if (choice === "apply") {
|
|
14573
|
+
const out = codeApply([1]);
|
|
14574
|
+
setHistorical((prev) => [...prev, { id: `walk-${Date.now()}`, role: "info", text: out }]);
|
|
14575
|
+
} else if (choice === "reject") {
|
|
14576
|
+
const out = codeDiscard([1]);
|
|
14577
|
+
setHistorical((prev) => [...prev, { id: `walk-${Date.now()}`, role: "info", text: out }]);
|
|
14578
|
+
} else if (choice === "apply-rest-of-turn") {
|
|
14579
|
+
const out = codeApply();
|
|
14580
|
+
setHistorical((prev) => [...prev, { id: `walk-${Date.now()}`, role: "info", text: out }]);
|
|
14581
|
+
setWalkthroughActive(false);
|
|
14582
|
+
return;
|
|
14583
|
+
} else if (choice === "flip-to-auto") {
|
|
14584
|
+
setEditMode("auto");
|
|
14585
|
+
saveEditMode("auto");
|
|
14586
|
+
const out = codeApply([1]);
|
|
14587
|
+
setHistorical((prev) => [
|
|
14588
|
+
...prev,
|
|
14589
|
+
{ id: `walk-${Date.now()}`, role: "info", text: out },
|
|
14590
|
+
{
|
|
14591
|
+
id: `walk-flip-${Date.now()}`,
|
|
14592
|
+
role: "info",
|
|
14593
|
+
text: "\u25B8 flipped to AUTO mode \u2014 future edits will apply immediately. Walk exited."
|
|
14594
|
+
}
|
|
14595
|
+
]);
|
|
14596
|
+
setWalkthroughActive(false);
|
|
14597
|
+
return;
|
|
14598
|
+
}
|
|
14599
|
+
if (pendingEdits.current.length === 0) setWalkthroughActive(false);
|
|
14600
|
+
},
|
|
14601
|
+
[codeApply, codeDiscard]
|
|
14602
|
+
);
|
|
14603
|
+
const getLoopStatus = useCallback4(() => {
|
|
14604
|
+
const cur = activeLoopRef.current;
|
|
14605
|
+
if (!cur) return null;
|
|
14606
|
+
return {
|
|
14607
|
+
prompt: cur.prompt,
|
|
14608
|
+
intervalMs: cur.intervalMs,
|
|
14609
|
+
iter: cur.iter,
|
|
14610
|
+
nextFireMs: Math.max(0, cur.nextFireAt - Date.now())
|
|
14611
|
+
};
|
|
14612
|
+
}, []);
|
|
13849
14613
|
const handleSubmit = useCallback4(
|
|
13850
14614
|
async (raw) => {
|
|
13851
14615
|
let text = raw.trim();
|
|
13852
|
-
if (!text
|
|
14616
|
+
if (!text) return;
|
|
14617
|
+
if (activeLoopRef.current && !loopFiringRef.current) {
|
|
14618
|
+
stopLoop();
|
|
14619
|
+
}
|
|
14620
|
+
loopFiringRef.current = false;
|
|
14621
|
+
if (busy) return;
|
|
13853
14622
|
if (atMatches && atMatches.length > 0 && atPicker) {
|
|
13854
14623
|
const sel = atMatches[atSelected] ?? atMatches[0];
|
|
13855
14624
|
if (sel) {
|
|
@@ -13882,18 +14651,20 @@ function App({
|
|
|
13882
14651
|
return;
|
|
13883
14652
|
}
|
|
13884
14653
|
const hashParse = detectHashMemory(text);
|
|
13885
|
-
if (hashParse?.kind === "memory") {
|
|
14654
|
+
if (hashParse?.kind === "memory" || hashParse?.kind === "memory-global") {
|
|
14655
|
+
const isGlobal = hashParse.kind === "memory-global";
|
|
13886
14656
|
const memRoot = codeMode?.rootDir ?? process.cwd();
|
|
13887
14657
|
promptHistory.current.push(text);
|
|
13888
14658
|
try {
|
|
13889
|
-
const result = appendProjectMemory(memRoot, hashParse.note);
|
|
14659
|
+
const result = isGlobal ? appendGlobalMemory(hashParse.note) : appendProjectMemory(memRoot, hashParse.note);
|
|
13890
14660
|
const verb = result.created ? "created" : "appended to";
|
|
14661
|
+
const scopeTag = isGlobal ? "global" : "project";
|
|
13891
14662
|
setHistorical((prev) => [
|
|
13892
14663
|
...prev,
|
|
13893
14664
|
{
|
|
13894
14665
|
id: `hash-${Date.now()}`,
|
|
13895
14666
|
role: "info",
|
|
13896
|
-
text: `\u25B8 noted \u2014 ${verb} ${result.path}`
|
|
14667
|
+
text: `\u25B8 noted (${scopeTag}) \u2014 ${verb} ${result.path}`
|
|
13897
14668
|
}
|
|
13898
14669
|
]);
|
|
13899
14670
|
} catch (err) {
|
|
@@ -13936,7 +14707,7 @@ function App({
|
|
|
13936
14707
|
...prev,
|
|
13937
14708
|
{ id: `bang-o-${Date.now()}`, role: "info", text: formatted }
|
|
13938
14709
|
]);
|
|
13939
|
-
|
|
14710
|
+
loop2.appendAndPersist({
|
|
13940
14711
|
role: "user",
|
|
13941
14712
|
content: formatBangUserMessage(bangCmd, formatted)
|
|
13942
14713
|
});
|
|
@@ -13968,7 +14739,7 @@ function App({
|
|
|
13968
14739
|
}
|
|
13969
14740
|
const slash = parseSlash(text);
|
|
13970
14741
|
if (slash) {
|
|
13971
|
-
const result = handleSlash(slash.cmd, slash.args,
|
|
14742
|
+
const result = handleSlash(slash.cmd, slash.args, loop2, {
|
|
13972
14743
|
mcpSpecs,
|
|
13973
14744
|
mcpServers,
|
|
13974
14745
|
codeUndo: codeMode ? codeUndo : void 0,
|
|
@@ -13986,13 +14757,17 @@ function App({
|
|
|
13986
14757
|
editMode: codeMode ? editMode : void 0,
|
|
13987
14758
|
setEditMode: codeMode ? setEditMode : void 0,
|
|
13988
14759
|
armPro: () => {
|
|
13989
|
-
|
|
14760
|
+
loop2.armProForNextTurn();
|
|
13990
14761
|
setProArmed(true);
|
|
13991
14762
|
},
|
|
13992
14763
|
disarmPro: () => {
|
|
13993
|
-
|
|
14764
|
+
loop2.disarmPro();
|
|
13994
14765
|
setProArmed(false);
|
|
13995
14766
|
},
|
|
14767
|
+
startLoop,
|
|
14768
|
+
stopLoop,
|
|
14769
|
+
getLoopStatus,
|
|
14770
|
+
startWalkthrough: codeMode ? startWalkthrough : void 0,
|
|
13996
14771
|
jobs: codeMode?.jobs,
|
|
13997
14772
|
postInfo: (text2) => setHistorical((prev) => [
|
|
13998
14773
|
...prev,
|
|
@@ -14009,12 +14784,13 @@ function App({
|
|
|
14009
14784
|
refreshModels
|
|
14010
14785
|
});
|
|
14011
14786
|
if (result.exit) {
|
|
14787
|
+
if (activeLoopRef.current) stopLoop();
|
|
14012
14788
|
transcriptRef.current?.end();
|
|
14013
14789
|
exit2();
|
|
14014
14790
|
return;
|
|
14015
14791
|
}
|
|
14016
14792
|
if (result.clear && result.info) {
|
|
14017
|
-
|
|
14793
|
+
stdout3?.write("\x1B[2J\x1B[3J\x1B[H");
|
|
14018
14794
|
setHistorical([
|
|
14019
14795
|
{
|
|
14020
14796
|
id: `sys-${Date.now()}`,
|
|
@@ -14027,16 +14803,18 @@ function App({
|
|
|
14027
14803
|
clearPendingEdits(session ?? null);
|
|
14028
14804
|
syncPendingCount();
|
|
14029
14805
|
}
|
|
14806
|
+
if (activeLoopRef.current) stopLoop();
|
|
14030
14807
|
return;
|
|
14031
14808
|
}
|
|
14032
14809
|
if (result.clear) {
|
|
14033
|
-
|
|
14810
|
+
stdout3?.write("\x1B[2J\x1B[3J\x1B[H");
|
|
14034
14811
|
setHistorical([]);
|
|
14035
14812
|
if (codeMode) {
|
|
14036
14813
|
pendingEdits.current = [];
|
|
14037
14814
|
clearPendingEdits(session ?? null);
|
|
14038
14815
|
syncPendingCount();
|
|
14039
14816
|
}
|
|
14817
|
+
if (activeLoopRef.current) stopLoop();
|
|
14040
14818
|
return;
|
|
14041
14819
|
}
|
|
14042
14820
|
if (result.info) {
|
|
@@ -14165,8 +14943,47 @@ function App({
|
|
|
14165
14943
|
}
|
|
14166
14944
|
}
|
|
14167
14945
|
}
|
|
14946
|
+
if (/(?:^|\s)@https?:\/\//.test(text)) {
|
|
14947
|
+
try {
|
|
14948
|
+
const urlExpanded = await expandAtUrls(modelInput, {
|
|
14949
|
+
fetcher: webFetch,
|
|
14950
|
+
cache: atUrlCache.current
|
|
14951
|
+
});
|
|
14952
|
+
if (urlExpanded.expansions.length > 0) {
|
|
14953
|
+
modelInput = urlExpanded.text;
|
|
14954
|
+
const inlined = urlExpanded.expansions.filter((ex) => ex.ok).map((ex) => {
|
|
14955
|
+
const tag = ex.title ? `${ex.title} (${ex.url})` : ex.url;
|
|
14956
|
+
const trunc = ex.truncated ? " \xB7 truncated" : "";
|
|
14957
|
+
return `${tag} \xB7 ${(ex.chars ?? 0).toLocaleString()} chars${trunc}`;
|
|
14958
|
+
});
|
|
14959
|
+
const skipped = urlExpanded.expansions.filter((ex) => !ex.ok).map((ex) => `${ex.url} (${ex.skip ?? "fetch-error"})`);
|
|
14960
|
+
const parts = [];
|
|
14961
|
+
if (inlined.length > 0) parts.push(`inlined ${inlined.join("; ")}`);
|
|
14962
|
+
if (skipped.length > 0) parts.push(`skipped ${skipped.join("; ")}`);
|
|
14963
|
+
if (parts.length > 0) {
|
|
14964
|
+
setHistorical((prev) => [
|
|
14965
|
+
...prev,
|
|
14966
|
+
{
|
|
14967
|
+
id: `aturl-${Date.now()}`,
|
|
14968
|
+
role: "info",
|
|
14969
|
+
text: `\u25B8 @url: ${parts.join("; ")}`
|
|
14970
|
+
}
|
|
14971
|
+
]);
|
|
14972
|
+
}
|
|
14973
|
+
}
|
|
14974
|
+
} catch (err) {
|
|
14975
|
+
setHistorical((prev) => [
|
|
14976
|
+
...prev,
|
|
14977
|
+
{
|
|
14978
|
+
id: `aturl-e-${Date.now()}`,
|
|
14979
|
+
role: "warning",
|
|
14980
|
+
text: `@url expansion failed: ${err.message}`
|
|
14981
|
+
}
|
|
14982
|
+
]);
|
|
14983
|
+
}
|
|
14984
|
+
}
|
|
14168
14985
|
try {
|
|
14169
|
-
for await (const ev of
|
|
14986
|
+
for await (const ev of loop2.step(modelInput)) {
|
|
14170
14987
|
writeTranscript(ev);
|
|
14171
14988
|
if (ev.role !== "status") {
|
|
14172
14989
|
setStatusLine((cur) => cur ? null : cur);
|
|
@@ -14206,7 +15023,7 @@ function App({
|
|
|
14206
15023
|
flush();
|
|
14207
15024
|
const repairNote = ev.repair ? describeRepair(ev.repair) : "";
|
|
14208
15025
|
setStreaming(null);
|
|
14209
|
-
setSummary(
|
|
15026
|
+
setSummary(loop2.stats.summary());
|
|
14210
15027
|
if (ev.stats?.usage) {
|
|
14211
15028
|
appendUsage({
|
|
14212
15029
|
session: session ?? null,
|
|
@@ -14240,7 +15057,7 @@ function App({
|
|
|
14240
15057
|
if (codeMode && finalText && !ev.forcedSummary) {
|
|
14241
15058
|
const blocks = parseEditBlocks(finalText);
|
|
14242
15059
|
if (blocks.length > 0) {
|
|
14243
|
-
if (editModeRef.current === "auto") {
|
|
15060
|
+
if (editModeRef.current === "auto" || editModeRef.current === "yolo") {
|
|
14244
15061
|
const snaps = snapshotBeforeEdits(blocks, codeMode.rootDir);
|
|
14245
15062
|
const results = applyEditBlocks(blocks, codeMode.rootDir);
|
|
14246
15063
|
const good = results.some(
|
|
@@ -14452,7 +15269,7 @@ function App({
|
|
|
14452
15269
|
event: "Stop",
|
|
14453
15270
|
cwd: hookCwd,
|
|
14454
15271
|
lastAssistantText: streamRef.text,
|
|
14455
|
-
turn:
|
|
15272
|
+
turn: loop2.stats.summary().turns
|
|
14456
15273
|
}
|
|
14457
15274
|
});
|
|
14458
15275
|
for (const o of stopReport.outcomes) {
|
|
@@ -14473,7 +15290,7 @@ function App({
|
|
|
14473
15290
|
setOngoingTool(null);
|
|
14474
15291
|
setToolProgress(null);
|
|
14475
15292
|
setStatusLine(null);
|
|
14476
|
-
setSummary(
|
|
15293
|
+
setSummary(loop2.stats.summary());
|
|
14477
15294
|
setBusy(false);
|
|
14478
15295
|
setTurnOnPro(false);
|
|
14479
15296
|
refreshBalance();
|
|
@@ -14491,7 +15308,7 @@ function App({
|
|
|
14491
15308
|
exit2,
|
|
14492
15309
|
hookCwd,
|
|
14493
15310
|
hookList,
|
|
14494
|
-
|
|
15311
|
+
loop2,
|
|
14495
15312
|
latestVersion,
|
|
14496
15313
|
mcpSpecs,
|
|
14497
15314
|
mcpServers,
|
|
@@ -14520,15 +15337,57 @@ function App({
|
|
|
14520
15337
|
refreshModels,
|
|
14521
15338
|
proArmed,
|
|
14522
15339
|
persistPlanState,
|
|
14523
|
-
|
|
15340
|
+
stdout3,
|
|
15341
|
+
stopLoop,
|
|
15342
|
+
startLoop,
|
|
15343
|
+
getLoopStatus,
|
|
15344
|
+
startWalkthrough
|
|
14524
15345
|
]
|
|
14525
15346
|
);
|
|
14526
|
-
|
|
14527
|
-
|
|
14528
|
-
|
|
14529
|
-
|
|
14530
|
-
|
|
14531
|
-
|
|
15347
|
+
useEffect6(() => {
|
|
15348
|
+
handleSubmitRef.current = handleSubmit;
|
|
15349
|
+
}, [handleSubmit]);
|
|
15350
|
+
useEffect6(() => {
|
|
15351
|
+
if (!activeLoop) return;
|
|
15352
|
+
const delay = Math.max(0, activeLoop.nextFireAt - Date.now());
|
|
15353
|
+
const timer = setTimeout(async () => {
|
|
15354
|
+
loopTimerRef.current = null;
|
|
15355
|
+
if (busyRef.current) {
|
|
15356
|
+
setActiveLoop((cur2) => cur2 ? { ...cur2, nextFireAt: Date.now() + 1e3 } : cur2);
|
|
15357
|
+
return;
|
|
15358
|
+
}
|
|
15359
|
+
const cur = activeLoopRef.current;
|
|
15360
|
+
if (!cur) return;
|
|
15361
|
+
const nextIter = cur.iter + 1;
|
|
15362
|
+
setActiveLoop(
|
|
15363
|
+
(c) => c ? { ...c, iter: nextIter, nextFireAt: Date.now() + cur.intervalMs } : c
|
|
15364
|
+
);
|
|
15365
|
+
setHistorical((prev) => [
|
|
15366
|
+
...prev,
|
|
15367
|
+
{
|
|
15368
|
+
id: `loop-fire-${Date.now()}`,
|
|
15369
|
+
role: "info",
|
|
15370
|
+
text: `\u25B8 /loop iter ${nextIter} \u2192 ${cur.prompt}`
|
|
15371
|
+
}
|
|
15372
|
+
]);
|
|
15373
|
+
loopFiringRef.current = true;
|
|
15374
|
+
try {
|
|
15375
|
+
await handleSubmitRef.current?.(cur.prompt);
|
|
15376
|
+
} catch {
|
|
15377
|
+
stopLoop();
|
|
15378
|
+
} finally {
|
|
15379
|
+
loopFiringRef.current = false;
|
|
15380
|
+
}
|
|
15381
|
+
}, delay);
|
|
15382
|
+
loopTimerRef.current = timer;
|
|
15383
|
+
return () => clearTimeout(timer);
|
|
15384
|
+
}, [activeLoop, stopLoop]);
|
|
15385
|
+
const handleShellConfirm = useCallback4(
|
|
15386
|
+
async (choice) => {
|
|
15387
|
+
const pending = pendingShell;
|
|
15388
|
+
if (!pending || !codeMode) return;
|
|
15389
|
+
const { command: cmd, kind } = pending;
|
|
15390
|
+
setPendingShell(null);
|
|
14532
15391
|
let synthetic;
|
|
14533
15392
|
if (choice === "deny") {
|
|
14534
15393
|
setHistorical((prev) => [
|
|
@@ -14612,13 +15471,13 @@ ${body}`;
|
|
|
14612
15471
|
}
|
|
14613
15472
|
}
|
|
14614
15473
|
if (busy) {
|
|
14615
|
-
|
|
15474
|
+
loop2.abort();
|
|
14616
15475
|
setQueuedSubmit(synthetic);
|
|
14617
15476
|
} else {
|
|
14618
15477
|
await handleSubmit(synthetic);
|
|
14619
15478
|
}
|
|
14620
15479
|
},
|
|
14621
|
-
[pendingShell, codeMode, handleSubmit, busy,
|
|
15480
|
+
[pendingShell, codeMode, handleSubmit, busy, loop2]
|
|
14622
15481
|
);
|
|
14623
15482
|
useEffect6(() => {
|
|
14624
15483
|
if (!busy && queuedSubmit !== null) {
|
|
@@ -14656,13 +15515,13 @@ ${body}`;
|
|
|
14656
15515
|
{ id: `plan-${choice}-${Date.now()}`, role: "info", text: marker }
|
|
14657
15516
|
]);
|
|
14658
15517
|
if (busy) {
|
|
14659
|
-
|
|
15518
|
+
loop2.abort();
|
|
14660
15519
|
setQueuedSubmit(synthetic);
|
|
14661
15520
|
} else {
|
|
14662
15521
|
await handleSubmit(synthetic);
|
|
14663
15522
|
}
|
|
14664
15523
|
},
|
|
14665
|
-
[pendingPlan, togglePlanMode, busy,
|
|
15524
|
+
[pendingPlan, togglePlanMode, busy, loop2, handleSubmit, persistPlanState]
|
|
14666
15525
|
);
|
|
14667
15526
|
const handlePlanConfirmRef = useRef6(handlePlanConfirm);
|
|
14668
15527
|
useEffect6(() => {
|
|
@@ -14713,13 +15572,13 @@ Stay in plan mode \u2014 address the feedback (explore more if needed), then sub
|
|
|
14713
15572
|
{ id: `plan-${staged.mode}-${Date.now()}`, role: "info", text: marker }
|
|
14714
15573
|
]);
|
|
14715
15574
|
if (busy) {
|
|
14716
|
-
|
|
15575
|
+
loop2.abort();
|
|
14717
15576
|
setQueuedSubmit(synthetic);
|
|
14718
15577
|
} else {
|
|
14719
15578
|
await handleSubmit(synthetic);
|
|
14720
15579
|
}
|
|
14721
15580
|
},
|
|
14722
|
-
[stagedInput, togglePlanMode, busy,
|
|
15581
|
+
[stagedInput, togglePlanMode, busy, loop2, handleSubmit]
|
|
14723
15582
|
);
|
|
14724
15583
|
const handleStagedInputCancel = useCallback4(() => {
|
|
14725
15584
|
if (stagedInput?.plan) setPendingPlan(stagedInput.plan);
|
|
@@ -14748,13 +15607,13 @@ Stay in plan mode \u2014 address the feedback (explore more if needed), then sub
|
|
|
14748
15607
|
{ id: `cp-${choice}-${Date.now()}`, role: "info", text: marker }
|
|
14749
15608
|
]);
|
|
14750
15609
|
if (busy) {
|
|
14751
|
-
|
|
15610
|
+
loop2.abort();
|
|
14752
15611
|
setQueuedSubmit(synthetic);
|
|
14753
15612
|
} else {
|
|
14754
15613
|
await handleSubmit(synthetic);
|
|
14755
15614
|
}
|
|
14756
15615
|
},
|
|
14757
|
-
[pendingCheckpoint, busy,
|
|
15616
|
+
[pendingCheckpoint, busy, loop2, handleSubmit]
|
|
14758
15617
|
);
|
|
14759
15618
|
const handleCheckpointConfirmRef = useRef6(handleCheckpointConfirm);
|
|
14760
15619
|
useEffect6(() => {
|
|
@@ -14782,13 +15641,13 @@ If the feedback only tweaks how you execute (extra constraints, style preference
|
|
|
14782
15641
|
{ id: `cp-revise-${Date.now()}`, role: "info", text: marker }
|
|
14783
15642
|
]);
|
|
14784
15643
|
if (busy) {
|
|
14785
|
-
|
|
15644
|
+
loop2.abort();
|
|
14786
15645
|
setQueuedSubmit(synthetic);
|
|
14787
15646
|
} else {
|
|
14788
15647
|
await handleSubmit(synthetic);
|
|
14789
15648
|
}
|
|
14790
15649
|
},
|
|
14791
|
-
[stagedCheckpointRevise, busy,
|
|
15650
|
+
[stagedCheckpointRevise, busy, loop2, handleSubmit]
|
|
14792
15651
|
);
|
|
14793
15652
|
const handleCheckpointReviseCancel = useCallback4(() => {
|
|
14794
15653
|
const snap = stagedCheckpointRevise;
|
|
@@ -14811,7 +15670,7 @@ If the feedback only tweaks how you execute (extra constraints, style preference
|
|
|
14811
15670
|
{ id: `choice-cancel-${Date.now()}`, role: "info", text: "\u25B8 choice cancelled" }
|
|
14812
15671
|
]);
|
|
14813
15672
|
if (busy) {
|
|
14814
|
-
|
|
15673
|
+
loop2.abort();
|
|
14815
15674
|
setQueuedSubmit(synthetic2);
|
|
14816
15675
|
} else {
|
|
14817
15676
|
await handleSubmit(synthetic2);
|
|
@@ -14826,13 +15685,13 @@ If the feedback only tweaks how you execute (extra constraints, style preference
|
|
|
14826
15685
|
{ id: `choice-pick-${Date.now()}`, role: "info", text: `\u25B8 chose ${label}` }
|
|
14827
15686
|
]);
|
|
14828
15687
|
if (busy) {
|
|
14829
|
-
|
|
15688
|
+
loop2.abort();
|
|
14830
15689
|
setQueuedSubmit(synthetic);
|
|
14831
15690
|
} else {
|
|
14832
15691
|
await handleSubmit(synthetic);
|
|
14833
15692
|
}
|
|
14834
15693
|
},
|
|
14835
|
-
[pendingChoice, busy,
|
|
15694
|
+
[pendingChoice, busy, loop2, handleSubmit]
|
|
14836
15695
|
);
|
|
14837
15696
|
const handleChoiceConfirmRef = useRef6(handleChoiceConfirm);
|
|
14838
15697
|
useEffect6(() => {
|
|
@@ -14857,13 +15716,13 @@ Read it carefully and proceed \u2014 don't snap back to the options you listed u
|
|
|
14857
15716
|
{ id: `choice-custom-${Date.now()}`, role: "info", text: marker }
|
|
14858
15717
|
]);
|
|
14859
15718
|
if (busy) {
|
|
14860
|
-
|
|
15719
|
+
loop2.abort();
|
|
14861
15720
|
setQueuedSubmit(synthetic);
|
|
14862
15721
|
} else {
|
|
14863
15722
|
await handleSubmit(synthetic);
|
|
14864
15723
|
}
|
|
14865
15724
|
},
|
|
14866
|
-
[busy,
|
|
15725
|
+
[busy, loop2, handleSubmit]
|
|
14867
15726
|
);
|
|
14868
15727
|
const handleChoiceCustomCancel = useCallback4(() => {
|
|
14869
15728
|
const snap = stagedChoiceCustom;
|
|
@@ -14882,7 +15741,7 @@ Read it carefully and proceed \u2014 don't snap back to the options you listed u
|
|
|
14882
15741
|
{ id: `revise-reject-${Date.now()}`, role: "info", text: "\u25B8 revision rejected" }
|
|
14883
15742
|
]);
|
|
14884
15743
|
if (busy) {
|
|
14885
|
-
|
|
15744
|
+
loop2.abort();
|
|
14886
15745
|
setQueuedSubmit(synthetic2);
|
|
14887
15746
|
} else {
|
|
14888
15747
|
await handleSubmit(synthetic2);
|
|
@@ -14917,13 +15776,13 @@ ${snap.remainingSteps.map((s, i) => ` ${i + 1}. ${s.id} \xB7 ${s.title} \u2014
|
|
|
14917
15776
|
|
|
14918
15777
|
Continue executing from the next pending step. Call mark_step_complete after each one as before.`;
|
|
14919
15778
|
if (busy) {
|
|
14920
|
-
|
|
15779
|
+
loop2.abort();
|
|
14921
15780
|
setQueuedSubmit(synthetic);
|
|
14922
15781
|
} else {
|
|
14923
15782
|
await handleSubmit(synthetic);
|
|
14924
15783
|
}
|
|
14925
15784
|
},
|
|
14926
|
-
[pendingRevision, busy,
|
|
15785
|
+
[pendingRevision, busy, loop2, handleSubmit, persistPlanState]
|
|
14927
15786
|
);
|
|
14928
15787
|
const handleReviseConfirmRef = useRef6(handleReviseConfirm);
|
|
14929
15788
|
useEffect6(() => {
|
|
@@ -14936,17 +15795,17 @@ Continue executing from the next pending step. Call mark_step_complete after eac
|
|
|
14936
15795
|
return /* @__PURE__ */ React23.createElement(React23.Fragment, null, /* @__PURE__ */ React23.createElement(
|
|
14937
15796
|
TickerProvider,
|
|
14938
15797
|
{
|
|
14939
|
-
disabled: PLAIN_UI || isResizing || !!pendingPlan || !!pendingShell || !!pendingEditReview || !!pendingCheckpoint || !!stagedCheckpointRevise || !!pendingChoice || !!stagedChoiceCustom || !!pendingRevision
|
|
15798
|
+
disabled: PLAIN_UI || isResizing || !!pendingPlan || !!pendingShell || !!pendingEditReview || walkthroughActive || !!pendingCheckpoint || !!stagedCheckpointRevise || !!pendingChoice || !!stagedChoiceCustom || !!pendingRevision
|
|
14940
15799
|
},
|
|
14941
15800
|
/* @__PURE__ */ React23.createElement(Box21, { flexDirection: "column" }, /* @__PURE__ */ React23.createElement(
|
|
14942
15801
|
StatsPanel,
|
|
14943
15802
|
{
|
|
14944
15803
|
summary,
|
|
14945
|
-
model:
|
|
15804
|
+
model: loop2.model,
|
|
14946
15805
|
prefixHash,
|
|
14947
|
-
harvestOn:
|
|
14948
|
-
branchBudget:
|
|
14949
|
-
reasoningEffort:
|
|
15806
|
+
harvestOn: loop2.harvestEnabled,
|
|
15807
|
+
branchBudget: loop2.branchOptions.budget,
|
|
15808
|
+
reasoningEffort: loop2.reasoningEffort,
|
|
14950
15809
|
planMode,
|
|
14951
15810
|
editMode: codeMode ? editMode : void 0,
|
|
14952
15811
|
balance,
|
|
@@ -15028,13 +15887,20 @@ Continue executing from the next pending step. Call mark_step_complete after eac
|
|
|
15028
15887
|
{
|
|
15029
15888
|
block: pendingEditReview,
|
|
15030
15889
|
onChoose: (choice) => {
|
|
15031
|
-
const
|
|
15032
|
-
if (
|
|
15890
|
+
const resolve9 = editReviewResolveRef.current;
|
|
15891
|
+
if (resolve9) {
|
|
15033
15892
|
editReviewResolveRef.current = null;
|
|
15034
|
-
|
|
15893
|
+
resolve9(choice);
|
|
15035
15894
|
}
|
|
15036
15895
|
}
|
|
15037
15896
|
}
|
|
15897
|
+
) : walkthroughActive && pendingEdits.current.length > 0 ? /* @__PURE__ */ React23.createElement(
|
|
15898
|
+
EditConfirm,
|
|
15899
|
+
{
|
|
15900
|
+
key: `walk-${pendingTick}`,
|
|
15901
|
+
block: pendingEdits.current[0],
|
|
15902
|
+
onChoose: handleWalkChoice
|
|
15903
|
+
}
|
|
15038
15904
|
) : /* @__PURE__ */ React23.createElement(React23.Fragment, null, codeMode ? /* @__PURE__ */ React23.createElement(
|
|
15039
15905
|
ModeStatusBar,
|
|
15040
15906
|
{
|
|
@@ -15045,7 +15911,7 @@ Continue executing from the next pending step. Call mark_step_complete after eac
|
|
|
15045
15911
|
undoArmed: !!undoBanner || hasUndoable(),
|
|
15046
15912
|
jobs: codeMode.jobs
|
|
15047
15913
|
}
|
|
15048
|
-
) : null, /* @__PURE__ */ React23.createElement(
|
|
15914
|
+
) : null, activeLoop ? /* @__PURE__ */ React23.createElement(LoopStatusRow, { loop: activeLoop }) : null, /* @__PURE__ */ React23.createElement(
|
|
15049
15915
|
PromptInput,
|
|
15050
15916
|
{
|
|
15051
15917
|
value: input,
|
|
@@ -15076,7 +15942,7 @@ Continue executing from the next pending step. Call mark_step_complete after eac
|
|
|
15076
15942
|
}
|
|
15077
15943
|
|
|
15078
15944
|
// src/cli/ui/SessionPicker.tsx
|
|
15079
|
-
import { Box as Box22, Text as
|
|
15945
|
+
import { Box as Box22, Text as Text20 } from "ink";
|
|
15080
15946
|
import React24 from "react";
|
|
15081
15947
|
function SessionPicker({
|
|
15082
15948
|
sessionName,
|
|
@@ -15084,7 +15950,7 @@ function SessionPicker({
|
|
|
15084
15950
|
lastActive,
|
|
15085
15951
|
onChoose
|
|
15086
15952
|
}) {
|
|
15087
|
-
return /* @__PURE__ */ React24.createElement(Box22, { flexDirection: "column", marginY: 1 }, /* @__PURE__ */ React24.createElement(Box22, { marginBottom: 1 }, /* @__PURE__ */ React24.createElement(
|
|
15953
|
+
return /* @__PURE__ */ React24.createElement(Box22, { flexDirection: "column", marginY: 1 }, /* @__PURE__ */ React24.createElement(Box22, { marginBottom: 1 }, /* @__PURE__ */ React24.createElement(Text20, { bold: true, color: "cyan" }, `Session "${sessionName}" has ${messageCount} prior message${messageCount === 1 ? "" : "s"}`), /* @__PURE__ */ React24.createElement(Text20, { dimColor: true }, ` \xB7 last active ${relativeTime2(lastActive)}`)), /* @__PURE__ */ React24.createElement(
|
|
15088
15954
|
SingleSelect,
|
|
15089
15955
|
{
|
|
15090
15956
|
initialValue: "new",
|
|
@@ -15107,7 +15973,7 @@ function SessionPicker({
|
|
|
15107
15973
|
],
|
|
15108
15974
|
onSubmit: (v) => onChoose(v)
|
|
15109
15975
|
}
|
|
15110
|
-
), /* @__PURE__ */ React24.createElement(Box22, { marginTop: 1 }, /* @__PURE__ */ React24.createElement(
|
|
15976
|
+
), /* @__PURE__ */ React24.createElement(Box22, { marginTop: 1 }, /* @__PURE__ */ React24.createElement(Text20, { dimColor: true }, "[\u2191\u2193] navigate \xB7 [Enter] select")));
|
|
15111
15977
|
}
|
|
15112
15978
|
function relativeTime2(date) {
|
|
15113
15979
|
const ms = Date.now() - date.getTime();
|
|
@@ -15123,7 +15989,7 @@ function relativeTime2(date) {
|
|
|
15123
15989
|
}
|
|
15124
15990
|
|
|
15125
15991
|
// src/cli/ui/Setup.tsx
|
|
15126
|
-
import { Box as Box23, Text as
|
|
15992
|
+
import { Box as Box23, Text as Text21, useApp as useApp2 } from "ink";
|
|
15127
15993
|
import TextInput from "ink-text-input";
|
|
15128
15994
|
import React25, { useState as useState11 } from "react";
|
|
15129
15995
|
function Setup({ onReady }) {
|
|
@@ -15149,7 +16015,7 @@ function Setup({ onReady }) {
|
|
|
15149
16015
|
}
|
|
15150
16016
|
onReady(trimmed);
|
|
15151
16017
|
};
|
|
15152
|
-
return /* @__PURE__ */ React25.createElement(Box23, { flexDirection: "column", borderStyle: "round", borderColor: "cyan", paddingX: 1 }, /* @__PURE__ */ React25.createElement(
|
|
16018
|
+
return /* @__PURE__ */ React25.createElement(Box23, { flexDirection: "column", borderStyle: "round", borderColor: "cyan", paddingX: 1 }, /* @__PURE__ */ React25.createElement(Text21, { bold: true, color: "cyan" }, "Welcome to Reasonix."), /* @__PURE__ */ React25.createElement(Box23, { marginTop: 1 }, /* @__PURE__ */ React25.createElement(Text21, null, "Paste your DeepSeek API key to get started.")), /* @__PURE__ */ React25.createElement(Text21, { dimColor: true }, "Get one (free credit on signup): https://platform.deepseek.com/api_keys"), /* @__PURE__ */ React25.createElement(Text21, { dimColor: true }, "Saved locally to ", defaultConfigPath()), /* @__PURE__ */ React25.createElement(Box23, { marginTop: 1 }, /* @__PURE__ */ React25.createElement(Text21, { bold: true, color: "cyan" }, "key \u203A "), /* @__PURE__ */ React25.createElement(
|
|
15153
16019
|
TextInput,
|
|
15154
16020
|
{
|
|
15155
16021
|
value,
|
|
@@ -15158,7 +16024,7 @@ function Setup({ onReady }) {
|
|
|
15158
16024
|
mask: "\u2022",
|
|
15159
16025
|
placeholder: "sk-..."
|
|
15160
16026
|
}
|
|
15161
|
-
)), error ? /* @__PURE__ */ React25.createElement(Box23, { marginTop: 1 }, /* @__PURE__ */ React25.createElement(
|
|
16027
|
+
)), error ? /* @__PURE__ */ React25.createElement(Box23, { marginTop: 1 }, /* @__PURE__ */ React25.createElement(Text21, { color: "red" }, error)) : value ? /* @__PURE__ */ React25.createElement(Box23, { marginTop: 1 }, /* @__PURE__ */ React25.createElement(Text21, { dimColor: true }, "preview: ", redactKey(value))) : null, /* @__PURE__ */ React25.createElement(Box23, { marginTop: 1 }, /* @__PURE__ */ React25.createElement(Text21, { dimColor: true }, "(Type /exit to abort.)")));
|
|
15162
16028
|
}
|
|
15163
16029
|
|
|
15164
16030
|
// src/cli/commands/chat.tsx
|
|
@@ -15331,8 +16197,653 @@ async function chatCommand(opts) {
|
|
|
15331
16197
|
|
|
15332
16198
|
// src/cli/commands/code.tsx
|
|
15333
16199
|
import { basename as basename2, resolve as resolve7 } from "path";
|
|
16200
|
+
|
|
16201
|
+
// src/index/semantic/builder.ts
|
|
16202
|
+
import { promises as fs5 } from "fs";
|
|
16203
|
+
import path4 from "path";
|
|
16204
|
+
|
|
16205
|
+
// src/index/semantic/chunker.ts
|
|
16206
|
+
import { promises as fs3 } from "fs";
|
|
16207
|
+
import path2 from "path";
|
|
16208
|
+
var DEFAULT_MAX_CHUNK_CHARS = 4e3;
|
|
16209
|
+
var SKIP_DIRS = /* @__PURE__ */ new Set([
|
|
16210
|
+
"node_modules",
|
|
16211
|
+
".git",
|
|
16212
|
+
"dist",
|
|
16213
|
+
"build",
|
|
16214
|
+
"out",
|
|
16215
|
+
".next",
|
|
16216
|
+
".nuxt",
|
|
16217
|
+
"target",
|
|
16218
|
+
".venv",
|
|
16219
|
+
"venv",
|
|
16220
|
+
"__pycache__",
|
|
16221
|
+
".pytest_cache",
|
|
16222
|
+
".mypy_cache",
|
|
16223
|
+
".cache",
|
|
16224
|
+
"coverage",
|
|
16225
|
+
".turbo",
|
|
16226
|
+
".vercel",
|
|
16227
|
+
".reasonix"
|
|
16228
|
+
]);
|
|
16229
|
+
var SKIP_FILES = /* @__PURE__ */ new Set([
|
|
16230
|
+
"package-lock.json",
|
|
16231
|
+
"yarn.lock",
|
|
16232
|
+
"pnpm-lock.yaml",
|
|
16233
|
+
"Cargo.lock",
|
|
16234
|
+
"poetry.lock",
|
|
16235
|
+
"Pipfile.lock",
|
|
16236
|
+
"go.sum",
|
|
16237
|
+
".DS_Store"
|
|
16238
|
+
]);
|
|
16239
|
+
var BINARY_EXTS = /* @__PURE__ */ new Set([
|
|
16240
|
+
// Images
|
|
16241
|
+
".png",
|
|
16242
|
+
".jpg",
|
|
16243
|
+
".jpeg",
|
|
16244
|
+
".gif",
|
|
16245
|
+
".webp",
|
|
16246
|
+
".bmp",
|
|
16247
|
+
".ico",
|
|
16248
|
+
".tiff",
|
|
16249
|
+
// Fonts
|
|
16250
|
+
".woff",
|
|
16251
|
+
".woff2",
|
|
16252
|
+
".ttf",
|
|
16253
|
+
".otf",
|
|
16254
|
+
".eot",
|
|
16255
|
+
// Archives / binaries
|
|
16256
|
+
".zip",
|
|
16257
|
+
".tar",
|
|
16258
|
+
".gz",
|
|
16259
|
+
".rar",
|
|
16260
|
+
".7z",
|
|
16261
|
+
".exe",
|
|
16262
|
+
".dll",
|
|
16263
|
+
".so",
|
|
16264
|
+
".dylib",
|
|
16265
|
+
".class",
|
|
16266
|
+
".jar",
|
|
16267
|
+
".wasm",
|
|
16268
|
+
".o",
|
|
16269
|
+
".a",
|
|
16270
|
+
// Media
|
|
16271
|
+
".mp3",
|
|
16272
|
+
".mp4",
|
|
16273
|
+
".wav",
|
|
16274
|
+
".ogg",
|
|
16275
|
+
".webm",
|
|
16276
|
+
".mov",
|
|
16277
|
+
// Other
|
|
16278
|
+
".pdf",
|
|
16279
|
+
".sqlite",
|
|
16280
|
+
".db"
|
|
16281
|
+
]);
|
|
16282
|
+
function chunkText(text, filePath, windowLines, overlap, maxChunkChars = DEFAULT_MAX_CHUNK_CHARS) {
|
|
16283
|
+
const lines = text.split(/\r?\n/);
|
|
16284
|
+
if (lines.length === 0 || lines.length === 1 && lines[0] === "") return [];
|
|
16285
|
+
const stride = Math.max(1, windowLines - overlap);
|
|
16286
|
+
const chunks = [];
|
|
16287
|
+
for (let start = 0; start < lines.length; start += stride) {
|
|
16288
|
+
const end = Math.min(lines.length, start + windowLines);
|
|
16289
|
+
const slice = lines.slice(start, end).join("\n").trim();
|
|
16290
|
+
if (slice.length === 0) {
|
|
16291
|
+
if (end >= lines.length) break;
|
|
16292
|
+
continue;
|
|
16293
|
+
}
|
|
16294
|
+
const window = {
|
|
16295
|
+
path: filePath,
|
|
16296
|
+
startLine: start + 1,
|
|
16297
|
+
endLine: end,
|
|
16298
|
+
text: slice
|
|
16299
|
+
};
|
|
16300
|
+
for (const sub of safeSplit(window, maxChunkChars)) chunks.push(sub);
|
|
16301
|
+
if (end >= lines.length) break;
|
|
16302
|
+
}
|
|
16303
|
+
return chunks;
|
|
16304
|
+
}
|
|
16305
|
+
function safeSplit(chunk, maxChars) {
|
|
16306
|
+
if (chunk.text.length <= maxChars) return [chunk];
|
|
16307
|
+
const lines = chunk.text.split("\n");
|
|
16308
|
+
const out = [];
|
|
16309
|
+
let bufLines = [];
|
|
16310
|
+
let bufStart = chunk.startLine;
|
|
16311
|
+
let bufLen = 0;
|
|
16312
|
+
const flush = (untilLineNo) => {
|
|
16313
|
+
if (bufLines.length === 0) return;
|
|
16314
|
+
out.push({
|
|
16315
|
+
path: chunk.path,
|
|
16316
|
+
startLine: bufStart,
|
|
16317
|
+
endLine: untilLineNo,
|
|
16318
|
+
text: bufLines.join("\n")
|
|
16319
|
+
});
|
|
16320
|
+
bufLines = [];
|
|
16321
|
+
bufLen = 0;
|
|
16322
|
+
};
|
|
16323
|
+
for (let i = 0; i < lines.length; i++) {
|
|
16324
|
+
const line = lines[i] ?? "";
|
|
16325
|
+
const lineLen = line.length + 1;
|
|
16326
|
+
if (lineLen > maxChars) {
|
|
16327
|
+
flush(chunk.startLine + i - 1);
|
|
16328
|
+
out.push({
|
|
16329
|
+
path: chunk.path,
|
|
16330
|
+
startLine: chunk.startLine + i,
|
|
16331
|
+
endLine: chunk.startLine + i,
|
|
16332
|
+
text: line.slice(0, maxChars)
|
|
16333
|
+
});
|
|
16334
|
+
bufStart = chunk.startLine + i + 1;
|
|
16335
|
+
continue;
|
|
16336
|
+
}
|
|
16337
|
+
if (bufLen + lineLen > maxChars && bufLines.length > 0) {
|
|
16338
|
+
flush(chunk.startLine + i - 1);
|
|
16339
|
+
bufStart = chunk.startLine + i;
|
|
16340
|
+
}
|
|
16341
|
+
bufLines.push(line);
|
|
16342
|
+
bufLen += lineLen;
|
|
16343
|
+
}
|
|
16344
|
+
flush(chunk.endLine);
|
|
16345
|
+
return out;
|
|
16346
|
+
}
|
|
16347
|
+
async function* walkChunks(root, opts = {}) {
|
|
16348
|
+
const windowLines = opts.windowLines ?? 60;
|
|
16349
|
+
const overlap = Math.min(opts.overlap ?? 12, Math.max(0, windowLines - 1));
|
|
16350
|
+
const maxFileBytes = opts.maxFileBytes ?? 256 * 1024;
|
|
16351
|
+
const maxChunkChars = opts.maxChunkChars ?? DEFAULT_MAX_CHUNK_CHARS;
|
|
16352
|
+
const stack = [root];
|
|
16353
|
+
while (stack.length > 0) {
|
|
16354
|
+
const dir = stack.pop();
|
|
16355
|
+
if (!dir) break;
|
|
16356
|
+
let entries;
|
|
16357
|
+
try {
|
|
16358
|
+
entries = await fs3.readdir(dir, { withFileTypes: true });
|
|
16359
|
+
} catch {
|
|
16360
|
+
continue;
|
|
16361
|
+
}
|
|
16362
|
+
for (const entry of entries) {
|
|
16363
|
+
const name = entry.name;
|
|
16364
|
+
if (entry.isDirectory()) {
|
|
16365
|
+
if (SKIP_DIRS.has(name) || name.startsWith(".")) {
|
|
16366
|
+
if (SKIP_DIRS.has(name) || name === ".git") continue;
|
|
16367
|
+
}
|
|
16368
|
+
stack.push(path2.join(dir, name));
|
|
16369
|
+
continue;
|
|
16370
|
+
}
|
|
16371
|
+
if (!entry.isFile()) continue;
|
|
16372
|
+
if (SKIP_FILES.has(name)) continue;
|
|
16373
|
+
const ext = path2.extname(name).toLowerCase();
|
|
16374
|
+
if (BINARY_EXTS.has(ext)) continue;
|
|
16375
|
+
const abs = path2.join(dir, name);
|
|
16376
|
+
let stat;
|
|
16377
|
+
try {
|
|
16378
|
+
stat = await fs3.stat(abs);
|
|
16379
|
+
} catch {
|
|
16380
|
+
continue;
|
|
16381
|
+
}
|
|
16382
|
+
if (stat.size > maxFileBytes) continue;
|
|
16383
|
+
let text;
|
|
16384
|
+
try {
|
|
16385
|
+
text = await fs3.readFile(abs, "utf8");
|
|
16386
|
+
} catch {
|
|
16387
|
+
continue;
|
|
16388
|
+
}
|
|
16389
|
+
if (text.indexOf("\0") !== -1) continue;
|
|
16390
|
+
const rel = path2.relative(root, abs).split(path2.sep).join("/");
|
|
16391
|
+
for (const chunk of chunkText(text, rel, windowLines, overlap, maxChunkChars)) {
|
|
16392
|
+
yield chunk;
|
|
16393
|
+
}
|
|
16394
|
+
}
|
|
16395
|
+
}
|
|
16396
|
+
}
|
|
16397
|
+
|
|
16398
|
+
// src/index/semantic/store.ts
|
|
16399
|
+
import { promises as fs4 } from "fs";
|
|
16400
|
+
import path3 from "path";
|
|
16401
|
+
var STORE_VERSION = 1;
|
|
16402
|
+
var META_FILE = "index.meta.json";
|
|
16403
|
+
var DATA_FILE = "index.jsonl";
|
|
16404
|
+
var SemanticStore = class {
|
|
16405
|
+
constructor(indexDir, model2) {
|
|
16406
|
+
this.indexDir = indexDir;
|
|
16407
|
+
this.model = model2;
|
|
16408
|
+
}
|
|
16409
|
+
indexDir;
|
|
16410
|
+
model;
|
|
16411
|
+
entries = [];
|
|
16412
|
+
byPath = /* @__PURE__ */ new Map();
|
|
16413
|
+
dim = 0;
|
|
16414
|
+
/** True when no entries are loaded — the index doesn't exist or is empty. */
|
|
16415
|
+
get empty() {
|
|
16416
|
+
return this.entries.length === 0;
|
|
16417
|
+
}
|
|
16418
|
+
/** Total number of indexed chunks. */
|
|
16419
|
+
get size() {
|
|
16420
|
+
return this.entries.length;
|
|
16421
|
+
}
|
|
16422
|
+
/** Read-only view, mostly for tests. */
|
|
16423
|
+
get all() {
|
|
16424
|
+
return this.entries;
|
|
16425
|
+
}
|
|
16426
|
+
/** Last-known mtime per indexed file (ms epoch) for incremental rebuilds. */
|
|
16427
|
+
fileMtimes() {
|
|
16428
|
+
const out = /* @__PURE__ */ new Map();
|
|
16429
|
+
for (const [p, group] of this.byPath) {
|
|
16430
|
+
const first = group[0];
|
|
16431
|
+
if (first) out.set(p, first.mtimeMs);
|
|
16432
|
+
}
|
|
16433
|
+
return out;
|
|
16434
|
+
}
|
|
16435
|
+
/** Append entries to in-memory state and to disk. Re-indexes the
|
|
16436
|
+
* `byPath` map. Caller is responsible for L2-normalizing each
|
|
16437
|
+
* embedding before calling — the search hot path assumes unit vectors. */
|
|
16438
|
+
async add(entries) {
|
|
16439
|
+
if (entries.length === 0) return;
|
|
16440
|
+
if (this.dim === 0) this.dim = entries[0].embedding.length;
|
|
16441
|
+
const lines = [];
|
|
16442
|
+
for (const e of entries) {
|
|
16443
|
+
if (e.embedding.length !== this.dim) {
|
|
16444
|
+
throw new Error(
|
|
16445
|
+
`embedding dim mismatch: expected ${this.dim}, got ${e.embedding.length} for ${e.path}:${e.startLine}`
|
|
16446
|
+
);
|
|
16447
|
+
}
|
|
16448
|
+
this.entries.push(e);
|
|
16449
|
+
const list = this.byPath.get(e.path);
|
|
16450
|
+
if (list) list.push(e);
|
|
16451
|
+
else this.byPath.set(e.path, [e]);
|
|
16452
|
+
lines.push(serializeEntry(e));
|
|
16453
|
+
}
|
|
16454
|
+
await fs4.mkdir(this.indexDir, { recursive: true });
|
|
16455
|
+
await fs4.appendFile(path3.join(this.indexDir, DATA_FILE), `${lines.join("\n")}
|
|
16456
|
+
`, "utf8");
|
|
16457
|
+
await this.writeMeta();
|
|
16458
|
+
}
|
|
16459
|
+
/**
|
|
16460
|
+
* Drop every entry whose `path` is in `paths`. Used by incremental
|
|
16461
|
+
* rebuild: when a file's mtime changes, the existing entries for
|
|
16462
|
+
* it are evicted before re-chunking + re-embedding. Implementation
|
|
16463
|
+
* rewrites the JSONL — append-only is fine for adds, but deletes
|
|
16464
|
+
* need a compaction pass.
|
|
16465
|
+
*/
|
|
16466
|
+
async remove(paths) {
|
|
16467
|
+
if (paths.length === 0) return 0;
|
|
16468
|
+
const drop = new Set(paths);
|
|
16469
|
+
const before = this.entries.length;
|
|
16470
|
+
this.entries = this.entries.filter((e) => !drop.has(e.path));
|
|
16471
|
+
for (const p of paths) this.byPath.delete(p);
|
|
16472
|
+
const removed = before - this.entries.length;
|
|
16473
|
+
if (removed > 0) await this.flush();
|
|
16474
|
+
return removed;
|
|
16475
|
+
}
|
|
16476
|
+
/**
|
|
16477
|
+
* Top-K cosine search. `query` MUST already be L2-normalized — the
|
|
16478
|
+
* caller embeds + normalizes once per query. Filtering hits by
|
|
16479
|
+
* minimum score (`minScore`) is optional; tune via UI to suppress
|
|
16480
|
+
* weakly relevant snippets that distract the model.
|
|
16481
|
+
*/
|
|
16482
|
+
search(query, topK = 8, minScore = 0) {
|
|
16483
|
+
if (this.entries.length === 0) return [];
|
|
16484
|
+
if (query.length !== this.dim && this.dim !== 0) {
|
|
16485
|
+
throw new Error(`query dim ${query.length} \u2260 index dim ${this.dim}`);
|
|
16486
|
+
}
|
|
16487
|
+
const heap = [];
|
|
16488
|
+
for (const entry of this.entries) {
|
|
16489
|
+
const score = dot(query, entry.embedding);
|
|
16490
|
+
if (score < minScore) continue;
|
|
16491
|
+
if (heap.length < topK) {
|
|
16492
|
+
heap.push({ entry, score });
|
|
16493
|
+
if (heap.length === topK) heap.sort((a, b) => a.score - b.score);
|
|
16494
|
+
} else if (score > heap[0].score) {
|
|
16495
|
+
heap[0] = { entry, score };
|
|
16496
|
+
for (let i = 0; i < heap.length - 1; i++) {
|
|
16497
|
+
if (heap[i].score > heap[i + 1].score) {
|
|
16498
|
+
const tmp = heap[i];
|
|
16499
|
+
heap[i] = heap[i + 1];
|
|
16500
|
+
heap[i + 1] = tmp;
|
|
16501
|
+
}
|
|
16502
|
+
}
|
|
16503
|
+
}
|
|
16504
|
+
}
|
|
16505
|
+
return heap.sort((a, b) => b.score - a.score);
|
|
16506
|
+
}
|
|
16507
|
+
/**
|
|
16508
|
+
* Rewrite the JSONL on disk with the current in-memory state. Used
|
|
16509
|
+
* after `remove` and from `flush`. We write to a temp file and
|
|
16510
|
+
* rename so a Ctrl+C mid-write never leaves the index half-empty.
|
|
16511
|
+
*/
|
|
16512
|
+
async flush() {
|
|
16513
|
+
await fs4.mkdir(this.indexDir, { recursive: true });
|
|
16514
|
+
const tmp = path3.join(this.indexDir, `${DATA_FILE}.tmp`);
|
|
16515
|
+
const final = path3.join(this.indexDir, DATA_FILE);
|
|
16516
|
+
const lines = this.entries.map(serializeEntry).join("\n");
|
|
16517
|
+
await fs4.writeFile(tmp, lines.length > 0 ? `${lines}
|
|
16518
|
+
` : "", "utf8");
|
|
16519
|
+
await fs4.rename(tmp, final);
|
|
16520
|
+
await this.writeMeta();
|
|
16521
|
+
}
|
|
16522
|
+
async writeMeta() {
|
|
16523
|
+
const meta = {
|
|
16524
|
+
version: STORE_VERSION,
|
|
16525
|
+
model: this.model,
|
|
16526
|
+
dim: this.dim,
|
|
16527
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
16528
|
+
};
|
|
16529
|
+
await fs4.writeFile(
|
|
16530
|
+
path3.join(this.indexDir, META_FILE),
|
|
16531
|
+
`${JSON.stringify(meta, null, 2)}
|
|
16532
|
+
`,
|
|
16533
|
+
"utf8"
|
|
16534
|
+
);
|
|
16535
|
+
}
|
|
16536
|
+
/** Drop everything from disk + memory. Used by `--rebuild`. */
|
|
16537
|
+
async wipe() {
|
|
16538
|
+
this.entries = [];
|
|
16539
|
+
this.byPath.clear();
|
|
16540
|
+
this.dim = 0;
|
|
16541
|
+
await fs4.rm(path3.join(this.indexDir, DATA_FILE), { force: true });
|
|
16542
|
+
await fs4.rm(path3.join(this.indexDir, META_FILE), { force: true });
|
|
16543
|
+
}
|
|
16544
|
+
};
|
|
16545
|
+
async function openStore(indexDir, model2) {
|
|
16546
|
+
const store = new SemanticStore(indexDir, model2);
|
|
16547
|
+
const dataPath = path3.join(indexDir, DATA_FILE);
|
|
16548
|
+
const metaPath = path3.join(indexDir, META_FILE);
|
|
16549
|
+
let meta = null;
|
|
16550
|
+
try {
|
|
16551
|
+
const raw2 = await fs4.readFile(metaPath, "utf8");
|
|
16552
|
+
meta = JSON.parse(raw2);
|
|
16553
|
+
} catch {
|
|
16554
|
+
}
|
|
16555
|
+
if (meta) {
|
|
16556
|
+
if (meta.version !== STORE_VERSION) {
|
|
16557
|
+
throw new Error(
|
|
16558
|
+
`Index format version ${meta.version} does not match current ${STORE_VERSION}. Run \`reasonix index --rebuild\`.`
|
|
16559
|
+
);
|
|
16560
|
+
}
|
|
16561
|
+
if (meta.model !== model2) {
|
|
16562
|
+
throw new Error(
|
|
16563
|
+
`Index was built with model "${meta.model}" but current is "${model2}". Run \`reasonix index --rebuild\`.`
|
|
16564
|
+
);
|
|
16565
|
+
}
|
|
16566
|
+
}
|
|
16567
|
+
let raw;
|
|
16568
|
+
try {
|
|
16569
|
+
raw = await fs4.readFile(dataPath, "utf8");
|
|
16570
|
+
} catch {
|
|
16571
|
+
return store;
|
|
16572
|
+
}
|
|
16573
|
+
for (const line of raw.split("\n")) {
|
|
16574
|
+
if (line.length === 0) continue;
|
|
16575
|
+
try {
|
|
16576
|
+
const entry = deserializeEntry(line);
|
|
16577
|
+
store.dim = entry.embedding.length;
|
|
16578
|
+
store.entries.push(entry);
|
|
16579
|
+
const map = store.byPath;
|
|
16580
|
+
const list = map.get(entry.path);
|
|
16581
|
+
if (list) list.push(entry);
|
|
16582
|
+
else map.set(entry.path, [entry]);
|
|
16583
|
+
} catch {
|
|
16584
|
+
}
|
|
16585
|
+
}
|
|
16586
|
+
return store;
|
|
16587
|
+
}
|
|
16588
|
+
function normalize(v) {
|
|
16589
|
+
let sum = 0;
|
|
16590
|
+
for (let i = 0; i < v.length; i++) sum += v[i] * v[i];
|
|
16591
|
+
const inv = sum > 0 ? 1 / Math.sqrt(sum) : 0;
|
|
16592
|
+
for (let i = 0; i < v.length; i++) v[i] = v[i] * inv;
|
|
16593
|
+
return v;
|
|
16594
|
+
}
|
|
16595
|
+
function dot(a, b) {
|
|
16596
|
+
let s = 0;
|
|
16597
|
+
for (let i = 0; i < a.length; i++) s += a[i] * b[i];
|
|
16598
|
+
return s;
|
|
16599
|
+
}
|
|
16600
|
+
function serializeEntry(e) {
|
|
16601
|
+
const buf = Buffer.from(e.embedding.buffer, e.embedding.byteOffset, e.embedding.byteLength);
|
|
16602
|
+
return JSON.stringify({
|
|
16603
|
+
p: e.path,
|
|
16604
|
+
s: e.startLine,
|
|
16605
|
+
e: e.endLine,
|
|
16606
|
+
m: e.mtimeMs,
|
|
16607
|
+
t: e.text,
|
|
16608
|
+
v: buf.toString("base64")
|
|
16609
|
+
});
|
|
16610
|
+
}
|
|
16611
|
+
function deserializeEntry(line) {
|
|
16612
|
+
const parsed = JSON.parse(line);
|
|
16613
|
+
const bytes = Buffer.from(parsed.v, "base64");
|
|
16614
|
+
const f32 = new Float32Array(
|
|
16615
|
+
bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength)
|
|
16616
|
+
);
|
|
16617
|
+
return {
|
|
16618
|
+
path: parsed.p,
|
|
16619
|
+
startLine: parsed.s,
|
|
16620
|
+
endLine: parsed.e,
|
|
16621
|
+
mtimeMs: parsed.m,
|
|
16622
|
+
text: parsed.t,
|
|
16623
|
+
embedding: f32
|
|
16624
|
+
};
|
|
16625
|
+
}
|
|
16626
|
+
|
|
16627
|
+
// src/index/semantic/builder.ts
|
|
16628
|
+
var INDEX_DIR_NAME = path4.join(".reasonix", "semantic");
|
|
16629
|
+
async function buildIndex(root, opts = {}) {
|
|
16630
|
+
const t0 = Date.now();
|
|
16631
|
+
const indexDir = path4.join(root, INDEX_DIR_NAME);
|
|
16632
|
+
const probe = await probeOllama({ baseUrl: opts.baseUrl, signal: opts.signal });
|
|
16633
|
+
if (!probe.ok) {
|
|
16634
|
+
throw new Error(
|
|
16635
|
+
`Ollama is not reachable: ${probe.error}. Install from https://ollama.com, then \`ollama serve\` and \`ollama pull ${opts.model ?? "nomic-embed-text"}\`.`
|
|
16636
|
+
);
|
|
16637
|
+
}
|
|
16638
|
+
const model2 = opts.model ?? process.env.REASONIX_EMBED_MODEL ?? "nomic-embed-text";
|
|
16639
|
+
const store = await openStore(indexDir, model2);
|
|
16640
|
+
if (opts.rebuild) await store.wipe();
|
|
16641
|
+
const lastMtimes = store.fileMtimes();
|
|
16642
|
+
const seenPaths = /* @__PURE__ */ new Set();
|
|
16643
|
+
const fileChunks = /* @__PURE__ */ new Map();
|
|
16644
|
+
let filesScanned = 0;
|
|
16645
|
+
let filesSkipped = 0;
|
|
16646
|
+
for await (const chunk of walkChunks(root, {
|
|
16647
|
+
windowLines: opts.windowLines,
|
|
16648
|
+
overlap: opts.overlap,
|
|
16649
|
+
maxFileBytes: opts.maxFileBytes
|
|
16650
|
+
})) {
|
|
16651
|
+
seenPaths.add(chunk.path);
|
|
16652
|
+
let bucket = fileChunks.get(chunk.path);
|
|
16653
|
+
if (!bucket) {
|
|
16654
|
+
filesScanned++;
|
|
16655
|
+
const abs = path4.join(root, chunk.path);
|
|
16656
|
+
let mtimeMs = 0;
|
|
16657
|
+
try {
|
|
16658
|
+
const stat = await fs5.stat(abs);
|
|
16659
|
+
mtimeMs = stat.mtimeMs;
|
|
16660
|
+
} catch {
|
|
16661
|
+
continue;
|
|
16662
|
+
}
|
|
16663
|
+
const last = lastMtimes.get(chunk.path);
|
|
16664
|
+
if (last !== void 0 && last === mtimeMs && !opts.rebuild) {
|
|
16665
|
+
filesSkipped++;
|
|
16666
|
+
continue;
|
|
16667
|
+
}
|
|
16668
|
+
bucket = { chunks: [], mtimeMs };
|
|
16669
|
+
fileChunks.set(chunk.path, bucket);
|
|
16670
|
+
}
|
|
16671
|
+
bucket.chunks.push(chunk);
|
|
16672
|
+
opts.onProgress?.({ phase: "scan", filesScanned });
|
|
16673
|
+
}
|
|
16674
|
+
const deletedPaths = [];
|
|
16675
|
+
for (const oldPath of lastMtimes.keys()) {
|
|
16676
|
+
if (!seenPaths.has(oldPath)) deletedPaths.push(oldPath);
|
|
16677
|
+
}
|
|
16678
|
+
const replacePaths = [...fileChunks.keys()].filter((p) => lastMtimes.has(p));
|
|
16679
|
+
const removed = await store.remove([...deletedPaths, ...replacePaths]);
|
|
16680
|
+
let chunksAdded = 0;
|
|
16681
|
+
let chunksSkipped = 0;
|
|
16682
|
+
const filesChanged = fileChunks.size;
|
|
16683
|
+
let chunksTotal = 0;
|
|
16684
|
+
for (const { chunks } of fileChunks.values()) chunksTotal += chunks.length;
|
|
16685
|
+
let chunksDone = 0;
|
|
16686
|
+
for (const [, bucket] of fileChunks) {
|
|
16687
|
+
if (bucket.chunks.length === 0) continue;
|
|
16688
|
+
const texts = bucket.chunks.map((c) => c.text);
|
|
16689
|
+
const vectors = await embedAll(texts, {
|
|
16690
|
+
...opts,
|
|
16691
|
+
onProgress: (done, total) => {
|
|
16692
|
+
opts.onProgress?.({
|
|
16693
|
+
phase: "embed",
|
|
16694
|
+
filesScanned,
|
|
16695
|
+
filesChanged,
|
|
16696
|
+
chunksTotal,
|
|
16697
|
+
chunksDone: chunksDone + done
|
|
16698
|
+
});
|
|
16699
|
+
if (done === total) chunksDone += total;
|
|
16700
|
+
},
|
|
16701
|
+
onError: (idx, err) => {
|
|
16702
|
+
chunksSkipped++;
|
|
16703
|
+
const c = bucket.chunks[idx];
|
|
16704
|
+
const where = c ? `${c.path}:${c.startLine}-${c.endLine}` : `chunk #${idx}`;
|
|
16705
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
16706
|
+
process.stderr.write(`
|
|
16707
|
+
! skipped ${where}: ${msg}
|
|
16708
|
+
`);
|
|
16709
|
+
}
|
|
16710
|
+
});
|
|
16711
|
+
const entries = [];
|
|
16712
|
+
for (let i = 0; i < bucket.chunks.length; i++) {
|
|
16713
|
+
const vec = vectors[i];
|
|
16714
|
+
if (!vec) continue;
|
|
16715
|
+
const c = bucket.chunks[i];
|
|
16716
|
+
if (!c) continue;
|
|
16717
|
+
normalize(vec);
|
|
16718
|
+
entries.push({
|
|
16719
|
+
path: c.path,
|
|
16720
|
+
startLine: c.startLine,
|
|
16721
|
+
endLine: c.endLine,
|
|
16722
|
+
text: c.text,
|
|
16723
|
+
embedding: vec,
|
|
16724
|
+
mtimeMs: bucket.mtimeMs
|
|
16725
|
+
});
|
|
16726
|
+
}
|
|
16727
|
+
if (entries.length > 0) await store.add(entries);
|
|
16728
|
+
chunksAdded += entries.length;
|
|
16729
|
+
}
|
|
16730
|
+
opts.onProgress?.({
|
|
16731
|
+
phase: "done",
|
|
16732
|
+
filesScanned,
|
|
16733
|
+
filesSkipped,
|
|
16734
|
+
filesChanged,
|
|
16735
|
+
chunksTotal,
|
|
16736
|
+
chunksDone
|
|
16737
|
+
});
|
|
16738
|
+
return {
|
|
16739
|
+
filesScanned,
|
|
16740
|
+
filesChanged,
|
|
16741
|
+
chunksAdded,
|
|
16742
|
+
chunksRemoved: removed,
|
|
16743
|
+
chunksSkipped,
|
|
16744
|
+
durationMs: Date.now() - t0
|
|
16745
|
+
};
|
|
16746
|
+
}
|
|
16747
|
+
async function querySemantic(root, query, opts = {}) {
|
|
16748
|
+
const indexDir = path4.join(root, INDEX_DIR_NAME);
|
|
16749
|
+
const model2 = opts.model ?? process.env.REASONIX_EMBED_MODEL ?? "nomic-embed-text";
|
|
16750
|
+
const store = await openStore(indexDir, model2);
|
|
16751
|
+
if (store.empty) return null;
|
|
16752
|
+
const qvec = await embed(query, opts);
|
|
16753
|
+
normalize(qvec);
|
|
16754
|
+
return store.search(qvec, opts.topK ?? 8, opts.minScore ?? 0.3);
|
|
16755
|
+
}
|
|
16756
|
+
async function indexExists(root) {
|
|
16757
|
+
const meta = path4.join(root, INDEX_DIR_NAME, "index.meta.json");
|
|
16758
|
+
try {
|
|
16759
|
+
await fs5.access(meta);
|
|
16760
|
+
return true;
|
|
16761
|
+
} catch {
|
|
16762
|
+
return false;
|
|
16763
|
+
}
|
|
16764
|
+
}
|
|
16765
|
+
|
|
16766
|
+
// src/index/semantic/tool.ts
|
|
16767
|
+
async function registerSemanticSearchTool(registry, opts) {
|
|
16768
|
+
if (!await indexExists(opts.root)) return false;
|
|
16769
|
+
const defaultTopK = opts.defaultTopK ?? 8;
|
|
16770
|
+
const defaultMinScore = opts.defaultMinScore ?? 0.3;
|
|
16771
|
+
registry.register({
|
|
16772
|
+
name: "semantic_search",
|
|
16773
|
+
description: "FIRST CHOICE for descriptive queries. Use this BEFORE search_content (grep) when the user describes WHAT code does ('where do we handle X', 'which file owns Y', 'how does Z work', 'find the logic that \u2026'). Returns ranked snippets ordered by semantic relevance \u2014 finds the right file even when your description shares no words with the code. Falls back to search_content / search_files only for: exact identifiers, regex patterns, or counting occurrences of a known token. If your first instinct is grep on a paraphrased question, you are wrong \u2014 try semantic_search first.",
|
|
16774
|
+
readOnly: true,
|
|
16775
|
+
parameters: {
|
|
16776
|
+
type: "object",
|
|
16777
|
+
properties: {
|
|
16778
|
+
query: {
|
|
16779
|
+
type: "string",
|
|
16780
|
+
description: "Natural-language description, phrased as a question or noun phrase: 'where do we validate the session cookie?' / 'retry backoff logic' / 'code that prevents user changes from immediately landing on disk'. Do NOT pass exact identifiers \u2014 those are search_content's job."
|
|
16781
|
+
},
|
|
16782
|
+
topK: {
|
|
16783
|
+
type: "integer",
|
|
16784
|
+
description: `Number of snippets to return (1..16). Default ${defaultTopK}.`
|
|
16785
|
+
},
|
|
16786
|
+
minScore: {
|
|
16787
|
+
type: "number",
|
|
16788
|
+
description: `Drop snippets with cosine score below this (0..1). Default ${defaultMinScore}. Raise for stricter matches; lower if the index is small.`
|
|
16789
|
+
}
|
|
16790
|
+
},
|
|
16791
|
+
required: ["query"]
|
|
16792
|
+
},
|
|
16793
|
+
fn: async (args, ctx) => {
|
|
16794
|
+
const hits = await querySemantic(opts.root, args.query, {
|
|
16795
|
+
topK: args.topK ?? defaultTopK,
|
|
16796
|
+
minScore: args.minScore ?? defaultMinScore,
|
|
16797
|
+
baseUrl: opts.baseUrl,
|
|
16798
|
+
model: opts.model,
|
|
16799
|
+
signal: ctx?.signal
|
|
16800
|
+
});
|
|
16801
|
+
if (hits === null) {
|
|
16802
|
+
return "No semantic index found for this project. Run `reasonix index` to build one.";
|
|
16803
|
+
}
|
|
16804
|
+
if (hits.length === 0) {
|
|
16805
|
+
return `query: ${args.query}
|
|
16806
|
+
|
|
16807
|
+
no matches above the score threshold (${args.minScore ?? defaultMinScore}).`;
|
|
16808
|
+
}
|
|
16809
|
+
return formatHits(args.query, hits);
|
|
16810
|
+
}
|
|
16811
|
+
});
|
|
16812
|
+
return true;
|
|
16813
|
+
}
|
|
16814
|
+
function formatHits(query, hits) {
|
|
16815
|
+
const lines = [`query: ${query}`, `
|
|
16816
|
+
results (${hits.length}):`];
|
|
16817
|
+
hits.forEach((h, i) => {
|
|
16818
|
+
const { entry, score } = h;
|
|
16819
|
+
lines.push(
|
|
16820
|
+
`
|
|
16821
|
+
${i + 1}. ${entry.path}:${entry.startLine}-${entry.endLine} (score ${score.toFixed(3)})`
|
|
16822
|
+
);
|
|
16823
|
+
const preview = entry.text.split("\n").slice(0, 8).join("\n");
|
|
16824
|
+
lines.push(indentBlock(preview, " "));
|
|
16825
|
+
if (entry.text.split("\n").length > 8) {
|
|
16826
|
+
lines.push(
|
|
16827
|
+
` \u2026(${entry.text.split("\n").length - 8} more lines \u2014 read_file ${entry.path}:${entry.startLine} for the full chunk)`
|
|
16828
|
+
);
|
|
16829
|
+
}
|
|
16830
|
+
});
|
|
16831
|
+
return lines.join("\n");
|
|
16832
|
+
}
|
|
16833
|
+
function indentBlock(text, prefix) {
|
|
16834
|
+
return text.split("\n").map((l) => prefix + l).join("\n");
|
|
16835
|
+
}
|
|
16836
|
+
async function bootstrapSemanticSearchInCodeMode(registry, rootDir, opts = {}) {
|
|
16837
|
+
if (await indexExists(rootDir)) {
|
|
16838
|
+
await registerSemanticSearchTool(registry, { ...opts, root: rootDir });
|
|
16839
|
+
return { enabled: true };
|
|
16840
|
+
}
|
|
16841
|
+
return { enabled: false };
|
|
16842
|
+
}
|
|
16843
|
+
|
|
16844
|
+
// src/cli/commands/code.tsx
|
|
15334
16845
|
async function codeCommand(opts = {}) {
|
|
15335
|
-
const { codeSystemPrompt: codeSystemPrompt2 } = await import("./prompt-
|
|
16846
|
+
const { codeSystemPrompt: codeSystemPrompt2 } = await import("./prompt-FMYQ7IDW.js");
|
|
15336
16847
|
const rootDir = resolve7(opts.dir ?? process.cwd());
|
|
15337
16848
|
const session = opts.noSession ? void 0 : `code-${sanitizeName(basename2(rootDir))}`;
|
|
15338
16849
|
const tools = new ToolRegistry();
|
|
@@ -15346,13 +16857,18 @@ async function codeCommand(opts = {}) {
|
|
|
15346
16857
|
// via ShellConfirm mid-session takes effect on the next shell call
|
|
15347
16858
|
// instead of waiting for `/new` or a relaunch.
|
|
15348
16859
|
extraAllowed: () => loadProjectShellAllowed(rootDir),
|
|
16860
|
+
// `yolo` edit-mode disables shell confirmations entirely. Re-read
|
|
16861
|
+
// from config on each dispatch so /mode yolo (or Shift+Tab cycling
|
|
16862
|
+
// through to it) flips the gate live without forcing a relaunch.
|
|
16863
|
+
allowAll: () => loadEditMode() === "yolo",
|
|
15349
16864
|
jobs: jobs2
|
|
15350
16865
|
});
|
|
15351
16866
|
registerPlanTool(tools);
|
|
15352
16867
|
registerChoiceTool(tools);
|
|
15353
16868
|
registerMemoryTools(tools, { projectRoot: rootDir });
|
|
16869
|
+
const semantic2 = await bootstrapSemanticSearchInCodeMode(tools, rootDir);
|
|
15354
16870
|
process.stderr.write(
|
|
15355
|
-
`\u25B8 reasonix code: rooted at ${rootDir}, session "${session ?? "(ephemeral)"}" \xB7 ${tools.size} native tool(s)
|
|
16871
|
+
`\u25B8 reasonix code: rooted at ${rootDir}, session "${session ?? "(ephemeral)"}" \xB7 ${tools.size} native tool(s)${semantic2.enabled ? " \xB7 semantic_search on" : ""}
|
|
15356
16872
|
`
|
|
15357
16873
|
);
|
|
15358
16874
|
process.once("exit", () => {
|
|
@@ -15361,7 +16877,7 @@ async function codeCommand(opts = {}) {
|
|
|
15361
16877
|
await chatCommand({
|
|
15362
16878
|
model: opts.model ?? "deepseek-v4-flash",
|
|
15363
16879
|
harvest: opts.harvest ?? false,
|
|
15364
|
-
system: codeSystemPrompt2(rootDir),
|
|
16880
|
+
system: codeSystemPrompt2(rootDir, { hasSemanticSearch: semantic2.enabled }),
|
|
15365
16881
|
transcript: opts.transcript,
|
|
15366
16882
|
session,
|
|
15367
16883
|
seedTools: tools,
|
|
@@ -15378,32 +16894,32 @@ import { render as render2 } from "ink";
|
|
|
15378
16894
|
import React29 from "react";
|
|
15379
16895
|
|
|
15380
16896
|
// src/cli/ui/DiffApp.tsx
|
|
15381
|
-
import { Box as Box25, Static as Static2, Text as
|
|
16897
|
+
import { Box as Box25, Static as Static2, Text as Text23, useApp as useApp3, useInput } from "ink";
|
|
15382
16898
|
import React28, { useState as useState13 } from "react";
|
|
15383
16899
|
|
|
15384
16900
|
// src/cli/ui/RecordView.tsx
|
|
15385
|
-
import { Box as Box24, Text as
|
|
16901
|
+
import { Box as Box24, Text as Text22 } from "ink";
|
|
15386
16902
|
import React27 from "react";
|
|
15387
16903
|
function RecordView({ rec, compact: compact2 = false }) {
|
|
15388
16904
|
const toolArgsMax = compact2 ? 120 : 200;
|
|
15389
16905
|
const toolContentMax = compact2 ? 200 : 400;
|
|
15390
16906
|
if (rec.role === "user") {
|
|
15391
16907
|
const content = rec.content.includes("\n") ? rec.content.split("\n").join("\n ") : rec.content;
|
|
15392
|
-
return /* @__PURE__ */ React27.createElement(Box24, { marginTop: 1 }, /* @__PURE__ */ React27.createElement(
|
|
16908
|
+
return /* @__PURE__ */ React27.createElement(Box24, { marginTop: 1 }, /* @__PURE__ */ React27.createElement(Text22, { bold: true, color: "cyan" }, "you \u203A", " "), /* @__PURE__ */ React27.createElement(Text22, null, content));
|
|
15393
16909
|
}
|
|
15394
16910
|
if (rec.role === "assistant_final") {
|
|
15395
|
-
return /* @__PURE__ */ React27.createElement(Box24, { flexDirection: "column", marginTop: 1 }, /* @__PURE__ */ React27.createElement(Box24, null, /* @__PURE__ */ React27.createElement(
|
|
16911
|
+
return /* @__PURE__ */ React27.createElement(Box24, { flexDirection: "column", marginTop: 1 }, /* @__PURE__ */ React27.createElement(Box24, null, /* @__PURE__ */ React27.createElement(Text22, { bold: true, color: "green" }, "assistant"), rec.cost !== void 0 ? /* @__PURE__ */ React27.createElement(Text22, { dimColor: true }, " $", rec.cost.toFixed(6)) : null, rec.usage ? /* @__PURE__ */ React27.createElement(CacheBadge, { usage: rec.usage }) : null), rec.planState ? /* @__PURE__ */ React27.createElement(PlanStateBlock, { planState: rec.planState }) : null, rec.content ? /* @__PURE__ */ React27.createElement(Text22, null, rec.content) : /* @__PURE__ */ React27.createElement(Text22, { dimColor: true, italic: true }, "(tool-call response only)"));
|
|
15396
16912
|
}
|
|
15397
16913
|
if (rec.role === "tool") {
|
|
15398
|
-
return /* @__PURE__ */ React27.createElement(Box24, { flexDirection: "column", marginTop: 1 }, /* @__PURE__ */ React27.createElement(
|
|
16914
|
+
return /* @__PURE__ */ React27.createElement(Box24, { flexDirection: "column", marginTop: 1 }, /* @__PURE__ */ React27.createElement(Text22, { color: "yellow" }, "tool<", rec.tool ?? "?", ">"), rec.args ? /* @__PURE__ */ React27.createElement(Text22, { dimColor: true }, " args: ", truncate2(rec.args, toolArgsMax)) : null, /* @__PURE__ */ React27.createElement(Text22, { dimColor: true }, " \u2192 ", truncate2(rec.content, toolContentMax)));
|
|
15399
16915
|
}
|
|
15400
16916
|
if (rec.role === "error") {
|
|
15401
|
-
return /* @__PURE__ */ React27.createElement(Box24, { marginTop: 1 }, /* @__PURE__ */ React27.createElement(
|
|
16917
|
+
return /* @__PURE__ */ React27.createElement(Box24, { marginTop: 1 }, /* @__PURE__ */ React27.createElement(Text22, { color: "red", bold: true }, "error", " "), /* @__PURE__ */ React27.createElement(Text22, { color: "red" }, rec.error ?? rec.content));
|
|
15402
16918
|
}
|
|
15403
16919
|
if (rec.role === "done" || rec.role === "assistant_delta") {
|
|
15404
16920
|
return null;
|
|
15405
16921
|
}
|
|
15406
|
-
return /* @__PURE__ */ React27.createElement(Box24, null, /* @__PURE__ */ React27.createElement(
|
|
16922
|
+
return /* @__PURE__ */ React27.createElement(Box24, null, /* @__PURE__ */ React27.createElement(Text22, { dimColor: true }, "[", rec.role, "] ", rec.content));
|
|
15407
16923
|
}
|
|
15408
16924
|
function CacheBadge({ usage }) {
|
|
15409
16925
|
const hit = usage.prompt_cache_hit_tokens ?? 0;
|
|
@@ -15412,7 +16928,7 @@ function CacheBadge({ usage }) {
|
|
|
15412
16928
|
if (total === 0) return null;
|
|
15413
16929
|
const pct2 = hit / total * 100;
|
|
15414
16930
|
const color = pct2 >= 70 ? "green" : pct2 >= 40 ? "yellow" : "red";
|
|
15415
|
-
return /* @__PURE__ */ React27.createElement(
|
|
16931
|
+
return /* @__PURE__ */ React27.createElement(Text22, null, /* @__PURE__ */ React27.createElement(Text22, { dimColor: true }, " \xB7 cache "), /* @__PURE__ */ React27.createElement(Text22, { color }, pct2.toFixed(1), "%"));
|
|
15416
16932
|
}
|
|
15417
16933
|
function truncate2(s, max) {
|
|
15418
16934
|
return s.length <= max ? s : `${s.slice(0, max)}\u2026 (+${s.length - max} chars)`;
|
|
@@ -15446,7 +16962,7 @@ function DiffApp({ report }) {
|
|
|
15446
16962
|
}
|
|
15447
16963
|
});
|
|
15448
16964
|
const pair = report.pairs[idx];
|
|
15449
|
-
return /* @__PURE__ */ React28.createElement(Box25, { flexDirection: "column" }, /* @__PURE__ */ React28.createElement(DiffHeader, { report }), /* @__PURE__ */ React28.createElement(Box25, { marginTop: 1, paddingX: 1, justifyContent: "space-between" }, /* @__PURE__ */ React28.createElement(
|
|
16965
|
+
return /* @__PURE__ */ React28.createElement(Box25, { flexDirection: "column" }, /* @__PURE__ */ React28.createElement(DiffHeader, { report }), /* @__PURE__ */ React28.createElement(Box25, { marginTop: 1, paddingX: 1, justifyContent: "space-between" }, /* @__PURE__ */ React28.createElement(Text23, { color: "cyan", bold: true }, "turn ", pair?.turn ?? "?", " (", idx + 1, " / ", report.pairs.length, ")"), /* @__PURE__ */ React28.createElement(Text23, null, pair ? /* @__PURE__ */ React28.createElement(KindBadge, { kind: pair.kind }) : null)), /* @__PURE__ */ React28.createElement(Box25, { flexDirection: "row", marginTop: 1 }, /* @__PURE__ */ React28.createElement(Pane, { label: report.a.label, headerColor: "blue", records: paneRecords(pair, "a") }), /* @__PURE__ */ React28.createElement(Pane, { label: report.b.label, headerColor: "magenta", records: paneRecords(pair, "b") })), pair?.divergenceNote ? /* @__PURE__ */ React28.createElement(Box25, { marginTop: 1, paddingX: 1 }, /* @__PURE__ */ React28.createElement(Text23, { color: "yellow" }, "\u2605 "), /* @__PURE__ */ React28.createElement(Text23, null, pair.divergenceNote)) : null, /* @__PURE__ */ React28.createElement(Box25, { marginTop: 1, paddingX: 1, borderStyle: "single", borderColor: "gray" }, /* @__PURE__ */ React28.createElement(Text23, { dimColor: true }, /* @__PURE__ */ React28.createElement(Text23, { bold: true }, "j"), "/", /* @__PURE__ */ React28.createElement(Text23, { bold: true }, "\u2193"), " next \xB7 ", /* @__PURE__ */ React28.createElement(Text23, { bold: true }, "k"), "/", /* @__PURE__ */ React28.createElement(Text23, { bold: true }, "\u2191"), " ", "prev \xB7 ", /* @__PURE__ */ React28.createElement(Text23, { bold: true }, "n"), " next-diverge \xB7 ", /* @__PURE__ */ React28.createElement(Text23, { bold: true }, "N"), "/", /* @__PURE__ */ React28.createElement(Text23, { bold: true }, "p"), " ", "prev-diverge \xB7 ", /* @__PURE__ */ React28.createElement(Text23, { bold: true }, "g"), "/", /* @__PURE__ */ React28.createElement(Text23, { bold: true }, "G"), " first/last \xB7 ", /* @__PURE__ */ React28.createElement(Text23, { bold: true }, "q"), " ", "quit")));
|
|
15450
16966
|
}
|
|
15451
16967
|
function DiffHeader({ report }) {
|
|
15452
16968
|
const a = report.a;
|
|
@@ -15464,7 +16980,7 @@ function DiffHeader({ report }) {
|
|
|
15464
16980
|
} else if (a.stats.prefixHashes[0] && a.stats.prefixHashes[0] === b.stats.prefixHashes[0]) {
|
|
15465
16981
|
prefixLine = `shared prefix hash ${a.stats.prefixHashes[0].slice(0, 12)}\u2026 \u2014 cache delta attributable to log stability, not prompt change.`;
|
|
15466
16982
|
}
|
|
15467
|
-
return /* @__PURE__ */ React28.createElement(Box25, { flexDirection: "column", borderStyle: "round", borderColor: "cyan", paddingX: 1 }, /* @__PURE__ */ React28.createElement(Box25, { justifyContent: "space-between" }, /* @__PURE__ */ React28.createElement(
|
|
16983
|
+
return /* @__PURE__ */ React28.createElement(Box25, { flexDirection: "column", borderStyle: "round", borderColor: "cyan", paddingX: 1 }, /* @__PURE__ */ React28.createElement(Box25, { justifyContent: "space-between" }, /* @__PURE__ */ React28.createElement(Text23, null, /* @__PURE__ */ React28.createElement(Text23, { color: "cyan", bold: true }, "reasonix diff"), /* @__PURE__ */ React28.createElement(Text23, { dimColor: true }, " \xB7 A="), /* @__PURE__ */ React28.createElement(Text23, { color: "blue" }, a.label), /* @__PURE__ */ React28.createElement(Text23, { dimColor: true }, " vs B="), /* @__PURE__ */ React28.createElement(Text23, { color: "magenta" }, b.label)), /* @__PURE__ */ React28.createElement(Text23, { dimColor: true }, report.pairs.length, " turns aligned")), /* @__PURE__ */ React28.createElement(Box25, { marginTop: 1, gap: 3 }, /* @__PURE__ */ React28.createElement(Text23, null, /* @__PURE__ */ React28.createElement(Text23, { dimColor: true }, "cache "), /* @__PURE__ */ React28.createElement(Text23, null, (a.stats.cacheHitRatio * 100).toFixed(1), "%"), /* @__PURE__ */ React28.createElement(Text23, { dimColor: true }, " \u2192 "), /* @__PURE__ */ React28.createElement(Text23, null, (b.stats.cacheHitRatio * 100).toFixed(1), "%"), /* @__PURE__ */ React28.createElement(Text23, { color: cacheDelta >= 0 ? "green" : "red", bold: true }, " ", cacheDelta >= 0 ? "+" : "", (cacheDelta * 100).toFixed(1), "pp")), /* @__PURE__ */ React28.createElement(Text23, null, /* @__PURE__ */ React28.createElement(Text23, { dimColor: true }, "cost "), /* @__PURE__ */ React28.createElement(Text23, null, "$", a.stats.totalCostUsd.toFixed(6)), /* @__PURE__ */ React28.createElement(Text23, { dimColor: true }, " \u2192 "), /* @__PURE__ */ React28.createElement(Text23, null, "$", b.stats.totalCostUsd.toFixed(6)), /* @__PURE__ */ React28.createElement(Text23, { color: costDelta2 <= 0 ? "green" : "red", bold: true }, " ", costDelta2 >= 0 ? "+" : "", costDelta2.toFixed(1), "%")), /* @__PURE__ */ React28.createElement(Text23, null, /* @__PURE__ */ React28.createElement(Text23, { dimColor: true }, "model calls "), /* @__PURE__ */ React28.createElement(Text23, null, a.stats.turns, " \u2192 ", b.stats.turns))), prefixLine ? /* @__PURE__ */ React28.createElement(Box25, { marginTop: 1 }, /* @__PURE__ */ React28.createElement(Text23, { dimColor: true, italic: true }, prefixLine)) : null);
|
|
15468
16984
|
}
|
|
15469
16985
|
function Pane({
|
|
15470
16986
|
label,
|
|
@@ -15480,21 +16996,21 @@ function Pane({
|
|
|
15480
16996
|
borderStyle: "single",
|
|
15481
16997
|
borderColor: headerColor
|
|
15482
16998
|
},
|
|
15483
|
-
/* @__PURE__ */ React28.createElement(
|
|
15484
|
-
records.length === 0 ? /* @__PURE__ */ React28.createElement(Box25, { marginTop: 1 }, /* @__PURE__ */ React28.createElement(
|
|
16999
|
+
/* @__PURE__ */ React28.createElement(Text23, { color: headerColor, bold: true }, label),
|
|
17000
|
+
records.length === 0 ? /* @__PURE__ */ React28.createElement(Box25, { marginTop: 1 }, /* @__PURE__ */ React28.createElement(Text23, { dimColor: true, italic: true }, "(no records on this side for this turn)")) : /* @__PURE__ */ React28.createElement(Static2, { items: records.map((rec, i) => ({ key: `${label}-${i}`, rec })) }, ({ key, rec }) => /* @__PURE__ */ React28.createElement(RecordView, { key, rec, compact: true }))
|
|
15485
17001
|
);
|
|
15486
17002
|
}
|
|
15487
17003
|
function KindBadge({ kind }) {
|
|
15488
17004
|
if (kind === "match") {
|
|
15489
|
-
return /* @__PURE__ */ React28.createElement(
|
|
17005
|
+
return /* @__PURE__ */ React28.createElement(Text23, { color: "green" }, "\u2713 match");
|
|
15490
17006
|
}
|
|
15491
17007
|
if (kind === "diverge") {
|
|
15492
|
-
return /* @__PURE__ */ React28.createElement(
|
|
17008
|
+
return /* @__PURE__ */ React28.createElement(Text23, { color: "yellow" }, "\u2605 diverge");
|
|
15493
17009
|
}
|
|
15494
17010
|
if (kind === "only_in_a") {
|
|
15495
|
-
return /* @__PURE__ */ React28.createElement(
|
|
17011
|
+
return /* @__PURE__ */ React28.createElement(Text23, { color: "blue" }, "\u2190 only in A");
|
|
15496
17012
|
}
|
|
15497
|
-
return /* @__PURE__ */ React28.createElement(
|
|
17013
|
+
return /* @__PURE__ */ React28.createElement(Text23, { color: "magenta" }, "\u2192 only in B");
|
|
15498
17014
|
}
|
|
15499
17015
|
function paneRecords(pair, side) {
|
|
15500
17016
|
if (!pair) return [];
|
|
@@ -15535,6 +17051,200 @@ markdown report written to ${opts.mdPath}`);
|
|
|
15535
17051
|
console.log(renderSummaryTable(report));
|
|
15536
17052
|
}
|
|
15537
17053
|
|
|
17054
|
+
// src/cli/commands/index.ts
|
|
17055
|
+
import { resolve as resolve8 } from "path";
|
|
17056
|
+
|
|
17057
|
+
// src/index/semantic/preflight.ts
|
|
17058
|
+
import { stdin as stdin2, stdout } from "process";
|
|
17059
|
+
import { createInterface } from "readline/promises";
|
|
17060
|
+
async function ollamaPreflight(opts) {
|
|
17061
|
+
const log = opts.log ?? ((line) => process.stderr.write(line));
|
|
17062
|
+
const status2 = await checkOllamaStatus(opts.model, opts.baseUrl);
|
|
17063
|
+
if (!status2.binaryFound) {
|
|
17064
|
+
log(t("ollamaNotFound"));
|
|
17065
|
+
return false;
|
|
17066
|
+
}
|
|
17067
|
+
if (!status2.daemonRunning) {
|
|
17068
|
+
if (!opts.interactive && !opts.yesToAll) {
|
|
17069
|
+
log(t("daemonNotReachableHint"));
|
|
17070
|
+
return false;
|
|
17071
|
+
}
|
|
17072
|
+
const ok = opts.yesToAll || await confirm(t("daemonStartConfirm"), true);
|
|
17073
|
+
if (!ok) {
|
|
17074
|
+
log(t("daemonAbortStart"));
|
|
17075
|
+
return false;
|
|
17076
|
+
}
|
|
17077
|
+
log(t("daemonStarting"));
|
|
17078
|
+
const started = await startOllamaDaemon({ baseUrl: opts.baseUrl, timeoutMs: 15e3 });
|
|
17079
|
+
if (!started.ready) {
|
|
17080
|
+
log(t("daemonStartTimeout"));
|
|
17081
|
+
return false;
|
|
17082
|
+
}
|
|
17083
|
+
log(t("daemonReady", { pid: started.pid ? ` (pid ${started.pid})` : "" }));
|
|
17084
|
+
}
|
|
17085
|
+
const after = status2.daemonRunning ? status2 : await checkOllamaStatus(opts.model, opts.baseUrl);
|
|
17086
|
+
if (!after.modelPulled) {
|
|
17087
|
+
if (!opts.interactive && !opts.yesToAll) {
|
|
17088
|
+
log(t("modelNotPulledHint", { model: opts.model }));
|
|
17089
|
+
return false;
|
|
17090
|
+
}
|
|
17091
|
+
const ok = opts.yesToAll || await confirm(t("modelPullConfirm", { model: opts.model }), true);
|
|
17092
|
+
if (!ok) {
|
|
17093
|
+
log(t("modelAbortPull"));
|
|
17094
|
+
return false;
|
|
17095
|
+
}
|
|
17096
|
+
log(t("modelPulling", { model: opts.model }));
|
|
17097
|
+
const ESC = String.fromCharCode(27);
|
|
17098
|
+
const ANSI_CSI = new RegExp(`${ESC}\\[[0-9;]*[A-Za-z]`, "g");
|
|
17099
|
+
const code = await pullOllamaModel(opts.model, {
|
|
17100
|
+
onLine: (line) => {
|
|
17101
|
+
const cleaned = line.replace(ANSI_CSI, "").trim();
|
|
17102
|
+
if (cleaned.length === 0) return;
|
|
17103
|
+
log(` ${cleaned}
|
|
17104
|
+
`);
|
|
17105
|
+
}
|
|
17106
|
+
});
|
|
17107
|
+
if (code !== 0) {
|
|
17108
|
+
log(t("modelPullFailed", { model: opts.model, code }));
|
|
17109
|
+
return false;
|
|
17110
|
+
}
|
|
17111
|
+
log(t("modelPulled", { model: opts.model }));
|
|
17112
|
+
}
|
|
17113
|
+
return true;
|
|
17114
|
+
}
|
|
17115
|
+
async function confirm(question, defaultYes) {
|
|
17116
|
+
const suffix = defaultYes ? "[Y/n]" : "[y/N]";
|
|
17117
|
+
const rl = createInterface({ input: stdin2, output: stdout });
|
|
17118
|
+
try {
|
|
17119
|
+
const raw = (await rl.question(`${question} ${suffix} `)).trim().toLowerCase();
|
|
17120
|
+
if (raw === "") return defaultYes;
|
|
17121
|
+
return raw === "y" || raw === "yes";
|
|
17122
|
+
} finally {
|
|
17123
|
+
rl.close();
|
|
17124
|
+
}
|
|
17125
|
+
}
|
|
17126
|
+
|
|
17127
|
+
// src/cli/commands/index.ts
|
|
17128
|
+
async function indexCommand(opts = {}) {
|
|
17129
|
+
const root = resolve8(opts.dir ?? process.cwd());
|
|
17130
|
+
const tty = process.stderr.isTTY === true && process.stdin.isTTY === true;
|
|
17131
|
+
const model2 = opts.model ?? process.env.REASONIX_EMBED_MODEL ?? "nomic-embed-text";
|
|
17132
|
+
const preflightOk = await ollamaPreflight({
|
|
17133
|
+
model: model2,
|
|
17134
|
+
baseUrl: opts.ollamaUrl,
|
|
17135
|
+
interactive: tty && !opts.yes,
|
|
17136
|
+
yesToAll: opts.yes ?? false
|
|
17137
|
+
});
|
|
17138
|
+
if (!preflightOk) process.exit(1);
|
|
17139
|
+
const writer = makeProgressWriter(tty);
|
|
17140
|
+
const t0 = Date.now();
|
|
17141
|
+
let result;
|
|
17142
|
+
try {
|
|
17143
|
+
result = await buildIndex(root, {
|
|
17144
|
+
rebuild: opts.rebuild,
|
|
17145
|
+
model: model2,
|
|
17146
|
+
baseUrl: opts.ollamaUrl,
|
|
17147
|
+
onProgress: (p) => writer.update(p)
|
|
17148
|
+
});
|
|
17149
|
+
} catch (err) {
|
|
17150
|
+
writer.clear();
|
|
17151
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
17152
|
+
process.stderr.write(t("indexFailed", { msg }));
|
|
17153
|
+
process.exit(1);
|
|
17154
|
+
}
|
|
17155
|
+
writer.clear();
|
|
17156
|
+
const seconds = ((Date.now() - t0) / 1e3).toFixed(1);
|
|
17157
|
+
const successKey = result.chunksSkipped > 0 ? "indexSuccessWithSkips" : "indexSuccess";
|
|
17158
|
+
process.stderr.write(
|
|
17159
|
+
t(successKey, {
|
|
17160
|
+
scanned: result.filesScanned,
|
|
17161
|
+
changed: result.filesChanged,
|
|
17162
|
+
added: result.chunksAdded,
|
|
17163
|
+
removed: result.chunksRemoved,
|
|
17164
|
+
skipped: result.chunksSkipped,
|
|
17165
|
+
seconds
|
|
17166
|
+
})
|
|
17167
|
+
);
|
|
17168
|
+
if (result.filesChanged === 0 && !opts.rebuild) {
|
|
17169
|
+
process.stderr.write(t("indexNothingToDo"));
|
|
17170
|
+
}
|
|
17171
|
+
}
|
|
17172
|
+
var SPINNER_FRAMES2 = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
|
|
17173
|
+
var SPINNER_INTERVAL_MS = 120;
|
|
17174
|
+
function makeProgressWriter(tty) {
|
|
17175
|
+
if (!tty) return makeNonTtyWriter();
|
|
17176
|
+
return makeTtyWriter();
|
|
17177
|
+
}
|
|
17178
|
+
function makeNonTtyWriter() {
|
|
17179
|
+
let lastPhase = null;
|
|
17180
|
+
let lastChunks = 0;
|
|
17181
|
+
return {
|
|
17182
|
+
update(p) {
|
|
17183
|
+
if (p.phase !== lastPhase) {
|
|
17184
|
+
lastPhase = p.phase;
|
|
17185
|
+
if (p.phase === "scan") {
|
|
17186
|
+
process.stderr.write(t("progressScanLine"));
|
|
17187
|
+
} else if (p.phase === "embed") {
|
|
17188
|
+
process.stderr.write(
|
|
17189
|
+
t("progressEmbedLine", {
|
|
17190
|
+
total: p.chunksTotal ?? 0,
|
|
17191
|
+
files: p.filesChanged ?? 0
|
|
17192
|
+
})
|
|
17193
|
+
);
|
|
17194
|
+
}
|
|
17195
|
+
}
|
|
17196
|
+
if (p.phase === "embed" && p.chunksDone !== void 0 && p.chunksDone - lastChunks >= 50) {
|
|
17197
|
+
lastChunks = p.chunksDone;
|
|
17198
|
+
process.stderr.write(
|
|
17199
|
+
t("progressEmbedHeartbeat", {
|
|
17200
|
+
done: p.chunksDone,
|
|
17201
|
+
total: p.chunksTotal ?? "?"
|
|
17202
|
+
})
|
|
17203
|
+
);
|
|
17204
|
+
}
|
|
17205
|
+
},
|
|
17206
|
+
clear() {
|
|
17207
|
+
}
|
|
17208
|
+
};
|
|
17209
|
+
}
|
|
17210
|
+
function makeTtyWriter() {
|
|
17211
|
+
let status2 = t("progressStarting");
|
|
17212
|
+
let lastLineLen = 0;
|
|
17213
|
+
let frameIdx = 0;
|
|
17214
|
+
const startTs = Date.now();
|
|
17215
|
+
const repaint = () => {
|
|
17216
|
+
const frame = SPINNER_FRAMES2[frameIdx % SPINNER_FRAMES2.length];
|
|
17217
|
+
frameIdx++;
|
|
17218
|
+
const elapsed = ((Date.now() - startTs) / 1e3).toFixed(1);
|
|
17219
|
+
const line = `${frame} ${status2} ${elapsed}s`;
|
|
17220
|
+
const padded = line + " ".repeat(Math.max(0, lastLineLen - line.length));
|
|
17221
|
+
process.stderr.write(`\r${padded}`);
|
|
17222
|
+
lastLineLen = line.length;
|
|
17223
|
+
};
|
|
17224
|
+
repaint();
|
|
17225
|
+
const interval = setInterval(repaint, SPINNER_INTERVAL_MS);
|
|
17226
|
+
return {
|
|
17227
|
+
update(p) {
|
|
17228
|
+
if (p.phase === "scan") {
|
|
17229
|
+
status2 = t("progressScan", { files: p.filesScanned ?? 0 });
|
|
17230
|
+
} else if (p.phase === "embed") {
|
|
17231
|
+
const done = p.chunksDone ?? 0;
|
|
17232
|
+
const total = p.chunksTotal ?? 0;
|
|
17233
|
+
const pct2 = total > 0 ? (done / total * 100).toFixed(0) : "0";
|
|
17234
|
+
status2 = t("progressEmbed", { done, total, pct: pct2 });
|
|
17235
|
+
}
|
|
17236
|
+
repaint();
|
|
17237
|
+
},
|
|
17238
|
+
clear() {
|
|
17239
|
+
clearInterval(interval);
|
|
17240
|
+
if (lastLineLen > 0) {
|
|
17241
|
+
process.stderr.write(`\r${" ".repeat(lastLineLen)}\r`);
|
|
17242
|
+
lastLineLen = 0;
|
|
17243
|
+
}
|
|
17244
|
+
}
|
|
17245
|
+
};
|
|
17246
|
+
}
|
|
17247
|
+
|
|
15538
17248
|
// src/cli/commands/mcp-inspect.ts
|
|
15539
17249
|
async function mcpInspectCommand(opts) {
|
|
15540
17250
|
const spec = parseMcpSpec(opts.spec);
|
|
@@ -15581,9 +17291,9 @@ function formatSection(title, section, render5) {
|
|
|
15581
17291
|
for (const item of section.items) lines.push(` ${render5(item)}`);
|
|
15582
17292
|
return lines.join("\n");
|
|
15583
17293
|
}
|
|
15584
|
-
function toolLine(
|
|
15585
|
-
const desc =
|
|
15586
|
-
return `\xB7 ${
|
|
17294
|
+
function toolLine(t2) {
|
|
17295
|
+
const desc = t2.description ? ` \u2014 ${oneLine(t2.description, 80)}` : "";
|
|
17296
|
+
return `\xB7 ${t2.name}${desc}`;
|
|
15587
17297
|
}
|
|
15588
17298
|
function resourceLine(r) {
|
|
15589
17299
|
const mime = r.mimeType ? ` [${r.mimeType}]` : "";
|
|
@@ -15669,7 +17379,7 @@ import { render as render3 } from "ink";
|
|
|
15669
17379
|
import React31 from "react";
|
|
15670
17380
|
|
|
15671
17381
|
// src/cli/ui/ReplayApp.tsx
|
|
15672
|
-
import { Box as Box26, Static as Static3, Text as
|
|
17382
|
+
import { Box as Box26, Static as Static3, Text as Text24, useApp as useApp4, useInput as useInput2 } from "ink";
|
|
15673
17383
|
import React30, { useMemo as useMemo4, useState as useState14 } from "react";
|
|
15674
17384
|
function ReplayApp({ meta, pages }) {
|
|
15675
17385
|
const { exit: exit2 } = useApp4();
|
|
@@ -15717,7 +17427,7 @@ function ReplayApp({ meta, pages }) {
|
|
|
15717
17427
|
model: cumStats.models[0] ?? meta?.model ?? "?",
|
|
15718
17428
|
prefixHash
|
|
15719
17429
|
}
|
|
15720
|
-
), /* @__PURE__ */ React30.createElement(Box26, { flexDirection: "column", marginTop: 1, paddingX: 1 }, /* @__PURE__ */ React30.createElement(Box26, { justifyContent: "space-between" }, /* @__PURE__ */ React30.createElement(
|
|
17430
|
+
), /* @__PURE__ */ React30.createElement(Box26, { flexDirection: "column", marginTop: 1, paddingX: 1 }, /* @__PURE__ */ React30.createElement(Box26, { justifyContent: "space-between" }, /* @__PURE__ */ React30.createElement(Text24, { color: "cyan", bold: true }, progressLabel), meta ? /* @__PURE__ */ React30.createElement(Text24, { dimColor: true }, meta.source, meta.task ? ` \xB7 ${meta.task}` : "", meta.mode ? ` \xB7 ${meta.mode}` : "") : null), currentPage ? /* @__PURE__ */ React30.createElement(Static3, { items: currentPage.records.map((rec, i) => ({ key: `${idx}-${i}`, rec })) }, ({ key, rec }) => /* @__PURE__ */ React30.createElement(RecordView, { key, rec })) : /* @__PURE__ */ React30.createElement(Text24, { dimColor: true, italic: true }, "no records")), /* @__PURE__ */ React30.createElement(Box26, { marginTop: 1, paddingX: 1, borderStyle: "single", borderColor: "gray" }, /* @__PURE__ */ React30.createElement(Text24, { dimColor: true }, /* @__PURE__ */ React30.createElement(Text24, { bold: true }, "j"), "/", /* @__PURE__ */ React30.createElement(Text24, { bold: true }, "\u2193"), "/", /* @__PURE__ */ React30.createElement(Text24, { bold: true }, "space"), " next \xB7 ", /* @__PURE__ */ React30.createElement(Text24, { bold: true }, "k"), "/", /* @__PURE__ */ React30.createElement(Text24, { bold: true }, "\u2191"), " prev \xB7 ", /* @__PURE__ */ React30.createElement(Text24, { bold: true }, "g"), " first \xB7 ", /* @__PURE__ */ React30.createElement(Text24, { bold: true }, "G"), " last \xB7", " ", /* @__PURE__ */ React30.createElement(Text24, { bold: true }, "q"), " quit")));
|
|
15721
17431
|
}
|
|
15722
17432
|
|
|
15723
17433
|
// src/cli/commands/replay.ts
|
|
@@ -15823,12 +17533,12 @@ function oneLine2(s, max = 200) {
|
|
|
15823
17533
|
}
|
|
15824
17534
|
|
|
15825
17535
|
// src/cli/commands/run.ts
|
|
15826
|
-
import { stdin as
|
|
15827
|
-
import { createInterface } from "readline/promises";
|
|
17536
|
+
import { stdin as stdin3, stdout as stdout2 } from "process";
|
|
17537
|
+
import { createInterface as createInterface2 } from "readline/promises";
|
|
15828
17538
|
async function ensureApiKey() {
|
|
15829
17539
|
const existing = loadApiKey();
|
|
15830
17540
|
if (existing) return existing;
|
|
15831
|
-
if (!
|
|
17541
|
+
if (!stdin3.isTTY) {
|
|
15832
17542
|
process.stderr.write(
|
|
15833
17543
|
"DEEPSEEK_API_KEY is not set and stdin is not a TTY (cannot prompt).\nSet the env var, or run `reasonix chat` once interactively to save a key.\n"
|
|
15834
17544
|
);
|
|
@@ -15837,7 +17547,7 @@ async function ensureApiKey() {
|
|
|
15837
17547
|
process.stdout.write(
|
|
15838
17548
|
"DeepSeek API key not configured.\nGet one at https://platform.deepseek.com/api_keys\n"
|
|
15839
17549
|
);
|
|
15840
|
-
const rl =
|
|
17550
|
+
const rl = createInterface2({ input: stdin3, output: stdout2 });
|
|
15841
17551
|
try {
|
|
15842
17552
|
while (true) {
|
|
15843
17553
|
const answer = (await rl.question("API key \u203A ")).trim();
|
|
@@ -15896,7 +17606,7 @@ async function runCommand2(opts) {
|
|
|
15896
17606
|
system: opts.system,
|
|
15897
17607
|
toolSpecs: tools?.specs()
|
|
15898
17608
|
});
|
|
15899
|
-
const
|
|
17609
|
+
const loop2 = new CacheFirstLoop({
|
|
15900
17610
|
client,
|
|
15901
17611
|
prefix,
|
|
15902
17612
|
tools,
|
|
@@ -15921,7 +17631,7 @@ async function runCommand2(opts) {
|
|
|
15921
17631
|
});
|
|
15922
17632
|
}
|
|
15923
17633
|
try {
|
|
15924
|
-
for await (const ev of
|
|
17634
|
+
for await (const ev of loop2.step(opts.task)) {
|
|
15925
17635
|
if (ev.role === "assistant_delta" && ev.content) process.stdout.write(ev.content);
|
|
15926
17636
|
if (ev.role === "tool") process.stdout.write(`
|
|
15927
17637
|
[tool ${ev.toolName}] ${ev.content}
|
|
@@ -15940,7 +17650,7 @@ async function runCommand2(opts) {
|
|
|
15940
17650
|
} finally {
|
|
15941
17651
|
transcriptStream?.end();
|
|
15942
17652
|
}
|
|
15943
|
-
const s =
|
|
17653
|
+
const s = loop2.stats.summary();
|
|
15944
17654
|
process.stdout.write(
|
|
15945
17655
|
`
|
|
15946
17656
|
\u2014 turns:${s.turns} cache:${(s.cacheHitRatio * 100).toFixed(1)}% cost:$${s.totalCostUsd.toFixed(6)} save-vs-claude:${s.savingsVsClaudePct.toFixed(1)}%
|
|
@@ -15988,14 +17698,14 @@ function listAll() {
|
|
|
15988
17698
|
console.log("Resume: reasonix chat --session <name>");
|
|
15989
17699
|
}
|
|
15990
17700
|
function inspectSession(name, verbose) {
|
|
15991
|
-
const
|
|
17701
|
+
const path5 = sessionPath(name);
|
|
15992
17702
|
const messages = loadSessionMessages(name);
|
|
15993
17703
|
if (messages.length === 0) {
|
|
15994
17704
|
console.error(`no session named "${name}" (or it's empty).`);
|
|
15995
|
-
console.error(`looked at: ${
|
|
17705
|
+
console.error(`looked at: ${path5}`);
|
|
15996
17706
|
process.exit(1);
|
|
15997
17707
|
}
|
|
15998
|
-
console.log(`[session] ${name} ${messages.length} messages ${
|
|
17708
|
+
console.log(`[session] ${name} ${messages.length} messages ${path5}`);
|
|
15999
17709
|
console.log("");
|
|
16000
17710
|
let turnIndex = 0;
|
|
16001
17711
|
for (const msg of messages) {
|
|
@@ -16037,7 +17747,7 @@ import { render as render4 } from "ink";
|
|
|
16037
17747
|
import React33 from "react";
|
|
16038
17748
|
|
|
16039
17749
|
// src/cli/ui/Wizard.tsx
|
|
16040
|
-
import { Box as Box27, Text as
|
|
17750
|
+
import { Box as Box27, Text as Text25, useApp as useApp5, useInput as useInput3 } from "ink";
|
|
16041
17751
|
import TextInput2 from "ink-text-input";
|
|
16042
17752
|
import React32, { useState as useState15 } from "react";
|
|
16043
17753
|
|
|
@@ -16111,7 +17821,7 @@ function Wizard({ onComplete, onCancel, existingApiKey, initial }) {
|
|
|
16111
17821
|
setStep("mcp");
|
|
16112
17822
|
}
|
|
16113
17823
|
}
|
|
16114
|
-
), /* @__PURE__ */ React32.createElement(Box27, { marginTop: 1 }, /* @__PURE__ */ React32.createElement(
|
|
17824
|
+
), /* @__PURE__ */ React32.createElement(Box27, { marginTop: 1 }, /* @__PURE__ */ React32.createElement(Text25, { dimColor: true }, "[\u2191\u2193] navigate \xB7 [Enter] confirm \xB7 [Esc] cancel")));
|
|
16115
17825
|
}
|
|
16116
17826
|
if (step === "mcp") {
|
|
16117
17827
|
return /* @__PURE__ */ React32.createElement(StepFrame, { title: "Which MCP servers should Reasonix wire up for you?", step: 2, total: 3 }, /* @__PURE__ */ React32.createElement(
|
|
@@ -16165,8 +17875,8 @@ function Wizard({ onComplete, onCancel, existingApiKey, initial }) {
|
|
|
16165
17875
|
}
|
|
16166
17876
|
), specs.map((spec, i) => (
|
|
16167
17877
|
// biome-ignore lint/suspicious/noArrayIndexKey: review-only render, order fixed
|
|
16168
|
-
/* @__PURE__ */ React32.createElement(Box27, { key: i, paddingLeft: 14 }, /* @__PURE__ */ React32.createElement(
|
|
16169
|
-
)), /* @__PURE__ */ React32.createElement(Box27, { marginTop: 1 }, /* @__PURE__ */ React32.createElement(
|
|
17878
|
+
/* @__PURE__ */ React32.createElement(Box27, { key: i, paddingLeft: 14 }, /* @__PURE__ */ React32.createElement(Text25, { dimColor: true }, "\xB7 ", spec))
|
|
17879
|
+
)), /* @__PURE__ */ React32.createElement(Box27, { marginTop: 1 }, /* @__PURE__ */ React32.createElement(Text25, null, "Saves to ", defaultConfigPath())), error ? /* @__PURE__ */ React32.createElement(Box27, { marginTop: 1 }, /* @__PURE__ */ React32.createElement(Text25, { color: "red" }, error)) : null, /* @__PURE__ */ React32.createElement(Box27, { marginTop: 1 }, /* @__PURE__ */ React32.createElement(Text25, { dimColor: true }, "[Enter] save \xB7 [Esc] cancel"))), /* @__PURE__ */ React32.createElement(
|
|
16170
17880
|
ReviewConfirm,
|
|
16171
17881
|
{
|
|
16172
17882
|
onConfirm: () => {
|
|
@@ -16192,7 +17902,7 @@ function Wizard({ onComplete, onCancel, existingApiKey, initial }) {
|
|
|
16192
17902
|
}
|
|
16193
17903
|
));
|
|
16194
17904
|
}
|
|
16195
|
-
return /* @__PURE__ */ React32.createElement(Box27, { flexDirection: "column", borderStyle: "round", borderColor: "green", paddingX: 1 }, /* @__PURE__ */ React32.createElement(
|
|
17905
|
+
return /* @__PURE__ */ React32.createElement(Box27, { flexDirection: "column", borderStyle: "round", borderColor: "green", paddingX: 1 }, /* @__PURE__ */ React32.createElement(Text25, { bold: true, color: "green" }, "\u25B8 Saved."), /* @__PURE__ */ React32.createElement(Box27, { marginTop: 1 }, /* @__PURE__ */ React32.createElement(Text25, null, "Run `reasonix` any time to start chatting \u2014 your settings are remembered.")), /* @__PURE__ */ React32.createElement(Box27, { marginTop: 1 }, /* @__PURE__ */ React32.createElement(Text25, { dimColor: true }, "[Enter] to exit")), /* @__PURE__ */ React32.createElement(ExitOnEnter, { onExit: exit2 }));
|
|
16196
17906
|
}
|
|
16197
17907
|
function ApiKeyStep({
|
|
16198
17908
|
onSubmit,
|
|
@@ -16200,7 +17910,7 @@ function ApiKeyStep({
|
|
|
16200
17910
|
onError
|
|
16201
17911
|
}) {
|
|
16202
17912
|
const [value, setValue] = useState15("");
|
|
16203
|
-
return /* @__PURE__ */ React32.createElement(Box27, { flexDirection: "column", borderStyle: "round", borderColor: "cyan", paddingX: 1 }, /* @__PURE__ */ React32.createElement(
|
|
17913
|
+
return /* @__PURE__ */ React32.createElement(Box27, { flexDirection: "column", borderStyle: "round", borderColor: "cyan", paddingX: 1 }, /* @__PURE__ */ React32.createElement(Text25, { bold: true, color: "cyan" }, "Welcome to Reasonix."), /* @__PURE__ */ React32.createElement(Box27, { marginTop: 1 }, /* @__PURE__ */ React32.createElement(Text25, null, "Paste your DeepSeek API key to get started.")), /* @__PURE__ */ React32.createElement(Text25, { dimColor: true }, "Get one (free credit on signup): https://platform.deepseek.com/api_keys"), /* @__PURE__ */ React32.createElement(Text25, { dimColor: true }, "Saved locally to ", defaultConfigPath()), /* @__PURE__ */ React32.createElement(Box27, { marginTop: 1 }, /* @__PURE__ */ React32.createElement(Text25, { bold: true, color: "cyan" }, "key \u203A "), /* @__PURE__ */ React32.createElement(
|
|
16204
17914
|
TextInput2,
|
|
16205
17915
|
{
|
|
16206
17916
|
value,
|
|
@@ -16217,7 +17927,7 @@ function ApiKeyStep({
|
|
|
16217
17927
|
mask: "\u2022",
|
|
16218
17928
|
placeholder: "sk-..."
|
|
16219
17929
|
}
|
|
16220
|
-
)), error ? /* @__PURE__ */ React32.createElement(Box27, { marginTop: 1 }, /* @__PURE__ */ React32.createElement(
|
|
17930
|
+
)), error ? /* @__PURE__ */ React32.createElement(Box27, { marginTop: 1 }, /* @__PURE__ */ React32.createElement(Text25, { color: "red" }, error)) : value ? /* @__PURE__ */ React32.createElement(Box27, { marginTop: 1 }, /* @__PURE__ */ React32.createElement(Text25, { dimColor: true }, "preview: ", redactKey(value))) : null);
|
|
16221
17931
|
}
|
|
16222
17932
|
function McpArgsStep({
|
|
16223
17933
|
entry,
|
|
@@ -16226,7 +17936,7 @@ function McpArgsStep({
|
|
|
16226
17936
|
onError
|
|
16227
17937
|
}) {
|
|
16228
17938
|
const [value, setValue] = useState15("");
|
|
16229
|
-
return /* @__PURE__ */ React32.createElement(StepFrame, { title: `Configure ${entry.name}`, step: 2, total: 3 }, /* @__PURE__ */ React32.createElement(Box27, { flexDirection: "column" }, /* @__PURE__ */ React32.createElement(
|
|
17939
|
+
return /* @__PURE__ */ React32.createElement(StepFrame, { title: `Configure ${entry.name}`, step: 2, total: 3 }, /* @__PURE__ */ React32.createElement(Box27, { flexDirection: "column" }, /* @__PURE__ */ React32.createElement(Text25, null, entry.summary), entry.note ? /* @__PURE__ */ React32.createElement(Box27, { marginTop: 1 }, /* @__PURE__ */ React32.createElement(Text25, { dimColor: true }, entry.note)) : null, /* @__PURE__ */ React32.createElement(Box27, { marginTop: 1 }, /* @__PURE__ */ React32.createElement(Text25, null, "Required parameter: "), /* @__PURE__ */ React32.createElement(Text25, { bold: true }, entry.userArgs)), /* @__PURE__ */ React32.createElement(Box27, { marginTop: 1 }, /* @__PURE__ */ React32.createElement(Text25, { bold: true, color: "cyan" }, entry.userArgs, " \u203A "), /* @__PURE__ */ React32.createElement(
|
|
16230
17940
|
TextInput2,
|
|
16231
17941
|
{
|
|
16232
17942
|
value,
|
|
@@ -16242,7 +17952,7 @@ function McpArgsStep({
|
|
|
16242
17952
|
},
|
|
16243
17953
|
placeholder: placeholderFor(entry)
|
|
16244
17954
|
}
|
|
16245
|
-
)), error ? /* @__PURE__ */ React32.createElement(Box27, { marginTop: 1 }, /* @__PURE__ */ React32.createElement(
|
|
17955
|
+
)), error ? /* @__PURE__ */ React32.createElement(Box27, { marginTop: 1 }, /* @__PURE__ */ React32.createElement(Text25, { color: "red" }, error)) : null));
|
|
16246
17956
|
}
|
|
16247
17957
|
function ReviewConfirm({ onConfirm }) {
|
|
16248
17958
|
useInput3((_i, key) => {
|
|
@@ -16262,10 +17972,10 @@ function StepFrame({
|
|
|
16262
17972
|
total,
|
|
16263
17973
|
children
|
|
16264
17974
|
}) {
|
|
16265
|
-
return /* @__PURE__ */ React32.createElement(Box27, { flexDirection: "column", borderStyle: "round", borderColor: "cyan", paddingX: 1 }, /* @__PURE__ */ React32.createElement(Box27, null, /* @__PURE__ */ React32.createElement(
|
|
17975
|
+
return /* @__PURE__ */ React32.createElement(Box27, { flexDirection: "column", borderStyle: "round", borderColor: "cyan", paddingX: 1 }, /* @__PURE__ */ React32.createElement(Box27, null, /* @__PURE__ */ React32.createElement(Text25, { dimColor: true }, "Step ", step, "/", total, " \xB7", " "), /* @__PURE__ */ React32.createElement(Text25, { bold: true, color: "cyan" }, title)), /* @__PURE__ */ React32.createElement(Box27, { marginTop: 1, flexDirection: "column" }, children));
|
|
16266
17976
|
}
|
|
16267
17977
|
function SummaryLine({ label, value }) {
|
|
16268
|
-
return /* @__PURE__ */ React32.createElement(Box27, null, /* @__PURE__ */ React32.createElement(
|
|
17978
|
+
return /* @__PURE__ */ React32.createElement(Box27, null, /* @__PURE__ */ React32.createElement(Text25, null, label.padEnd(12)), /* @__PURE__ */ React32.createElement(Text25, { bold: true }, value));
|
|
16269
17979
|
}
|
|
16270
17980
|
function presetItems() {
|
|
16271
17981
|
return ["fast", "smart", "max"].map((name) => ({
|
|
@@ -16339,7 +18049,7 @@ async function setupCommand(_opts = {}) {
|
|
|
16339
18049
|
}
|
|
16340
18050
|
|
|
16341
18051
|
// src/cli/commands/update.ts
|
|
16342
|
-
import { spawn as
|
|
18052
|
+
import { spawn as spawn6 } from "child_process";
|
|
16343
18053
|
function planUpdate(input) {
|
|
16344
18054
|
const diff = compareVersions(input.current, input.latest);
|
|
16345
18055
|
if (diff > 0) {
|
|
@@ -16369,13 +18079,13 @@ function planUpdate(input) {
|
|
|
16369
18079
|
};
|
|
16370
18080
|
}
|
|
16371
18081
|
function defaultSpawn(argv) {
|
|
16372
|
-
return new Promise((
|
|
16373
|
-
const child =
|
|
18082
|
+
return new Promise((resolve9, reject) => {
|
|
18083
|
+
const child = spawn6(argv[0], argv.slice(1), {
|
|
16374
18084
|
stdio: "inherit",
|
|
16375
18085
|
shell: process.platform === "win32"
|
|
16376
18086
|
});
|
|
16377
18087
|
child.once("error", reject);
|
|
16378
|
-
child.once("exit", (code) =>
|
|
18088
|
+
child.once("exit", (code) => resolve9(code ?? 1));
|
|
16379
18089
|
});
|
|
16380
18090
|
}
|
|
16381
18091
|
async function updateCommand(opts = {}) {
|
|
@@ -16671,6 +18381,16 @@ program.command("update").description(
|
|
|
16671
18381
|
).option("--dry-run", "Print the plan without executing the install").action(async (opts) => {
|
|
16672
18382
|
await updateCommand({ dryRun: !!opts.dryRun });
|
|
16673
18383
|
});
|
|
18384
|
+
program.command("index").description(
|
|
18385
|
+
"Build (or incrementally refresh) a local semantic search index for the project so `reasonix code` can answer 'where do we\u2026' questions by meaning, not just by token. Uses Ollama as the embedding backend; missing daemon / model is offered to the user with a confirm prompt."
|
|
18386
|
+
).option("--rebuild", "Wipe and rebuild from scratch").option("--model <name>", "Embedding model (default: nomic-embed-text)").option("--dir <path>", "Project root to index (default: cwd)").option("--ollama-url <url>", "Override Ollama base URL (default: http://localhost:11434)").option(
|
|
18387
|
+
"-y, --yes",
|
|
18388
|
+
"Skip preflight prompts \u2014 auto-start the daemon and pull the model if missing (use in scripts)"
|
|
18389
|
+
).action(
|
|
18390
|
+
async (opts) => {
|
|
18391
|
+
await indexCommand(opts);
|
|
18392
|
+
}
|
|
18393
|
+
);
|
|
16674
18394
|
program.parseAsync(process.argv).catch((err) => {
|
|
16675
18395
|
console.error(err);
|
|
16676
18396
|
process.exit(1);
|