@itsautomata/prism 0.2.0 → 0.3.1
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 -1
- package/dist/cli.js +2147 -380
- package/dist/wasm/tree-sitter-bash.wasm +0 -0
- package/dist/wasm/tree-sitter-c-sharp.wasm +0 -0
- package/dist/wasm/tree-sitter-c.wasm +0 -0
- package/dist/wasm/tree-sitter-clojure.wasm +0 -0
- package/dist/wasm/tree-sitter-cpp.wasm +0 -0
- package/dist/wasm/tree-sitter-css.wasm +0 -0
- package/dist/wasm/tree-sitter-dart.wasm +0 -0
- package/dist/wasm/tree-sitter-dockerfile.wasm +0 -0
- package/dist/wasm/tree-sitter-elixir.wasm +0 -0
- package/dist/wasm/tree-sitter-erlang.wasm +0 -0
- package/dist/wasm/tree-sitter-go.wasm +0 -0
- package/dist/wasm/tree-sitter-graphql.wasm +0 -0
- package/dist/wasm/tree-sitter-haskell.wasm +0 -0
- package/dist/wasm/tree-sitter-html.wasm +0 -0
- package/dist/wasm/tree-sitter-java.wasm +0 -0
- package/dist/wasm/tree-sitter-javascript.wasm +0 -0
- package/dist/wasm/tree-sitter-json.wasm +0 -0
- package/dist/wasm/tree-sitter-julia.wasm +0 -0
- package/dist/wasm/tree-sitter-kotlin.wasm +0 -0
- package/dist/wasm/tree-sitter-lua.wasm +0 -0
- package/dist/wasm/tree-sitter-make.wasm +0 -0
- package/dist/wasm/tree-sitter-markdown.wasm +0 -0
- package/dist/wasm/tree-sitter-ocaml.wasm +0 -0
- package/dist/wasm/tree-sitter-php.wasm +0 -0
- package/dist/wasm/tree-sitter-python.wasm +0 -0
- package/dist/wasm/tree-sitter-r.wasm +0 -0
- package/dist/wasm/tree-sitter-ruby.wasm +0 -0
- package/dist/wasm/tree-sitter-rust.wasm +0 -0
- package/dist/wasm/tree-sitter-scala.wasm +0 -0
- package/dist/wasm/tree-sitter-sql.wasm +0 -0
- package/dist/wasm/tree-sitter-svelte.wasm +0 -0
- package/dist/wasm/tree-sitter-swift.wasm +0 -0
- package/dist/wasm/tree-sitter-toml.wasm +0 -0
- package/dist/wasm/tree-sitter-tsx.wasm +0 -0
- package/dist/wasm/tree-sitter-typescript.wasm +0 -0
- package/dist/wasm/tree-sitter-yaml.wasm +0 -0
- package/dist/wasm/tree-sitter-zig.wasm +0 -0
- package/npm-shrinkwrap.json +4182 -0
- package/package.json +14 -3
package/dist/cli.js
CHANGED
|
@@ -11,7 +11,7 @@ import React4 from "react";
|
|
|
11
11
|
import { render } from "ink";
|
|
12
12
|
|
|
13
13
|
// src/ui/App.tsx
|
|
14
|
-
import { useState as useState3, useCallback as useCallback3, useMemo as useMemo2 } from "react";
|
|
14
|
+
import { useState as useState3, useCallback as useCallback3, useMemo as useMemo2, useEffect as useEffect3, useRef as useRef3 } from "react";
|
|
15
15
|
import { Box as Box8, useApp, useInput as useInput3 } from "ink";
|
|
16
16
|
|
|
17
17
|
// src/ui/Banner.tsx
|
|
@@ -40,8 +40,8 @@ var theme = {
|
|
|
40
40
|
// amber for shell mode and warnings
|
|
41
41
|
planMode: "#a78bfa",
|
|
42
42
|
// soft violet for plan mode (cool, contemplative)
|
|
43
|
-
error: "#
|
|
44
|
-
//
|
|
43
|
+
error: "#cf3d0c",
|
|
44
|
+
// burnt vermillion for errors
|
|
45
45
|
success: "#00ff88",
|
|
46
46
|
// same as primary
|
|
47
47
|
// UI elements
|
|
@@ -54,11 +54,11 @@ var theme = {
|
|
|
54
54
|
spinner: "#00ff88",
|
|
55
55
|
// loading spinner
|
|
56
56
|
// tool results
|
|
57
|
-
toolName: "#
|
|
58
|
-
// tool name highlight
|
|
57
|
+
toolName: "#c29734",
|
|
58
|
+
// muted gold for tool name highlight
|
|
59
59
|
toolOutput: "#aaaaaa",
|
|
60
60
|
// tool output text
|
|
61
|
-
toolError: "#
|
|
61
|
+
toolError: "#cf3d0c",
|
|
62
62
|
// tool error text
|
|
63
63
|
// thinking
|
|
64
64
|
thinking: "#666666"
|
|
@@ -258,8 +258,8 @@ function MessageBlock({ message }) {
|
|
|
258
258
|
return /* @__PURE__ */ jsx3(Box3, { marginTop: 0, marginBottom: 0, marginLeft: 2, children: /* @__PURE__ */ jsx3(Markdown, { text: message.text }) });
|
|
259
259
|
case "tool_call":
|
|
260
260
|
return /* @__PURE__ */ jsxs3(Box3, { marginTop: 0, marginBottom: 0, marginLeft: 2, children: [
|
|
261
|
-
/* @__PURE__ */ jsx3(Text3, { color: theme.
|
|
262
|
-
/* @__PURE__ */ jsx3(Text3, { color: theme.
|
|
261
|
+
/* @__PURE__ */ jsx3(Text3, { color: theme.toolName, children: "\u26A1 " }),
|
|
262
|
+
/* @__PURE__ */ jsx3(Text3, { color: theme.toolName, bold: true, children: message.toolName?.toLowerCase() })
|
|
263
263
|
] });
|
|
264
264
|
case "tool_result":
|
|
265
265
|
if (message.isError) {
|
|
@@ -282,6 +282,7 @@ import { Box as Box5, Text as Text5, useInput } from "ink";
|
|
|
282
282
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
|
|
283
283
|
import { join } from "path";
|
|
284
284
|
import { homedir as homedir2 } from "os";
|
|
285
|
+
import { createHash } from "crypto";
|
|
285
286
|
var PROFILES_DIR = join(homedir2(), ".prism", "models");
|
|
286
287
|
function ensureDir() {
|
|
287
288
|
if (!existsSync(PROFILES_DIR)) {
|
|
@@ -290,7 +291,8 @@ function ensureDir() {
|
|
|
290
291
|
}
|
|
291
292
|
function profilePath(model) {
|
|
292
293
|
const safe = model.replace(/[^a-zA-Z0-9._-]/g, "_");
|
|
293
|
-
|
|
294
|
+
const hash = createHash("sha256").update(model).digest("hex").slice(0, 8);
|
|
295
|
+
return join(PROFILES_DIR, `${safe}-${hash}.json`);
|
|
294
296
|
}
|
|
295
297
|
function loadProfile(model) {
|
|
296
298
|
const path = profilePath(model);
|
|
@@ -358,9 +360,17 @@ Follow these rules exactly. They override general instructions when they conflic
|
|
|
358
360
|
import { existsSync as existsSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2, mkdirSync as mkdirSync2, copyFileSync } from "fs";
|
|
359
361
|
import { join as join2 } from "path";
|
|
360
362
|
import { homedir as homedir3 } from "os";
|
|
361
|
-
import { createHash } from "crypto";
|
|
363
|
+
import { createHash as createHash2 } from "crypto";
|
|
362
364
|
import { execSync } from "child_process";
|
|
363
365
|
var PROJECTS_DIR = join2(homedir3(), ".prism", "projects");
|
|
366
|
+
function normalizeRemote(url) {
|
|
367
|
+
let s = url.trim().toLowerCase().replace(/\/+$/, "").replace(/\.git$/, "");
|
|
368
|
+
const scp = s.match(/^[^@]+@([^:]+):(.+)$/);
|
|
369
|
+
if (scp) return `${scp[1]}/${scp[2]}`;
|
|
370
|
+
const proto = s.match(/^[a-z][a-z0-9+.-]*:\/\/(?:[^@/]+@)?(.+)$/);
|
|
371
|
+
if (proto) return proto[1];
|
|
372
|
+
return s;
|
|
373
|
+
}
|
|
364
374
|
function getProjectId(cwd) {
|
|
365
375
|
let key = cwd;
|
|
366
376
|
try {
|
|
@@ -370,10 +380,10 @@ function getProjectId(cwd) {
|
|
|
370
380
|
timeout: 1e3,
|
|
371
381
|
stdio: ["pipe", "pipe", "pipe"]
|
|
372
382
|
}).trim();
|
|
373
|
-
if (remote) key = remote;
|
|
383
|
+
if (remote) key = normalizeRemote(remote);
|
|
374
384
|
} catch {
|
|
375
385
|
}
|
|
376
|
-
return
|
|
386
|
+
return createHash2("sha256").update(key).digest("hex").slice(0, 12);
|
|
377
387
|
}
|
|
378
388
|
function memoDir(id) {
|
|
379
389
|
return join2(PROJECTS_DIR, id);
|
|
@@ -832,13 +842,19 @@ function handleSlashCommand(input, model, profile, setProfile, setMessages, exit
|
|
|
832
842
|
}
|
|
833
843
|
return true;
|
|
834
844
|
case "/forget": {
|
|
835
|
-
const
|
|
836
|
-
|
|
837
|
-
|
|
845
|
+
const raw = args.trim();
|
|
846
|
+
const idx = parseInt(raw, 10) - 1;
|
|
847
|
+
if (!/^\d+$/.test(raw)) {
|
|
848
|
+
info("usage: /forget <number> (see /rules)");
|
|
838
849
|
} else {
|
|
850
|
+
const before = loadProfile(model).rules.length;
|
|
839
851
|
const updated = removeRule(model, idx);
|
|
840
|
-
|
|
841
|
-
|
|
852
|
+
if (updated.rules.length === before) {
|
|
853
|
+
info(`no rule #${raw}. you have ${before} rule${before === 1 ? "" : "s"} (see /rules).`);
|
|
854
|
+
} else {
|
|
855
|
+
setProfile(updated);
|
|
856
|
+
info("rule removed.");
|
|
857
|
+
}
|
|
842
858
|
}
|
|
843
859
|
return true;
|
|
844
860
|
}
|
|
@@ -1177,27 +1193,744 @@ function SlashHints({ matches, selectedIdx }) {
|
|
|
1177
1193
|
] });
|
|
1178
1194
|
}
|
|
1179
1195
|
|
|
1196
|
+
// src/ui/inputBuffer.ts
|
|
1197
|
+
import { randomUUID } from "crypto";
|
|
1198
|
+
var cp = (s) => Array.from(s);
|
|
1199
|
+
var clen = (s) => cp(s).length;
|
|
1200
|
+
var cslice = (s, a, b) => cp(s).slice(a, b).join("");
|
|
1201
|
+
function createBuffer() {
|
|
1202
|
+
return [];
|
|
1203
|
+
}
|
|
1204
|
+
function flatLength(buf) {
|
|
1205
|
+
let n = 0;
|
|
1206
|
+
for (const seg of buf) n += seg.kind === "text" ? clen(seg.chars) : 1;
|
|
1207
|
+
return n;
|
|
1208
|
+
}
|
|
1209
|
+
function locate(buf, pos) {
|
|
1210
|
+
const clamped = Math.max(0, Math.min(pos, flatLength(buf)));
|
|
1211
|
+
let consumed = 0;
|
|
1212
|
+
for (let i = 0; i < buf.length; i++) {
|
|
1213
|
+
const seg = buf[i];
|
|
1214
|
+
const len = seg.kind === "text" ? clen(seg.chars) : 1;
|
|
1215
|
+
if (clamped <= consumed + len) {
|
|
1216
|
+
return { segIdx: i, offset: clamped - consumed };
|
|
1217
|
+
}
|
|
1218
|
+
consumed += len;
|
|
1219
|
+
}
|
|
1220
|
+
return { segIdx: buf.length, offset: 0 };
|
|
1221
|
+
}
|
|
1222
|
+
function normalize(buf) {
|
|
1223
|
+
const out = [];
|
|
1224
|
+
for (const seg of buf) {
|
|
1225
|
+
if (seg.kind === "text" && seg.chars.length === 0) continue;
|
|
1226
|
+
const last = out[out.length - 1];
|
|
1227
|
+
if (last && last.kind === "text" && seg.kind === "text") {
|
|
1228
|
+
out[out.length - 1] = { kind: "text", chars: last.chars + seg.chars };
|
|
1229
|
+
} else {
|
|
1230
|
+
out.push(seg);
|
|
1231
|
+
}
|
|
1232
|
+
}
|
|
1233
|
+
return out;
|
|
1234
|
+
}
|
|
1235
|
+
function insertText(buf, pos, str) {
|
|
1236
|
+
if (!str) return buf;
|
|
1237
|
+
if (buf.length === 0) return [{ kind: "text", chars: str }];
|
|
1238
|
+
const { segIdx, offset } = locate(buf, pos);
|
|
1239
|
+
const next = [];
|
|
1240
|
+
for (let i = 0; i < buf.length; i++) {
|
|
1241
|
+
const seg = buf[i];
|
|
1242
|
+
if (i !== segIdx) {
|
|
1243
|
+
next.push(seg);
|
|
1244
|
+
continue;
|
|
1245
|
+
}
|
|
1246
|
+
if (seg.kind === "text") {
|
|
1247
|
+
next.push({ kind: "text", chars: cslice(seg.chars, 0, offset) + str + cslice(seg.chars, offset) });
|
|
1248
|
+
} else {
|
|
1249
|
+
if (offset === 0) {
|
|
1250
|
+
next.push({ kind: "text", chars: str }, seg);
|
|
1251
|
+
} else {
|
|
1252
|
+
next.push(seg, { kind: "text", chars: str });
|
|
1253
|
+
}
|
|
1254
|
+
}
|
|
1255
|
+
}
|
|
1256
|
+
if (segIdx === buf.length) {
|
|
1257
|
+
next.push({ kind: "text", chars: str });
|
|
1258
|
+
}
|
|
1259
|
+
return normalize(next);
|
|
1260
|
+
}
|
|
1261
|
+
function insertPill(buf, pos, content, store, idOverride) {
|
|
1262
|
+
const id = idOverride ?? randomUUID();
|
|
1263
|
+
store.set(id, content);
|
|
1264
|
+
const pill = { kind: "pill", id, size: content.length };
|
|
1265
|
+
if (buf.length === 0) return [pill];
|
|
1266
|
+
const { segIdx, offset } = locate(buf, pos);
|
|
1267
|
+
const next = [];
|
|
1268
|
+
for (let i = 0; i < buf.length; i++) {
|
|
1269
|
+
const seg = buf[i];
|
|
1270
|
+
if (i !== segIdx) {
|
|
1271
|
+
next.push(seg);
|
|
1272
|
+
continue;
|
|
1273
|
+
}
|
|
1274
|
+
if (seg.kind === "text") {
|
|
1275
|
+
next.push({ kind: "text", chars: cslice(seg.chars, 0, offset) }, pill, { kind: "text", chars: cslice(seg.chars, offset) });
|
|
1276
|
+
} else {
|
|
1277
|
+
if (offset === 0) next.push(pill, seg);
|
|
1278
|
+
else next.push(seg, pill);
|
|
1279
|
+
}
|
|
1280
|
+
}
|
|
1281
|
+
if (segIdx === buf.length) {
|
|
1282
|
+
next.push(pill);
|
|
1283
|
+
}
|
|
1284
|
+
return normalize(next);
|
|
1285
|
+
}
|
|
1286
|
+
function deleteBack(buf, pos) {
|
|
1287
|
+
if (pos <= 0 || buf.length === 0) return { buf, pos: 0 };
|
|
1288
|
+
const { segIdx, offset } = locate(buf, pos);
|
|
1289
|
+
const next = [];
|
|
1290
|
+
let removed = 0;
|
|
1291
|
+
for (let i = 0; i < buf.length; i++) {
|
|
1292
|
+
const seg = buf[i];
|
|
1293
|
+
if (i === segIdx) {
|
|
1294
|
+
if (seg.kind === "text") {
|
|
1295
|
+
if (offset === 0) {
|
|
1296
|
+
const prev = next.pop();
|
|
1297
|
+
if (!prev) {
|
|
1298
|
+
next.push(seg);
|
|
1299
|
+
break;
|
|
1300
|
+
}
|
|
1301
|
+
if (prev.kind === "text") {
|
|
1302
|
+
next.push({ kind: "text", chars: cslice(prev.chars, 0, -1) });
|
|
1303
|
+
}
|
|
1304
|
+
removed = 1;
|
|
1305
|
+
} else {
|
|
1306
|
+
next.push({ kind: "text", chars: cslice(seg.chars, 0, offset - 1) + cslice(seg.chars, offset) });
|
|
1307
|
+
removed = 1;
|
|
1308
|
+
}
|
|
1309
|
+
} else {
|
|
1310
|
+
if (offset === 0) {
|
|
1311
|
+
const prev = next.pop();
|
|
1312
|
+
if (!prev) {
|
|
1313
|
+
next.push(seg);
|
|
1314
|
+
break;
|
|
1315
|
+
}
|
|
1316
|
+
if (prev.kind === "text") {
|
|
1317
|
+
next.push({ kind: "text", chars: cslice(prev.chars, 0, -1) });
|
|
1318
|
+
}
|
|
1319
|
+
next.push(seg);
|
|
1320
|
+
removed = 1;
|
|
1321
|
+
} else {
|
|
1322
|
+
removed = 1;
|
|
1323
|
+
continue;
|
|
1324
|
+
}
|
|
1325
|
+
}
|
|
1326
|
+
} else {
|
|
1327
|
+
next.push(seg);
|
|
1328
|
+
}
|
|
1329
|
+
}
|
|
1330
|
+
return { buf: normalize(next), pos: pos - removed };
|
|
1331
|
+
}
|
|
1332
|
+
function deleteForward(buf, pos) {
|
|
1333
|
+
if (pos >= flatLength(buf) || buf.length === 0) return { buf, pos };
|
|
1334
|
+
const { segIdx, offset } = locate(buf, pos);
|
|
1335
|
+
const next = [];
|
|
1336
|
+
for (let i = 0; i < buf.length; i++) {
|
|
1337
|
+
const seg = buf[i];
|
|
1338
|
+
if (i !== segIdx) {
|
|
1339
|
+
next.push(seg);
|
|
1340
|
+
continue;
|
|
1341
|
+
}
|
|
1342
|
+
if (seg.kind === "text") {
|
|
1343
|
+
if (offset >= clen(seg.chars)) {
|
|
1344
|
+
next.push(seg);
|
|
1345
|
+
const target = buf[i + 1];
|
|
1346
|
+
if (target?.kind === "text") {
|
|
1347
|
+
next.push({ kind: "text", chars: cslice(target.chars, 1) });
|
|
1348
|
+
i++;
|
|
1349
|
+
} else if (target?.kind === "pill") {
|
|
1350
|
+
i++;
|
|
1351
|
+
}
|
|
1352
|
+
} else {
|
|
1353
|
+
next.push({ kind: "text", chars: cslice(seg.chars, 0, offset) + cslice(seg.chars, offset + 1) });
|
|
1354
|
+
}
|
|
1355
|
+
} else {
|
|
1356
|
+
if (offset === 1) {
|
|
1357
|
+
next.push(seg);
|
|
1358
|
+
const target = buf[i + 1];
|
|
1359
|
+
if (target?.kind === "text") {
|
|
1360
|
+
next.push({ kind: "text", chars: cslice(target.chars, 1) });
|
|
1361
|
+
i++;
|
|
1362
|
+
} else if (target?.kind === "pill") {
|
|
1363
|
+
i++;
|
|
1364
|
+
}
|
|
1365
|
+
} else {
|
|
1366
|
+
continue;
|
|
1367
|
+
}
|
|
1368
|
+
}
|
|
1369
|
+
}
|
|
1370
|
+
return { buf: normalize(next), pos };
|
|
1371
|
+
}
|
|
1372
|
+
function expand(buf, store) {
|
|
1373
|
+
let out = "";
|
|
1374
|
+
for (const seg of buf) {
|
|
1375
|
+
out += seg.kind === "text" ? seg.chars : store.get(seg.id) ?? "";
|
|
1376
|
+
}
|
|
1377
|
+
return out;
|
|
1378
|
+
}
|
|
1379
|
+
function wordBoundaryLeft(buf, pos) {
|
|
1380
|
+
if (pos <= 0) return 0;
|
|
1381
|
+
const { segIdx, offset } = locate(buf, pos);
|
|
1382
|
+
const seg = buf[segIdx];
|
|
1383
|
+
if (seg?.kind === "pill") {
|
|
1384
|
+
return pos - offset;
|
|
1385
|
+
}
|
|
1386
|
+
let i = segIdx;
|
|
1387
|
+
let off = offset;
|
|
1388
|
+
let cursor = pos;
|
|
1389
|
+
let inWord = false;
|
|
1390
|
+
while (cursor > 0) {
|
|
1391
|
+
const cur = buf[i];
|
|
1392
|
+
if (!cur) break;
|
|
1393
|
+
if (cur.kind === "pill") {
|
|
1394
|
+
return cursor;
|
|
1395
|
+
}
|
|
1396
|
+
if (off === 0) {
|
|
1397
|
+
i -= 1;
|
|
1398
|
+
if (i < 0) return 0;
|
|
1399
|
+
const prev = buf[i];
|
|
1400
|
+
off = prev.kind === "text" ? clen(prev.chars) : 1;
|
|
1401
|
+
continue;
|
|
1402
|
+
}
|
|
1403
|
+
const ch = cp(cur.chars)[off - 1];
|
|
1404
|
+
const isWordChar = /\w/.test(ch);
|
|
1405
|
+
if (isWordChar) inWord = true;
|
|
1406
|
+
else if (inWord) return cursor;
|
|
1407
|
+
cursor -= 1;
|
|
1408
|
+
off -= 1;
|
|
1409
|
+
}
|
|
1410
|
+
return cursor;
|
|
1411
|
+
}
|
|
1412
|
+
function wordBoundaryRight(buf, pos) {
|
|
1413
|
+
const total = flatLength(buf);
|
|
1414
|
+
if (pos >= total) return total;
|
|
1415
|
+
const { segIdx, offset } = locate(buf, pos);
|
|
1416
|
+
const seg = buf[segIdx];
|
|
1417
|
+
if (seg?.kind === "pill") {
|
|
1418
|
+
return pos + (1 - offset);
|
|
1419
|
+
}
|
|
1420
|
+
let i = segIdx;
|
|
1421
|
+
let off = offset;
|
|
1422
|
+
let cursor = pos;
|
|
1423
|
+
let inWord = false;
|
|
1424
|
+
while (cursor < total) {
|
|
1425
|
+
const cur = buf[i];
|
|
1426
|
+
if (!cur) break;
|
|
1427
|
+
if (cur.kind === "pill") {
|
|
1428
|
+
return cursor + 1;
|
|
1429
|
+
}
|
|
1430
|
+
if (off >= clen(cur.chars)) {
|
|
1431
|
+
i += 1;
|
|
1432
|
+
off = 0;
|
|
1433
|
+
continue;
|
|
1434
|
+
}
|
|
1435
|
+
const ch = cp(cur.chars)[off];
|
|
1436
|
+
const isWordChar = /\w/.test(ch);
|
|
1437
|
+
if (isWordChar) inWord = true;
|
|
1438
|
+
else if (inWord) return cursor;
|
|
1439
|
+
cursor += 1;
|
|
1440
|
+
off += 1;
|
|
1441
|
+
}
|
|
1442
|
+
return cursor;
|
|
1443
|
+
}
|
|
1444
|
+
function deleteWordBack(buf, pos) {
|
|
1445
|
+
const target = wordBoundaryLeft(buf, pos);
|
|
1446
|
+
if (target >= pos) return { buf, pos };
|
|
1447
|
+
let cur = pos;
|
|
1448
|
+
let working = buf;
|
|
1449
|
+
while (cur > target) {
|
|
1450
|
+
const step = deleteBack(working, cur);
|
|
1451
|
+
working = step.buf;
|
|
1452
|
+
cur = step.pos;
|
|
1453
|
+
}
|
|
1454
|
+
return { buf: working, pos: cur };
|
|
1455
|
+
}
|
|
1456
|
+
function killToEnd(buf, pos) {
|
|
1457
|
+
const total = flatLength(buf);
|
|
1458
|
+
if (pos >= total) return buf;
|
|
1459
|
+
let working = buf;
|
|
1460
|
+
let n = total - pos;
|
|
1461
|
+
while (n-- > 0) {
|
|
1462
|
+
const step = deleteForward(working, pos);
|
|
1463
|
+
working = step.buf;
|
|
1464
|
+
}
|
|
1465
|
+
return working;
|
|
1466
|
+
}
|
|
1467
|
+
function totalLines(buf) {
|
|
1468
|
+
let n = 1;
|
|
1469
|
+
for (const seg of buf) {
|
|
1470
|
+
if (seg.kind === "text") {
|
|
1471
|
+
for (const ch of cp(seg.chars)) {
|
|
1472
|
+
if (ch === "\n") n++;
|
|
1473
|
+
}
|
|
1474
|
+
}
|
|
1475
|
+
}
|
|
1476
|
+
return n;
|
|
1477
|
+
}
|
|
1478
|
+
function flatToLineCol(buf, pos) {
|
|
1479
|
+
let line = 0;
|
|
1480
|
+
let col = 0;
|
|
1481
|
+
let consumed = 0;
|
|
1482
|
+
for (const seg of buf) {
|
|
1483
|
+
if (consumed >= pos) return { line, col };
|
|
1484
|
+
if (seg.kind === "text") {
|
|
1485
|
+
for (const ch of cp(seg.chars)) {
|
|
1486
|
+
if (consumed >= pos) return { line, col };
|
|
1487
|
+
if (ch === "\n") {
|
|
1488
|
+
line++;
|
|
1489
|
+
col = 0;
|
|
1490
|
+
} else {
|
|
1491
|
+
col++;
|
|
1492
|
+
}
|
|
1493
|
+
consumed++;
|
|
1494
|
+
}
|
|
1495
|
+
} else {
|
|
1496
|
+
if (consumed >= pos) return { line, col };
|
|
1497
|
+
col++;
|
|
1498
|
+
consumed++;
|
|
1499
|
+
}
|
|
1500
|
+
}
|
|
1501
|
+
return { line, col };
|
|
1502
|
+
}
|
|
1503
|
+
function lineColToFlat(buf, targetLine, targetCol) {
|
|
1504
|
+
let line = 0;
|
|
1505
|
+
let col = 0;
|
|
1506
|
+
let pos = 0;
|
|
1507
|
+
for (const seg of buf) {
|
|
1508
|
+
if (seg.kind === "text") {
|
|
1509
|
+
for (const ch of cp(seg.chars)) {
|
|
1510
|
+
if (line === targetLine && col >= targetCol) return pos;
|
|
1511
|
+
if (line === targetLine && ch === "\n") return pos;
|
|
1512
|
+
if (line > targetLine) return pos;
|
|
1513
|
+
if (ch === "\n") {
|
|
1514
|
+
line++;
|
|
1515
|
+
col = 0;
|
|
1516
|
+
} else {
|
|
1517
|
+
col++;
|
|
1518
|
+
}
|
|
1519
|
+
pos++;
|
|
1520
|
+
}
|
|
1521
|
+
} else {
|
|
1522
|
+
if (line === targetLine && col >= targetCol) return pos;
|
|
1523
|
+
if (line > targetLine) return pos;
|
|
1524
|
+
col++;
|
|
1525
|
+
pos++;
|
|
1526
|
+
}
|
|
1527
|
+
}
|
|
1528
|
+
return pos;
|
|
1529
|
+
}
|
|
1530
|
+
function moveCursorUp(buf, pos) {
|
|
1531
|
+
const { line, col } = flatToLineCol(buf, pos);
|
|
1532
|
+
if (line === 0) return pos;
|
|
1533
|
+
return lineColToFlat(buf, line - 1, col);
|
|
1534
|
+
}
|
|
1535
|
+
function moveCursorDown(buf, pos) {
|
|
1536
|
+
const { line, col } = flatToLineCol(buf, pos);
|
|
1537
|
+
if (line >= totalLines(buf) - 1) return pos;
|
|
1538
|
+
return lineColToFlat(buf, line + 1, col);
|
|
1539
|
+
}
|
|
1540
|
+
function sliceToLines(buf, startLine, endLine, cursor) {
|
|
1541
|
+
const result = [];
|
|
1542
|
+
let line = 0;
|
|
1543
|
+
let consumed = 0;
|
|
1544
|
+
let cursorDropped = 0;
|
|
1545
|
+
for (const seg of buf) {
|
|
1546
|
+
if (line >= endLine) break;
|
|
1547
|
+
if (seg.kind === "text") {
|
|
1548
|
+
let chunk = "";
|
|
1549
|
+
for (const ch of cp(seg.chars)) {
|
|
1550
|
+
if (line < startLine) {
|
|
1551
|
+
if (consumed < cursor) cursorDropped++;
|
|
1552
|
+
} else if (line < endLine) {
|
|
1553
|
+
chunk += ch;
|
|
1554
|
+
}
|
|
1555
|
+
consumed++;
|
|
1556
|
+
if (ch === "\n") line++;
|
|
1557
|
+
if (line >= endLine) break;
|
|
1558
|
+
}
|
|
1559
|
+
if (chunk.endsWith("\n")) chunk = chunk.slice(0, -1);
|
|
1560
|
+
if (chunk) result.push({ kind: "text", chars: chunk });
|
|
1561
|
+
} else {
|
|
1562
|
+
if (line < startLine) {
|
|
1563
|
+
if (consumed < cursor) cursorDropped++;
|
|
1564
|
+
} else if (line < endLine) {
|
|
1565
|
+
result.push(seg);
|
|
1566
|
+
}
|
|
1567
|
+
consumed++;
|
|
1568
|
+
}
|
|
1569
|
+
}
|
|
1570
|
+
let cursorOut = Math.max(0, cursor - cursorDropped);
|
|
1571
|
+
const newLen = result.reduce((n, s) => n + (s.kind === "text" ? clen(s.chars) : 1), 0);
|
|
1572
|
+
if (cursorOut > newLen) cursorOut = newLen;
|
|
1573
|
+
return { buf: result, cursor: cursorOut };
|
|
1574
|
+
}
|
|
1575
|
+
|
|
1576
|
+
// src/config/config.ts
|
|
1577
|
+
import { existsSync as existsSync5, readFileSync as readFileSync5, writeFileSync as writeFileSync3, mkdirSync as mkdirSync3 } from "fs";
|
|
1578
|
+
import { join as join5 } from "path";
|
|
1579
|
+
import { homedir as homedir6 } from "os";
|
|
1580
|
+
var PRISM_DIR = join5(homedir6(), ".prism");
|
|
1581
|
+
var CONFIG_PATH = join5(PRISM_DIR, "config.toml");
|
|
1582
|
+
var TUNING_DEFAULTS = {
|
|
1583
|
+
repomap_max_files: 500,
|
|
1584
|
+
repomap_max_lines: 500,
|
|
1585
|
+
repomap_max_symbols_per_file: 10,
|
|
1586
|
+
lens_max_bytes: 64 * 1024,
|
|
1587
|
+
bash_timeout_ms: 3e4,
|
|
1588
|
+
bash_max_output_bytes: 512 * 1024,
|
|
1589
|
+
compaction_threshold: 0.8,
|
|
1590
|
+
empty_turn_nudge_cap: 2,
|
|
1591
|
+
input_viewport_max_lines: 10
|
|
1592
|
+
};
|
|
1593
|
+
var DEFAULTS = {
|
|
1594
|
+
default_provider: "ollama",
|
|
1595
|
+
default_model: "deepseek-r1:14b",
|
|
1596
|
+
openrouter: { api_key: "" },
|
|
1597
|
+
anthropic: { api_key: "" },
|
|
1598
|
+
openai: { api_key: "" },
|
|
1599
|
+
google: { api_key: "" },
|
|
1600
|
+
ollama: { base_url: "http://localhost:11434" },
|
|
1601
|
+
tuning: { ...TUNING_DEFAULTS }
|
|
1602
|
+
};
|
|
1603
|
+
function loadConfig() {
|
|
1604
|
+
const config = structuredClone(DEFAULTS);
|
|
1605
|
+
if (existsSync5(CONFIG_PATH)) {
|
|
1606
|
+
try {
|
|
1607
|
+
const text = readFileSync5(CONFIG_PATH, "utf-8");
|
|
1608
|
+
const parsed = parseToml(text);
|
|
1609
|
+
if (parsed.default_provider) config.default_provider = parsed.default_provider;
|
|
1610
|
+
if (parsed.default_model) config.default_model = parsed.default_model;
|
|
1611
|
+
if (parsed.openrouter?.api_key) config.openrouter.api_key = parsed.openrouter.api_key;
|
|
1612
|
+
if (parsed.anthropic?.api_key) config.anthropic.api_key = parsed.anthropic.api_key;
|
|
1613
|
+
if (parsed.openai?.api_key) config.openai.api_key = parsed.openai.api_key;
|
|
1614
|
+
if (parsed.google?.api_key) config.google.api_key = parsed.google.api_key;
|
|
1615
|
+
if (parsed.ollama?.base_url) config.ollama.base_url = parsed.ollama.base_url;
|
|
1616
|
+
if (parsed.tuning) {
|
|
1617
|
+
for (const key of Object.keys(TUNING_DEFAULTS)) {
|
|
1618
|
+
const raw = parsed.tuning[key];
|
|
1619
|
+
if (raw === void 0) continue;
|
|
1620
|
+
const num = Number(raw);
|
|
1621
|
+
if (Number.isFinite(num)) config.tuning[key] = num;
|
|
1622
|
+
}
|
|
1623
|
+
}
|
|
1624
|
+
} catch {
|
|
1625
|
+
}
|
|
1626
|
+
}
|
|
1627
|
+
if (process.env.OPENROUTER_API_KEY) config.openrouter.api_key = process.env.OPENROUTER_API_KEY;
|
|
1628
|
+
if (process.env.ANTHROPIC_API_KEY) config.anthropic.api_key = process.env.ANTHROPIC_API_KEY;
|
|
1629
|
+
if (process.env.OPENAI_API_KEY) config.openai.api_key = process.env.OPENAI_API_KEY;
|
|
1630
|
+
if (process.env.GOOGLE_API_KEY) config.google.api_key = process.env.GOOGLE_API_KEY;
|
|
1631
|
+
if (process.env.OLLAMA_HOST) config.ollama.base_url = process.env.OLLAMA_HOST;
|
|
1632
|
+
return config;
|
|
1633
|
+
}
|
|
1634
|
+
var TUNING_BLOCK = `# global tuning knobs. every key is optional. missing entries use the defaults
|
|
1635
|
+
# shown here. CLI flags (--max-files, --max-lines, --no-repomap) override these
|
|
1636
|
+
# on a per-session basis.
|
|
1637
|
+
[tuning]
|
|
1638
|
+
|
|
1639
|
+
# \u2500\u2500 retrieval / repo-map \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
1640
|
+
# the structural floor of the project that prism injects into every system
|
|
1641
|
+
# prompt: paths + symbols per file. bigger map = more context, more tokens.
|
|
1642
|
+
|
|
1643
|
+
# how many source files the walker visits per build. larger repos are sampled.
|
|
1644
|
+
# bumping this trades startup time for broader coverage.
|
|
1645
|
+
# override per-session: --max-files <n>
|
|
1646
|
+
repomap_max_files = 500
|
|
1647
|
+
|
|
1648
|
+
# how many lines the formatted block injects into the system prompt. excess
|
|
1649
|
+
# is truncated and replaced with a "...and N more files" footer the model can
|
|
1650
|
+
# act on with Read / Grep. higher = more structure visible, more tokens/turn.
|
|
1651
|
+
# override per-session: --max-lines <n>
|
|
1652
|
+
repomap_max_lines = 500
|
|
1653
|
+
|
|
1654
|
+
# symbols shown per file in the rendered map. higher = denser detail per file,
|
|
1655
|
+
# fewer files fit. lower = more files visible, less detail each.
|
|
1656
|
+
repomap_max_symbols_per_file = 10
|
|
1657
|
+
|
|
1658
|
+
# \u2500\u2500 memory layer \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
1659
|
+
|
|
1660
|
+
# size cap on lens.md (project-local rules, committed to the repo). only a
|
|
1661
|
+
# sanity bound against runaway files. normal lens.md is well under this.
|
|
1662
|
+
lens_max_bytes = 65536 # 64KB
|
|
1663
|
+
|
|
1664
|
+
# \u2500\u2500 shell escape (! prefix in the prompt) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
1665
|
+
|
|
1666
|
+
# timeout for '!<cmd>' shell escapes typed in the prompt. caps runaway
|
|
1667
|
+
# commands. does not affect the model's own Bash tool calls (those carry
|
|
1668
|
+
# their own timeout argument).
|
|
1669
|
+
bash_timeout_ms = 30000 # 30s
|
|
1670
|
+
|
|
1671
|
+
# size cap on captured output from '!<cmd>' and the Bash tool. excess is
|
|
1672
|
+
# truncated with a marker. protects the UI from 'find /'-style floods.
|
|
1673
|
+
bash_max_output_bytes = 524288 # 512KB
|
|
1674
|
+
|
|
1675
|
+
# \u2500\u2500 engine self-management \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
1676
|
+
|
|
1677
|
+
# fraction of the model's context window at which compaction fires
|
|
1678
|
+
# (trim \u2192 snip \u2192 summarize). 0.8 = "compact once you've used 80%". lower
|
|
1679
|
+
# = compact earlier (safer, more model calls). a secondary snip threshold
|
|
1680
|
+
# derives from this at 0.75\xD7 to give summarize room to breathe.
|
|
1681
|
+
compaction_threshold = 0.8
|
|
1682
|
+
|
|
1683
|
+
# how many consecutive empty model turns the engine will nudge before
|
|
1684
|
+
# giving up. higher = more patience with flaky models. lower = faster
|
|
1685
|
+
# failure on stuck loops.
|
|
1686
|
+
empty_turn_nudge_cap = 2
|
|
1687
|
+
|
|
1688
|
+
# \u2500\u2500 input UX \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
1689
|
+
|
|
1690
|
+
# max visible lines in the input box. typing past this freezes the box
|
|
1691
|
+
# height and scrolls the viewport to keep the cursor visible. above and
|
|
1692
|
+
# below the box, dim indicators show how many lines are hidden.
|
|
1693
|
+
input_viewport_max_lines = 10
|
|
1694
|
+
`;
|
|
1695
|
+
var BASE_TEMPLATE = `# prism config
|
|
1696
|
+
# env vars override these values. CLI flags override env vars and config.
|
|
1697
|
+
|
|
1698
|
+
default_provider = "ollama"
|
|
1699
|
+
default_model = "deepseek-r1:14b"
|
|
1700
|
+
|
|
1701
|
+
[openrouter]
|
|
1702
|
+
api_key = ""
|
|
1703
|
+
|
|
1704
|
+
[anthropic]
|
|
1705
|
+
api_key = ""
|
|
1706
|
+
|
|
1707
|
+
[openai]
|
|
1708
|
+
api_key = ""
|
|
1709
|
+
|
|
1710
|
+
[google]
|
|
1711
|
+
api_key = ""
|
|
1712
|
+
|
|
1713
|
+
[ollama]
|
|
1714
|
+
base_url = "http://localhost:11434"
|
|
1715
|
+
`;
|
|
1716
|
+
var REQUIRED_TABLES = [
|
|
1717
|
+
{ name: "tuning", block: TUNING_BLOCK }
|
|
1718
|
+
];
|
|
1719
|
+
function initConfig() {
|
|
1720
|
+
if (existsSync5(CONFIG_PATH)) return;
|
|
1721
|
+
if (!existsSync5(PRISM_DIR)) {
|
|
1722
|
+
mkdirSync3(PRISM_DIR, { recursive: true });
|
|
1723
|
+
}
|
|
1724
|
+
const fresh = BASE_TEMPLATE + "\n" + TUNING_BLOCK;
|
|
1725
|
+
writeFileSync3(CONFIG_PATH, fresh, "utf-8");
|
|
1726
|
+
}
|
|
1727
|
+
function migrateConfig() {
|
|
1728
|
+
if (!existsSync5(CONFIG_PATH)) return [];
|
|
1729
|
+
let text;
|
|
1730
|
+
try {
|
|
1731
|
+
text = readFileSync5(CONFIG_PATH, "utf-8");
|
|
1732
|
+
} catch {
|
|
1733
|
+
return [];
|
|
1734
|
+
}
|
|
1735
|
+
let parsed;
|
|
1736
|
+
try {
|
|
1737
|
+
parsed = parseToml(text);
|
|
1738
|
+
} catch {
|
|
1739
|
+
return [];
|
|
1740
|
+
}
|
|
1741
|
+
const missing = REQUIRED_TABLES.filter((t) => !parsed[t.name]);
|
|
1742
|
+
if (missing.length === 0) return [];
|
|
1743
|
+
const head = text.replace(/\s+$/, "");
|
|
1744
|
+
const appended = missing.map((t) => t.block).join("\n");
|
|
1745
|
+
const next = head + "\n\n" + appended + "\n";
|
|
1746
|
+
try {
|
|
1747
|
+
writeFileSync3(CONFIG_PATH, next, "utf-8");
|
|
1748
|
+
return missing.map((t) => t.name);
|
|
1749
|
+
} catch {
|
|
1750
|
+
return [];
|
|
1751
|
+
}
|
|
1752
|
+
}
|
|
1753
|
+
function getConfigPath() {
|
|
1754
|
+
return CONFIG_PATH;
|
|
1755
|
+
}
|
|
1756
|
+
function parseToml(text) {
|
|
1757
|
+
const result = {};
|
|
1758
|
+
let currentSection = result;
|
|
1759
|
+
for (const line of text.split("\n")) {
|
|
1760
|
+
const trimmed = line.trim();
|
|
1761
|
+
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
1762
|
+
const sectionMatch = trimmed.match(/^\[([^\]]+)\]\s*(?:#.*)?$/);
|
|
1763
|
+
if (sectionMatch) {
|
|
1764
|
+
const key = sectionMatch[1];
|
|
1765
|
+
result[key] = result[key] || {};
|
|
1766
|
+
currentSection = result[key];
|
|
1767
|
+
continue;
|
|
1768
|
+
}
|
|
1769
|
+
const kvMatch = trimmed.match(/^(\w+)\s*=\s*"([^"]*)"$/);
|
|
1770
|
+
if (kvMatch) {
|
|
1771
|
+
currentSection[kvMatch[1]] = kvMatch[2];
|
|
1772
|
+
continue;
|
|
1773
|
+
}
|
|
1774
|
+
const kvUnquoted = trimmed.match(/^(\w+)\s*=\s*(.+)$/);
|
|
1775
|
+
if (kvUnquoted) {
|
|
1776
|
+
currentSection[kvUnquoted[1]] = kvUnquoted[2].replace(/\s+#.*$/, "").trim();
|
|
1777
|
+
}
|
|
1778
|
+
}
|
|
1779
|
+
return result;
|
|
1780
|
+
}
|
|
1781
|
+
|
|
1782
|
+
// src/ui/spinnerPhrases.ts
|
|
1783
|
+
var STUCK_THRESHOLD_SEC = 30;
|
|
1784
|
+
var PHRASES = {
|
|
1785
|
+
planMode: [
|
|
1786
|
+
"mapping it out",
|
|
1787
|
+
"drafting the approach",
|
|
1788
|
+
"tracing the seam",
|
|
1789
|
+
"weighing the options",
|
|
1790
|
+
"before committing",
|
|
1791
|
+
"sketching the shape"
|
|
1792
|
+
],
|
|
1793
|
+
stuck: [
|
|
1794
|
+
"still working",
|
|
1795
|
+
"taking longer than usual",
|
|
1796
|
+
"pushing through",
|
|
1797
|
+
"still cooking",
|
|
1798
|
+
"this one is a puzzle",
|
|
1799
|
+
"sticking with it"
|
|
1800
|
+
],
|
|
1801
|
+
thinking: [
|
|
1802
|
+
"thinkering",
|
|
1803
|
+
"considering",
|
|
1804
|
+
"thinking",
|
|
1805
|
+
"give me a moment",
|
|
1806
|
+
"hold on lemme think",
|
|
1807
|
+
"figuring it out",
|
|
1808
|
+
"reasoning through",
|
|
1809
|
+
"cooking",
|
|
1810
|
+
"consulting the vibes"
|
|
1811
|
+
],
|
|
1812
|
+
Read: [
|
|
1813
|
+
"reading",
|
|
1814
|
+
"pulling up the file",
|
|
1815
|
+
"scanning the source",
|
|
1816
|
+
"peeking",
|
|
1817
|
+
"taking a look"
|
|
1818
|
+
],
|
|
1819
|
+
Edit: [
|
|
1820
|
+
"editing",
|
|
1821
|
+
"applying the patch",
|
|
1822
|
+
"rewriting in place",
|
|
1823
|
+
"tweaking it",
|
|
1824
|
+
"making the change"
|
|
1825
|
+
],
|
|
1826
|
+
Write: [
|
|
1827
|
+
"writing",
|
|
1828
|
+
"saving to disk",
|
|
1829
|
+
"putting it down",
|
|
1830
|
+
"committing the file"
|
|
1831
|
+
],
|
|
1832
|
+
Bash: [
|
|
1833
|
+
"running the shell",
|
|
1834
|
+
"firing the command",
|
|
1835
|
+
"on the terminal",
|
|
1836
|
+
"invoking the command"
|
|
1837
|
+
],
|
|
1838
|
+
Verify: [
|
|
1839
|
+
"running tests",
|
|
1840
|
+
"confirming the change",
|
|
1841
|
+
"asking the suite",
|
|
1842
|
+
"checking the work"
|
|
1843
|
+
],
|
|
1844
|
+
Agent: [
|
|
1845
|
+
"briefing a subagent to help me out",
|
|
1846
|
+
"delegating the work",
|
|
1847
|
+
"splitting the work with a team mate",
|
|
1848
|
+
"passing it down to someone else"
|
|
1849
|
+
],
|
|
1850
|
+
Glob: [
|
|
1851
|
+
"globbing",
|
|
1852
|
+
"finding files",
|
|
1853
|
+
"walking the tree",
|
|
1854
|
+
"looking around"
|
|
1855
|
+
],
|
|
1856
|
+
Grep: [
|
|
1857
|
+
"grepping",
|
|
1858
|
+
"searching the source",
|
|
1859
|
+
"pattern matching"
|
|
1860
|
+
],
|
|
1861
|
+
WebFetch: [
|
|
1862
|
+
"fetching",
|
|
1863
|
+
"reading the page"
|
|
1864
|
+
],
|
|
1865
|
+
WebSearch: [
|
|
1866
|
+
"searching the web",
|
|
1867
|
+
"looking it up",
|
|
1868
|
+
"consulting the world wide web"
|
|
1869
|
+
],
|
|
1870
|
+
useSkill: [
|
|
1871
|
+
"loading the skill",
|
|
1872
|
+
"invoking the routine"
|
|
1873
|
+
],
|
|
1874
|
+
afterTool: [
|
|
1875
|
+
"piecing it together",
|
|
1876
|
+
"reading the result",
|
|
1877
|
+
"connecting it back",
|
|
1878
|
+
"making sense of it",
|
|
1879
|
+
"looking at what came back"
|
|
1880
|
+
]
|
|
1881
|
+
};
|
|
1882
|
+
function chooseBucket(ctx) {
|
|
1883
|
+
if (ctx.inPlanMode && ctx.phase === "thinking") return "planMode";
|
|
1884
|
+
if (ctx.phase === "thinking" && ctx.elapsedSec >= STUCK_THRESHOLD_SEC) return "stuck";
|
|
1885
|
+
if (ctx.phase === "running" && ctx.tool && ctx.tool in PHRASES) return ctx.tool;
|
|
1886
|
+
if (ctx.phase === "after-tool") return "afterTool";
|
|
1887
|
+
return "thinking";
|
|
1888
|
+
}
|
|
1889
|
+
function pickPhrase(ctx, random = Math.random) {
|
|
1890
|
+
const bucket = chooseBucket(ctx);
|
|
1891
|
+
const pool = PHRASES[bucket] ?? PHRASES.thinking;
|
|
1892
|
+
const recent = ctx.recentPhrases;
|
|
1893
|
+
let candidates = pool;
|
|
1894
|
+
if (recent && recent.length > 0) {
|
|
1895
|
+
const filtered = pool.filter((p) => !recent.includes(p));
|
|
1896
|
+
if (filtered.length > 0) candidates = filtered;
|
|
1897
|
+
}
|
|
1898
|
+
const idx = Math.floor(random() * candidates.length);
|
|
1899
|
+
return candidates[idx] ?? candidates[0];
|
|
1900
|
+
}
|
|
1901
|
+
|
|
1180
1902
|
// src/ui/PromptInput.tsx
|
|
1181
1903
|
import { jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
|
|
1182
|
-
var
|
|
1183
|
-
|
|
1904
|
+
var SPINNER_FRAMES = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
|
|
1905
|
+
var SPINNER_INTERVAL_MS = 80;
|
|
1906
|
+
var PHRASE_ROTATE_MS = 8e3;
|
|
1907
|
+
var PHRASE_RECENT_WINDOW = 3;
|
|
1908
|
+
var PASTE_PILL_THRESHOLD = 1e3;
|
|
1909
|
+
var PromptInput = memo(function PromptInput2({ onSubmit, isLoading, inPlanMode, invokeSkills = [], activity, phase = "thinking", currentTool }) {
|
|
1910
|
+
const bufferRef = useRef(createBuffer());
|
|
1911
|
+
const pasteStoreRef = useRef(/* @__PURE__ */ new Map());
|
|
1184
1912
|
const cursorRef = useRef(0);
|
|
1185
|
-
const [
|
|
1913
|
+
const [displayBuf, setDisplayBuf] = useState([]);
|
|
1186
1914
|
const [cursorPos, setCursorPos] = useState(0);
|
|
1187
1915
|
const [selectedHintIdx, setSelectedHintIdx] = useState(0);
|
|
1916
|
+
const [tick, setTick] = useState(0);
|
|
1917
|
+
const [startedAt, setStartedAt] = useState(null);
|
|
1918
|
+
const [phrase, setPhrase] = useState("thinking");
|
|
1919
|
+
const recentPhrasesRef = useRef([]);
|
|
1188
1920
|
const timerRef = useRef(null);
|
|
1921
|
+
const escapeTimerRef = useRef(null);
|
|
1189
1922
|
const flushNow = useCallback(() => {
|
|
1190
1923
|
if (timerRef.current) {
|
|
1191
1924
|
clearTimeout(timerRef.current);
|
|
1192
1925
|
timerRef.current = null;
|
|
1193
1926
|
}
|
|
1194
|
-
|
|
1927
|
+
setDisplayBuf(bufferRef.current);
|
|
1195
1928
|
setCursorPos(cursorRef.current);
|
|
1196
1929
|
}, []);
|
|
1197
1930
|
const scheduleDisplayUpdate = useCallback(() => {
|
|
1198
1931
|
if (timerRef.current) return;
|
|
1199
1932
|
timerRef.current = setTimeout(() => {
|
|
1200
|
-
|
|
1933
|
+
setDisplayBuf(bufferRef.current);
|
|
1201
1934
|
setCursorPos(cursorRef.current);
|
|
1202
1935
|
timerRef.current = null;
|
|
1203
1936
|
}, 16);
|
|
@@ -1205,13 +1938,82 @@ var PromptInput = memo(function PromptInput2({ onSubmit, isLoading, inPlanMode,
|
|
|
1205
1938
|
useEffect(() => {
|
|
1206
1939
|
return () => {
|
|
1207
1940
|
if (timerRef.current) clearTimeout(timerRef.current);
|
|
1941
|
+
if (escapeTimerRef.current) clearTimeout(escapeTimerRef.current);
|
|
1208
1942
|
};
|
|
1209
1943
|
}, []);
|
|
1210
|
-
|
|
1944
|
+
useEffect(() => {
|
|
1945
|
+
if (!isLoading) {
|
|
1946
|
+
setStartedAt(null);
|
|
1947
|
+
return;
|
|
1948
|
+
}
|
|
1949
|
+
setStartedAt(Date.now());
|
|
1950
|
+
setTick(0);
|
|
1951
|
+
const id = setInterval(() => setTick((t) => t + 1), SPINNER_INTERVAL_MS);
|
|
1952
|
+
return () => clearInterval(id);
|
|
1953
|
+
}, [isLoading]);
|
|
1954
|
+
const startedAtRef = useRef(null);
|
|
1955
|
+
useEffect(() => {
|
|
1956
|
+
startedAtRef.current = startedAt;
|
|
1957
|
+
}, [startedAt]);
|
|
1958
|
+
useEffect(() => {
|
|
1959
|
+
if (!isLoading) {
|
|
1960
|
+
recentPhrasesRef.current = [];
|
|
1961
|
+
setPhrase("thinking");
|
|
1962
|
+
return;
|
|
1963
|
+
}
|
|
1964
|
+
const repick = () => {
|
|
1965
|
+
const elapsed = startedAtRef.current ? Math.floor((Date.now() - startedAtRef.current) / 1e3) : 0;
|
|
1966
|
+
const next = pickPhrase({
|
|
1967
|
+
phase,
|
|
1968
|
+
tool: currentTool,
|
|
1969
|
+
inPlanMode: !!inPlanMode,
|
|
1970
|
+
elapsedSec: elapsed,
|
|
1971
|
+
recentPhrases: recentPhrasesRef.current
|
|
1972
|
+
});
|
|
1973
|
+
const updated = [...recentPhrasesRef.current, next];
|
|
1974
|
+
if (updated.length > PHRASE_RECENT_WINDOW) updated.shift();
|
|
1975
|
+
recentPhrasesRef.current = updated;
|
|
1976
|
+
setPhrase(next);
|
|
1977
|
+
};
|
|
1978
|
+
repick();
|
|
1979
|
+
const id = setInterval(repick, PHRASE_ROTATE_MS);
|
|
1980
|
+
return () => clearInterval(id);
|
|
1981
|
+
}, [isLoading, phase, currentTool, inPlanMode]);
|
|
1982
|
+
const clearBufferNow = useCallback(() => {
|
|
1983
|
+
bufferRef.current = createBuffer();
|
|
1984
|
+
pasteStoreRef.current = /* @__PURE__ */ new Map();
|
|
1985
|
+
cursorRef.current = 0;
|
|
1986
|
+
flushNow();
|
|
1987
|
+
}, [flushNow]);
|
|
1988
|
+
const ignoreNextReturnRef = useRef(false);
|
|
1989
|
+
useEffect(() => {
|
|
1990
|
+
if (isLoading) return;
|
|
1991
|
+
const stdin = process.stdin;
|
|
1992
|
+
if (!stdin || !stdin.isTTY) return;
|
|
1993
|
+
const handler = (chunk) => {
|
|
1994
|
+
const s = typeof chunk === "string" ? chunk : chunk.toString("utf-8");
|
|
1995
|
+
const isNewlineEnter = s === "\x1B\r" || s === "\x1B\n" || /^\x1b\[13;\d+u$/.test(s) || /^\x1b\[27;\d+;13~$/.test(s);
|
|
1996
|
+
if (!isNewlineEnter) return;
|
|
1997
|
+
bufferRef.current = insertText(bufferRef.current, cursorRef.current, "\n");
|
|
1998
|
+
cursorRef.current += 1;
|
|
1999
|
+
scheduleDisplayUpdate();
|
|
2000
|
+
ignoreNextReturnRef.current = true;
|
|
2001
|
+
setTimeout(() => {
|
|
2002
|
+
ignoreNextReturnRef.current = false;
|
|
2003
|
+
}, 50);
|
|
2004
|
+
};
|
|
2005
|
+
stdin.prependListener("data", handler);
|
|
2006
|
+
return () => {
|
|
2007
|
+
stdin.off("data", handler);
|
|
2008
|
+
};
|
|
2009
|
+
}, [isLoading, scheduleDisplayUpdate]);
|
|
2010
|
+
const allText = displayBuf.every((s) => s.kind === "text");
|
|
2011
|
+
const displayText = allText ? displayBuf.map((s) => s.chars).join("") : "";
|
|
2012
|
+
const parts = displayText.split(/\s+/);
|
|
1211
2013
|
const firstWord = parts[0] || "";
|
|
1212
|
-
const isSkillCompletion = firstWord === "/run" && parts.length <= 2;
|
|
1213
|
-
const isSectionCompletion = firstWord === "/run" && parts.length >= 3;
|
|
1214
|
-
const isCmdCompletion =
|
|
2014
|
+
const isSkillCompletion = allText && firstWord === "/run" && parts.length <= 2;
|
|
2015
|
+
const isSectionCompletion = allText && firstWord === "/run" && parts.length >= 3;
|
|
2016
|
+
const isCmdCompletion = allText && displayText.startsWith("/") && !displayText.includes(" ") && parts.length === 1 && !isSkillCompletion;
|
|
1215
2017
|
const showHints = isCmdCompletion || isSkillCompletion || isSectionCompletion;
|
|
1216
2018
|
const matches = useMemo(() => {
|
|
1217
2019
|
if (!showHints) return [];
|
|
@@ -1233,8 +2035,45 @@ var PromptInput = memo(function PromptInput2({ onSubmit, isLoading, inPlanMode,
|
|
|
1233
2035
|
useEffect(() => {
|
|
1234
2036
|
setSelectedHintIdx(0);
|
|
1235
2037
|
}, [firstWord, showHints]);
|
|
2038
|
+
const RAW_WORD_LEFT = "\x1B[1;3D";
|
|
2039
|
+
const RAW_WORD_RIGHT = "\x1B[1;3C";
|
|
2040
|
+
const RAW_WORD_LEFT_CTRL = "\x1B[1;5D";
|
|
2041
|
+
const RAW_WORD_RIGHT_CTRL = "\x1B[1;5C";
|
|
2042
|
+
const RAW_DEL_WORD = "\x1B\x7F";
|
|
2043
|
+
const RAW_DEL_WORD_ALT = "";
|
|
2044
|
+
const RAW_KILL_LINE = "\v";
|
|
2045
|
+
const RAW_NEWLINE_ESC_CR = "\x1B\r";
|
|
2046
|
+
const RAW_NEWLINE_ESC_LF = "\x1B\n";
|
|
1236
2047
|
useInput((input, key) => {
|
|
1237
2048
|
if (isLoading) return;
|
|
2049
|
+
if (key.return && ignoreNextReturnRef.current) {
|
|
2050
|
+
ignoreNextReturnRef.current = false;
|
|
2051
|
+
return;
|
|
2052
|
+
}
|
|
2053
|
+
const isNewlineChord = key.return && (key.shift || key.meta) || input === RAW_NEWLINE_ESC_CR || input === RAW_NEWLINE_ESC_LF;
|
|
2054
|
+
if (isNewlineChord) {
|
|
2055
|
+
if (escapeTimerRef.current) {
|
|
2056
|
+
clearTimeout(escapeTimerRef.current);
|
|
2057
|
+
escapeTimerRef.current = null;
|
|
2058
|
+
}
|
|
2059
|
+
bufferRef.current = insertText(bufferRef.current, cursorRef.current, "\n");
|
|
2060
|
+
cursorRef.current += 1;
|
|
2061
|
+
scheduleDisplayUpdate();
|
|
2062
|
+
return;
|
|
2063
|
+
}
|
|
2064
|
+
if (key.return && escapeTimerRef.current) {
|
|
2065
|
+
clearTimeout(escapeTimerRef.current);
|
|
2066
|
+
escapeTimerRef.current = null;
|
|
2067
|
+
bufferRef.current = insertText(bufferRef.current, cursorRef.current, "\n");
|
|
2068
|
+
cursorRef.current += 1;
|
|
2069
|
+
scheduleDisplayUpdate();
|
|
2070
|
+
return;
|
|
2071
|
+
}
|
|
2072
|
+
if (escapeTimerRef.current && !key.escape) {
|
|
2073
|
+
clearTimeout(escapeTimerRef.current);
|
|
2074
|
+
escapeTimerRef.current = null;
|
|
2075
|
+
clearBufferNow();
|
|
2076
|
+
}
|
|
1238
2077
|
if (matches.length > 0) {
|
|
1239
2078
|
if (key.upArrow) {
|
|
1240
2079
|
setSelectedHintIdx((prev) => Math.max(0, prev - 1));
|
|
@@ -1245,12 +2084,12 @@ var PromptInput = memo(function PromptInput2({ onSubmit, isLoading, inPlanMode,
|
|
|
1245
2084
|
return;
|
|
1246
2085
|
}
|
|
1247
2086
|
if (key.tab || key.return) {
|
|
1248
|
-
const
|
|
1249
|
-
const liveParts =
|
|
2087
|
+
const liveText = bufferRef.current.every((s) => s.kind === "text") ? bufferRef.current.map((s) => s.chars).join("") : "";
|
|
2088
|
+
const liveParts = liveText.split(/\s+/);
|
|
1250
2089
|
const liveFirst = liveParts[0] ?? "";
|
|
1251
2090
|
const liveIsSkillCompletion = liveFirst === "/run" && liveParts.length <= 2;
|
|
1252
2091
|
const liveIsSectionCompletion = liveFirst === "/run" && liveParts.length >= 3;
|
|
1253
|
-
const liveIsCmdCompletion =
|
|
2092
|
+
const liveIsCmdCompletion = liveText.startsWith("/") && !liveText.includes(" ") && liveParts.length === 1 && !liveIsSkillCompletion;
|
|
1254
2093
|
const liveShowHints = liveIsCmdCompletion || liveIsSkillCompletion || liveIsSectionCompletion;
|
|
1255
2094
|
let liveMatches;
|
|
1256
2095
|
if (liveIsSectionCompletion) {
|
|
@@ -1272,9 +2111,9 @@ var PromptInput = memo(function PromptInput2({ onSubmit, isLoading, inPlanMode,
|
|
|
1272
2111
|
const selected = liveMatches[selectedHintIdx] ?? liveMatches[0];
|
|
1273
2112
|
if (selected) {
|
|
1274
2113
|
const newText = liveIsSectionCompletion ? `/run ${liveParts[1]} ${selected.name} ` : liveIsSkillCompletion ? `/run ${selected.name} ` : selected.name + (selected.args ? " " : "");
|
|
1275
|
-
if (
|
|
1276
|
-
bufferRef.current = newText;
|
|
1277
|
-
cursorRef.current = newText.length;
|
|
2114
|
+
if (liveText !== newText) {
|
|
2115
|
+
bufferRef.current = [{ kind: "text", chars: newText }];
|
|
2116
|
+
cursorRef.current = [...newText].length;
|
|
1278
2117
|
flushNow();
|
|
1279
2118
|
return;
|
|
1280
2119
|
}
|
|
@@ -1283,28 +2122,55 @@ var PromptInput = memo(function PromptInput2({ onSubmit, isLoading, inPlanMode,
|
|
|
1283
2122
|
}
|
|
1284
2123
|
}
|
|
1285
2124
|
if (key.return) {
|
|
1286
|
-
const text = bufferRef.current.trim();
|
|
2125
|
+
const text = expand(bufferRef.current, pasteStoreRef.current).trim();
|
|
1287
2126
|
if (text) {
|
|
1288
2127
|
onSubmit(text);
|
|
1289
|
-
bufferRef.current =
|
|
2128
|
+
bufferRef.current = createBuffer();
|
|
2129
|
+
pasteStoreRef.current = /* @__PURE__ */ new Map();
|
|
1290
2130
|
cursorRef.current = 0;
|
|
1291
2131
|
flushNow();
|
|
1292
2132
|
}
|
|
1293
2133
|
return;
|
|
1294
2134
|
}
|
|
2135
|
+
if (input === RAW_DEL_WORD || input === RAW_DEL_WORD_ALT || key.ctrl && input === "w") {
|
|
2136
|
+
const { buf, pos } = deleteWordBack(bufferRef.current, cursorRef.current);
|
|
2137
|
+
bufferRef.current = buf;
|
|
2138
|
+
cursorRef.current = pos;
|
|
2139
|
+
scheduleDisplayUpdate();
|
|
2140
|
+
return;
|
|
2141
|
+
}
|
|
2142
|
+
if (input === RAW_KILL_LINE || key.ctrl && input === "k") {
|
|
2143
|
+
bufferRef.current = killToEnd(bufferRef.current, cursorRef.current);
|
|
2144
|
+
scheduleDisplayUpdate();
|
|
2145
|
+
return;
|
|
2146
|
+
}
|
|
2147
|
+
if (input === RAW_WORD_LEFT || input === RAW_WORD_LEFT_CTRL || key.meta && key.leftArrow) {
|
|
2148
|
+
cursorRef.current = wordBoundaryLeft(bufferRef.current, cursorRef.current);
|
|
2149
|
+
scheduleDisplayUpdate();
|
|
2150
|
+
return;
|
|
2151
|
+
}
|
|
2152
|
+
if (input === RAW_WORD_RIGHT || input === RAW_WORD_RIGHT_CTRL || key.meta && key.rightArrow) {
|
|
2153
|
+
cursorRef.current = wordBoundaryRight(bufferRef.current, cursorRef.current);
|
|
2154
|
+
scheduleDisplayUpdate();
|
|
2155
|
+
return;
|
|
2156
|
+
}
|
|
1295
2157
|
if (key.backspace || key.delete) {
|
|
1296
|
-
const
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
scheduleDisplayUpdate();
|
|
1301
|
-
}
|
|
2158
|
+
const { buf, pos } = deleteBack(bufferRef.current, cursorRef.current);
|
|
2159
|
+
bufferRef.current = buf;
|
|
2160
|
+
cursorRef.current = pos;
|
|
2161
|
+
scheduleDisplayUpdate();
|
|
1302
2162
|
return;
|
|
1303
2163
|
}
|
|
1304
|
-
if (key.ctrl && input === "u"
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
2164
|
+
if (key.ctrl && input === "u") {
|
|
2165
|
+
clearBufferNow();
|
|
2166
|
+
return;
|
|
2167
|
+
}
|
|
2168
|
+
if (key.escape) {
|
|
2169
|
+
if (escapeTimerRef.current) clearTimeout(escapeTimerRef.current);
|
|
2170
|
+
escapeTimerRef.current = setTimeout(() => {
|
|
2171
|
+
clearBufferNow();
|
|
2172
|
+
escapeTimerRef.current = null;
|
|
2173
|
+
}, 50);
|
|
1308
2174
|
return;
|
|
1309
2175
|
}
|
|
1310
2176
|
if (key.leftArrow) {
|
|
@@ -1313,7 +2179,7 @@ var PromptInput = memo(function PromptInput2({ onSubmit, isLoading, inPlanMode,
|
|
|
1313
2179
|
return;
|
|
1314
2180
|
}
|
|
1315
2181
|
if (key.rightArrow) {
|
|
1316
|
-
cursorRef.current = Math.min(bufferRef.current
|
|
2182
|
+
cursorRef.current = Math.min(flatLength(bufferRef.current), cursorRef.current + 1);
|
|
1317
2183
|
scheduleDisplayUpdate();
|
|
1318
2184
|
return;
|
|
1319
2185
|
}
|
|
@@ -1323,57 +2189,158 @@ var PromptInput = memo(function PromptInput2({ onSubmit, isLoading, inPlanMode,
|
|
|
1323
2189
|
return;
|
|
1324
2190
|
}
|
|
1325
2191
|
if (key.ctrl && input === "e") {
|
|
1326
|
-
cursorRef.current = bufferRef.current
|
|
2192
|
+
cursorRef.current = flatLength(bufferRef.current);
|
|
2193
|
+
scheduleDisplayUpdate();
|
|
2194
|
+
return;
|
|
2195
|
+
}
|
|
2196
|
+
if (key.upArrow) {
|
|
2197
|
+
cursorRef.current = moveCursorUp(bufferRef.current, cursorRef.current);
|
|
2198
|
+
scheduleDisplayUpdate();
|
|
2199
|
+
return;
|
|
2200
|
+
}
|
|
2201
|
+
if (key.downArrow) {
|
|
2202
|
+
cursorRef.current = moveCursorDown(bufferRef.current, cursorRef.current);
|
|
1327
2203
|
scheduleDisplayUpdate();
|
|
1328
2204
|
return;
|
|
1329
2205
|
}
|
|
1330
|
-
if (key.ctrl || key.meta || key.
|
|
2206
|
+
if (key.ctrl || key.meta || key.tab) {
|
|
1331
2207
|
return;
|
|
1332
2208
|
}
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
2209
|
+
if (input.length >= PASTE_PILL_THRESHOLD) {
|
|
2210
|
+
bufferRef.current = insertPill(bufferRef.current, cursorRef.current, input, pasteStoreRef.current);
|
|
2211
|
+
cursorRef.current += 1;
|
|
2212
|
+
flushNow();
|
|
2213
|
+
return;
|
|
2214
|
+
}
|
|
2215
|
+
bufferRef.current = insertText(bufferRef.current, cursorRef.current, input);
|
|
2216
|
+
cursorRef.current += [...input].length;
|
|
1336
2217
|
scheduleDisplayUpdate();
|
|
1337
2218
|
}, { isActive: !isLoading });
|
|
1338
2219
|
if (isLoading) {
|
|
2220
|
+
const frame = SPINNER_FRAMES[tick % SPINNER_FRAMES.length];
|
|
2221
|
+
const label = activity ?? phrase;
|
|
2222
|
+
const elapsedSec = startedAt ? Math.floor((Date.now() - startedAt) / 1e3) : 0;
|
|
1339
2223
|
return /* @__PURE__ */ jsxs5(Box5, { marginTop: 1, flexDirection: "column", children: [
|
|
1340
2224
|
/* @__PURE__ */ jsxs5(Box5, { children: [
|
|
1341
|
-
/* @__PURE__ */
|
|
1342
|
-
|
|
2225
|
+
/* @__PURE__ */ jsxs5(Text5, { color: theme.spinner, children: [
|
|
2226
|
+
frame,
|
|
2227
|
+
" "
|
|
2228
|
+
] }),
|
|
2229
|
+
/* @__PURE__ */ jsxs5(Text5, { color: theme.textDim, children: [
|
|
2230
|
+
label,
|
|
2231
|
+
"..."
|
|
2232
|
+
] }),
|
|
2233
|
+
elapsedSec >= 1 && /* @__PURE__ */ jsxs5(Text5, { color: theme.textMuted, children: [
|
|
2234
|
+
" \xB7 ",
|
|
2235
|
+
elapsedSec,
|
|
2236
|
+
"s"
|
|
2237
|
+
] })
|
|
1343
2238
|
] }),
|
|
1344
2239
|
/* @__PURE__ */ jsx5(Box5, { children: /* @__PURE__ */ jsx5(Text5, { color: theme.textMuted, children: " esc to interrupt" }) })
|
|
1345
2240
|
] });
|
|
1346
2241
|
}
|
|
1347
|
-
const
|
|
2242
|
+
const firstSeg = displayBuf[0];
|
|
2243
|
+
const isShell = firstSeg?.kind === "text" && firstSeg.chars.startsWith("!");
|
|
1348
2244
|
const isPlanInput = inPlanMode && !isShell;
|
|
1349
2245
|
const promptChar = isShell ? "$" : isPlanInput ? "\u25C7" : "\u25C6";
|
|
1350
2246
|
const accent = isShell ? theme.warning : isPlanInput ? theme.planMode : theme.prompt;
|
|
1351
|
-
const
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
const
|
|
2247
|
+
const visibleSegs = isShell ? [
|
|
2248
|
+
{ kind: "text", chars: firstSeg.chars.slice(1) },
|
|
2249
|
+
...displayBuf.slice(1)
|
|
2250
|
+
] : displayBuf;
|
|
2251
|
+
const initialVisibleCursor = isShell ? Math.max(0, cursorPos - 1) : cursorPos;
|
|
2252
|
+
const maxLines = loadConfig().tuning.input_viewport_max_lines;
|
|
2253
|
+
const total = totalLines(visibleSegs);
|
|
2254
|
+
let renderSegs = visibleSegs;
|
|
2255
|
+
let renderCursor = initialVisibleCursor;
|
|
2256
|
+
let hiddenAbove = 0;
|
|
2257
|
+
let hiddenBelow = 0;
|
|
2258
|
+
if (total > maxLines) {
|
|
2259
|
+
const { line: curLine } = flatToLineCol(visibleSegs, initialVisibleCursor);
|
|
2260
|
+
const start = Math.max(0, Math.min(total - maxLines, curLine - Math.floor(maxLines / 2)));
|
|
2261
|
+
const end = start + maxLines;
|
|
2262
|
+
const sliced = sliceToLines(visibleSegs, start, end, initialVisibleCursor);
|
|
2263
|
+
renderSegs = sliced.buf;
|
|
2264
|
+
renderCursor = sliced.cursor;
|
|
2265
|
+
hiddenAbove = start;
|
|
2266
|
+
hiddenBelow = total - end;
|
|
2267
|
+
}
|
|
2268
|
+
const renderAtomLen = renderSegs.reduce(
|
|
2269
|
+
(acc, s) => acc + (s.kind === "text" ? [...s.chars].length : 1),
|
|
2270
|
+
0
|
|
2271
|
+
);
|
|
2272
|
+
const cursorOnEnd = renderCursor >= renderAtomLen;
|
|
2273
|
+
const bufferEmpty = visibleSegs.length === 0;
|
|
1356
2274
|
return /* @__PURE__ */ jsxs5(Box5, { marginTop: 1, flexDirection: "column", children: [
|
|
1357
2275
|
isShell && /* @__PURE__ */ jsx5(Text5, { color: theme.textMuted, children: " shell mode (delete the `!` to exit, or esc to clear. output stays here, the model won't see it)" }),
|
|
1358
2276
|
isPlanInput && /* @__PURE__ */ jsxs5(Text5, { color: theme.planMode, children: [
|
|
1359
2277
|
" plan mode ",
|
|
1360
2278
|
/* @__PURE__ */ jsx5(Text5, { color: theme.textMuted, children: "(type /exec-plan to execute, /cancel-plan to abandon, or push back to revise)" })
|
|
1361
2279
|
] }),
|
|
1362
|
-
/* @__PURE__ */ jsxs5(
|
|
2280
|
+
hiddenAbove > 0 && /* @__PURE__ */ jsxs5(Text5, { color: theme.textMuted, children: [
|
|
2281
|
+
" \u2191 ",
|
|
2282
|
+
hiddenAbove,
|
|
2283
|
+
" more line",
|
|
2284
|
+
hiddenAbove === 1 ? "" : "s",
|
|
2285
|
+
" above"
|
|
2286
|
+
] }),
|
|
2287
|
+
/* @__PURE__ */ jsx5(Box5, { borderStyle: "round", borderColor: accent, paddingX: 1, children: /* @__PURE__ */ jsxs5(Text5, { wrap: "wrap", color: isShell ? theme.warning : void 0, children: [
|
|
1363
2288
|
/* @__PURE__ */ jsxs5(Text5, { color: accent, children: [
|
|
1364
2289
|
promptChar,
|
|
1365
2290
|
" "
|
|
1366
2291
|
] }),
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
2292
|
+
renderSegments(renderSegs, renderCursor),
|
|
2293
|
+
cursorOnEnd && /* @__PURE__ */ jsx5(Text5, { inverse: true, children: " " }),
|
|
2294
|
+
bufferEmpty && /* @__PURE__ */ jsxs5(Text5, { color: theme.textMuted, children: [
|
|
2295
|
+
"ask anything... ",
|
|
2296
|
+
/* @__PURE__ */ jsx5(Text5, { dimColor: true, children: "(shift+enter for newline)" })
|
|
1372
2297
|
] })
|
|
2298
|
+
] }) }),
|
|
2299
|
+
hiddenBelow > 0 && /* @__PURE__ */ jsxs5(Text5, { color: theme.textMuted, children: [
|
|
2300
|
+
" \u2193 ",
|
|
2301
|
+
hiddenBelow,
|
|
2302
|
+
" more line",
|
|
2303
|
+
hiddenBelow === 1 ? "" : "s",
|
|
2304
|
+
" below"
|
|
1373
2305
|
] }),
|
|
1374
2306
|
/* @__PURE__ */ jsx5(SlashHints, { matches, selectedIdx: selectedHintIdx })
|
|
1375
2307
|
] });
|
|
1376
2308
|
});
|
|
2309
|
+
function renderSegments(segs, cursor) {
|
|
2310
|
+
const nodes = [];
|
|
2311
|
+
let pos = 0;
|
|
2312
|
+
for (let i = 0; i < segs.length; i++) {
|
|
2313
|
+
const seg = segs[i];
|
|
2314
|
+
if (seg.kind === "text") {
|
|
2315
|
+
const len = seg.chars.length;
|
|
2316
|
+
if (cursor >= pos && cursor < pos + len) {
|
|
2317
|
+
const off = cursor - pos;
|
|
2318
|
+
const before = seg.chars.slice(0, off);
|
|
2319
|
+
const ch = seg.chars[off] ?? " ";
|
|
2320
|
+
const after = seg.chars.slice(off + 1);
|
|
2321
|
+
if (before) nodes.push(/* @__PURE__ */ jsx5(Text5, { children: before }, `t-${i}-pre`));
|
|
2322
|
+
if (ch === "\n") {
|
|
2323
|
+
nodes.push(/* @__PURE__ */ jsx5(Text5, { inverse: true, children: " " }, `t-${i}-cur`));
|
|
2324
|
+
nodes.push(/* @__PURE__ */ jsx5(Text5, { children: "\n" }, `t-${i}-nl`));
|
|
2325
|
+
} else {
|
|
2326
|
+
nodes.push(/* @__PURE__ */ jsx5(Text5, { inverse: true, children: ch }, `t-${i}-cur`));
|
|
2327
|
+
}
|
|
2328
|
+
if (after) nodes.push(/* @__PURE__ */ jsx5(Text5, { children: after }, `t-${i}-post`));
|
|
2329
|
+
} else {
|
|
2330
|
+
nodes.push(/* @__PURE__ */ jsx5(Text5, { children: seg.chars }, `t-${i}`));
|
|
2331
|
+
}
|
|
2332
|
+
pos += len;
|
|
2333
|
+
} else {
|
|
2334
|
+
const label = `[paste ${seg.size} chars]`;
|
|
2335
|
+
const onPill = cursor === pos;
|
|
2336
|
+
nodes.push(
|
|
2337
|
+
/* @__PURE__ */ jsx5(Text5, { color: theme.prompt, dimColor: true, inverse: onPill, children: label }, `p-${seg.id}`)
|
|
2338
|
+
);
|
|
2339
|
+
pos += 1;
|
|
2340
|
+
}
|
|
2341
|
+
}
|
|
2342
|
+
return nodes;
|
|
2343
|
+
}
|
|
1377
2344
|
|
|
1378
2345
|
// src/ui/PermissionPrompt.tsx
|
|
1379
2346
|
import { useState as useState2, useRef as useRef2, useEffect as useEffect2, useCallback as useCallback2 } from "react";
|
|
@@ -1541,8 +2508,7 @@ function isSessionAllowed(toolName) {
|
|
|
1541
2508
|
function allowForSession(toolName) {
|
|
1542
2509
|
sessionRules.add(toolName);
|
|
1543
2510
|
}
|
|
1544
|
-
function needsPermission(toolName, permissionResult
|
|
1545
|
-
if (isReadOnly) return false;
|
|
2511
|
+
function needsPermission(toolName, permissionResult) {
|
|
1546
2512
|
if (isSessionAllowed(toolName)) return false;
|
|
1547
2513
|
if (permissionResult.behavior === "allow") return false;
|
|
1548
2514
|
if (permissionResult.behavior === "deny") return false;
|
|
@@ -1642,8 +2608,8 @@ function isObviousBadToolCall(block) {
|
|
|
1642
2608
|
}
|
|
1643
2609
|
if (/^[a-z]+$/.test(cmd) && cmd.length < 10 && !cmd.includes("/")) {
|
|
1644
2610
|
try {
|
|
1645
|
-
const { execSync:
|
|
1646
|
-
|
|
2611
|
+
const { execSync: execSync5 } = __require("child_process");
|
|
2612
|
+
execSync5(`which ${cmd}`, { stdio: "pipe" });
|
|
1647
2613
|
} catch {
|
|
1648
2614
|
return `"${cmd}" is not a recognized command. respond with text instead.`;
|
|
1649
2615
|
}
|
|
@@ -1652,6 +2618,15 @@ function isObviousBadToolCall(block) {
|
|
|
1652
2618
|
return null;
|
|
1653
2619
|
}
|
|
1654
2620
|
async function executeToolCall(block, tools, context, askPermission) {
|
|
2621
|
+
if (block.invalidArgs) {
|
|
2622
|
+
return {
|
|
2623
|
+
toolUseId: block.id,
|
|
2624
|
+
result: {
|
|
2625
|
+
content: `malformed tool arguments for ${block.name} \u2014 not executed. resend the call with valid JSON arguments.`,
|
|
2626
|
+
isError: true
|
|
2627
|
+
}
|
|
2628
|
+
};
|
|
2629
|
+
}
|
|
1655
2630
|
const badCallReason = isObviousBadToolCall(block);
|
|
1656
2631
|
if (badCallReason) {
|
|
1657
2632
|
return {
|
|
@@ -1692,8 +2667,16 @@ async function executeToolCall(block, tools, context, askPermission) {
|
|
|
1692
2667
|
}
|
|
1693
2668
|
};
|
|
1694
2669
|
}
|
|
1695
|
-
|
|
1696
|
-
|
|
2670
|
+
if (needsPermission(tool.name, permission)) {
|
|
2671
|
+
if (!askPermission) {
|
|
2672
|
+
return {
|
|
2673
|
+
toolUseId: block.id,
|
|
2674
|
+
result: {
|
|
2675
|
+
content: `permission required for ${tool.name} but no approval is available in this context`,
|
|
2676
|
+
isError: true
|
|
2677
|
+
}
|
|
2678
|
+
};
|
|
2679
|
+
}
|
|
1697
2680
|
const description = permission.behavior === "ask" ? permission.message : `run ${tool.name}`;
|
|
1698
2681
|
const choice = await askPermission(tool.name, description, block.id);
|
|
1699
2682
|
if (choice === "deny") {
|
|
@@ -1787,7 +2770,7 @@ async function runAgent(agent, task) {
|
|
|
1787
2770
|
const messages = [
|
|
1788
2771
|
{ role: "user", content: [{ type: "text", text: prompt }] }
|
|
1789
2772
|
];
|
|
1790
|
-
const context = { cwd: process.cwd(), signal };
|
|
2773
|
+
const context = { cwd: task.cwd ?? process.cwd(), signal };
|
|
1791
2774
|
let turnCount = 0;
|
|
1792
2775
|
let finalOutput = "";
|
|
1793
2776
|
while (turnCount < maxTurns) {
|
|
@@ -1830,7 +2813,9 @@ async function runAgent(agent, task) {
|
|
|
1830
2813
|
if (toolBlock) {
|
|
1831
2814
|
try {
|
|
1832
2815
|
toolBlock.input = JSON.parse(event.inputJson);
|
|
2816
|
+
toolBlock.invalidArgs = false;
|
|
1833
2817
|
} catch {
|
|
2818
|
+
if (event.inputJson.trim().length > 0) toolBlock.invalidArgs = true;
|
|
1834
2819
|
}
|
|
1835
2820
|
}
|
|
1836
2821
|
break;
|
|
@@ -1904,11 +2889,23 @@ ${last}`;
|
|
|
1904
2889
|
return { ...block, content: trimmed };
|
|
1905
2890
|
}
|
|
1906
2891
|
|
|
2892
|
+
// src/compact/pairing.ts
|
|
2893
|
+
function isToolResultBearingUser(msg) {
|
|
2894
|
+
return msg.role === "user" && msg.content.some((b) => b.type === "tool_result");
|
|
2895
|
+
}
|
|
2896
|
+
function safeKeepStart(messages, desiredKeepCount) {
|
|
2897
|
+
let start = Math.max(0, messages.length - desiredKeepCount);
|
|
2898
|
+
while (start < messages.length && isToolResultBearingUser(messages[start])) {
|
|
2899
|
+
start++;
|
|
2900
|
+
}
|
|
2901
|
+
return start;
|
|
2902
|
+
}
|
|
2903
|
+
|
|
1907
2904
|
// src/compact/snip.ts
|
|
1908
2905
|
function snipOldTurns(messages) {
|
|
1909
2906
|
if (messages.length <= 4) return messages;
|
|
1910
2907
|
const keepCount = Math.ceil(messages.length / 2);
|
|
1911
|
-
const snipped = messages.slice(
|
|
2908
|
+
const snipped = messages.slice(safeKeepStart(messages, keepCount));
|
|
1912
2909
|
const marker = {
|
|
1913
2910
|
role: "user",
|
|
1914
2911
|
content: [{
|
|
@@ -1939,8 +2936,9 @@ async function summarizeOldTurns(messages, provider, model, keepRecent = 10) {
|
|
|
1939
2936
|
if (messages.length <= keepRecent + 2) {
|
|
1940
2937
|
return { ok: true, messages };
|
|
1941
2938
|
}
|
|
1942
|
-
const
|
|
1943
|
-
const
|
|
2939
|
+
const start = safeKeepStart(messages, keepRecent);
|
|
2940
|
+
const oldMessages = messages.slice(0, start);
|
|
2941
|
+
const recentMessages = messages.slice(start);
|
|
1944
2942
|
const conversationText = oldMessages.map((msg) => {
|
|
1945
2943
|
const role = msg.role;
|
|
1946
2944
|
const text = msg.content.map((b) => {
|
|
@@ -2006,6 +3004,10 @@ async function* query(options) {
|
|
|
2006
3004
|
cwd: process.cwd(),
|
|
2007
3005
|
signal
|
|
2008
3006
|
};
|
|
3007
|
+
const { tuning } = loadConfig();
|
|
3008
|
+
const compactionThreshold = tuning.compaction_threshold;
|
|
3009
|
+
const snipThreshold = compactionThreshold * 0.75;
|
|
3010
|
+
const emptyTurnNudgeCap = tuning.empty_turn_nudge_cap;
|
|
2009
3011
|
let turnCount = 0;
|
|
2010
3012
|
let consecutiveErrors = 0;
|
|
2011
3013
|
let consecutiveEmptyTurns = 0;
|
|
@@ -2022,13 +3024,16 @@ async function* query(options) {
|
|
|
2022
3024
|
messages.splice(0, messages.length, ...trimOldToolResults(messages));
|
|
2023
3025
|
const tokenCount = countConversationTokens(messages);
|
|
2024
3026
|
yield { type: "token_update", used: tokenCount, max: capabilities.maxContextTokens, formatted: `${formatTokens(tokenCount)} / ${formatTokens(capabilities.maxContextTokens)}` };
|
|
2025
|
-
if (tokenCount > capabilities.maxContextTokens *
|
|
3027
|
+
if (tokenCount > capabilities.maxContextTokens * compactionThreshold) {
|
|
2026
3028
|
let compacted = false;
|
|
2027
3029
|
if (!summarizeBlocked) {
|
|
2028
3030
|
const result = await summarizeOldTurns(messages, provider, model);
|
|
2029
3031
|
if (result.ok) {
|
|
2030
3032
|
messages.splice(0, messages.length, ...result.messages);
|
|
2031
3033
|
compacted = true;
|
|
3034
|
+
if (countConversationTokens(messages) > capabilities.maxContextTokens * compactionThreshold) {
|
|
3035
|
+
messages.splice(0, messages.length, ...snipOldTurns(messages));
|
|
3036
|
+
}
|
|
2032
3037
|
} else {
|
|
2033
3038
|
summarizeBlocked = true;
|
|
2034
3039
|
yield { type: "error", error: `compaction degraded to snip: ${result.reason}` };
|
|
@@ -2038,7 +3043,7 @@ async function* query(options) {
|
|
|
2038
3043
|
const snipped = snipOldTurns(messages);
|
|
2039
3044
|
messages.splice(0, messages.length, ...snipped);
|
|
2040
3045
|
}
|
|
2041
|
-
} else if (tokenCount > capabilities.maxContextTokens *
|
|
3046
|
+
} else if (tokenCount > capabilities.maxContextTokens * snipThreshold) {
|
|
2042
3047
|
const snipped = snipOldTurns(messages);
|
|
2043
3048
|
messages.splice(0, messages.length, ...snipped);
|
|
2044
3049
|
}
|
|
@@ -2088,7 +3093,7 @@ async function* query(options) {
|
|
|
2088
3093
|
(b) => b.type === "text" && b.text.trim().length > 0
|
|
2089
3094
|
);
|
|
2090
3095
|
if (toolUseBlocks.length === 0 && !hasText) {
|
|
2091
|
-
if (consecutiveEmptyTurns >=
|
|
3096
|
+
if (consecutiveEmptyTurns >= emptyTurnNudgeCap) {
|
|
2092
3097
|
yield { type: "done", reason: "empty_turn_cap", turnCount };
|
|
2093
3098
|
return;
|
|
2094
3099
|
}
|
|
@@ -2218,7 +3223,9 @@ function collectContentBlock(event, content) {
|
|
|
2218
3223
|
if (toolBlock) {
|
|
2219
3224
|
try {
|
|
2220
3225
|
toolBlock.input = JSON.parse(event.inputJson);
|
|
3226
|
+
toolBlock.invalidArgs = false;
|
|
2221
3227
|
} catch {
|
|
3228
|
+
if (event.inputJson.trim().length > 0) toolBlock.invalidArgs = true;
|
|
2222
3229
|
}
|
|
2223
3230
|
}
|
|
2224
3231
|
break;
|
|
@@ -2251,7 +3258,8 @@ check if relevant files/paths exist. then report:
|
|
|
2251
3258
|
// runAgent filters Agent out internally so subagents cannot nest.
|
|
2252
3259
|
// turn cap and permission policy come from RECOVERY_AGENT.
|
|
2253
3260
|
tools: opts.tools,
|
|
2254
|
-
signal: opts.signal
|
|
3261
|
+
signal: opts.signal,
|
|
3262
|
+
cwd: opts.cwd
|
|
2255
3263
|
});
|
|
2256
3264
|
return result.output || "recovery agent could not diagnose the error";
|
|
2257
3265
|
}
|
|
@@ -2299,6 +3307,13 @@ function formatContext(ctx) {
|
|
|
2299
3307
|
if (ctx.deps.count > 0) {
|
|
2300
3308
|
lines.push(`deps: ${ctx.deps.count} (${ctx.deps.file})`);
|
|
2301
3309
|
}
|
|
3310
|
+
const { testing } = ctx;
|
|
3311
|
+
if (testing.hasTests) {
|
|
3312
|
+
const parts = [`tests: ${testing.testFileCount}`];
|
|
3313
|
+
if (testing.framework) parts.push(`framework: ${testing.framework}`);
|
|
3314
|
+
if (testing.command) parts.push(`scripts.test: ${testing.command}`);
|
|
3315
|
+
lines.push(parts.join(" \xB7 "));
|
|
3316
|
+
}
|
|
2302
3317
|
if (ctx.prism.learnedRules > 0) {
|
|
2303
3318
|
lines.push(`learned rules: ${ctx.prism.learnedRules}`);
|
|
2304
3319
|
}
|
|
@@ -2326,11 +3341,11 @@ function formatMemory(m) {
|
|
|
2326
3341
|
}
|
|
2327
3342
|
|
|
2328
3343
|
// src/context/lenses.ts
|
|
2329
|
-
import { existsSync as
|
|
2330
|
-
import { join as
|
|
3344
|
+
import { existsSync as existsSync6, readFileSync as readFileSync6, readdirSync as readdirSync3 } from "fs";
|
|
3345
|
+
import { join as join6, basename as basename3 } from "path";
|
|
2331
3346
|
function loadLenses(cwd) {
|
|
2332
|
-
const dir =
|
|
2333
|
-
if (!
|
|
3347
|
+
const dir = join6(cwd, ".prism");
|
|
3348
|
+
if (!existsSync6(dir)) return [];
|
|
2334
3349
|
let files;
|
|
2335
3350
|
try {
|
|
2336
3351
|
files = readdirSync3(dir, { withFileTypes: true }).filter((e) => e.isFile() && e.name.endsWith(".md")).map((e) => e.name);
|
|
@@ -2345,7 +3360,7 @@ function loadLenses(cwd) {
|
|
|
2345
3360
|
const lenses = [];
|
|
2346
3361
|
for (const file of files) {
|
|
2347
3362
|
try {
|
|
2348
|
-
const content =
|
|
3363
|
+
const content = readFileSync6(join6(dir, file), "utf-8").trim();
|
|
2349
3364
|
if (content.length > 0) {
|
|
2350
3365
|
lenses.push({ name: basename3(file, ".md"), content });
|
|
2351
3366
|
}
|
|
@@ -2356,8 +3371,42 @@ function loadLenses(cwd) {
|
|
|
2356
3371
|
}
|
|
2357
3372
|
|
|
2358
3373
|
// src/prompts/system.ts
|
|
3374
|
+
var refIds = /* @__PURE__ */ new WeakMap();
|
|
3375
|
+
var nextRefId = 0;
|
|
3376
|
+
function refId(obj) {
|
|
3377
|
+
if (!obj) return -1;
|
|
3378
|
+
let id = refIds.get(obj);
|
|
3379
|
+
if (id === void 0) {
|
|
3380
|
+
id = ++nextRefId;
|
|
3381
|
+
refIds.set(obj, id);
|
|
3382
|
+
}
|
|
3383
|
+
return id;
|
|
3384
|
+
}
|
|
3385
|
+
var cachedStaticKey = null;
|
|
3386
|
+
var cachedStaticPrefix = null;
|
|
3387
|
+
function staticKey(options) {
|
|
3388
|
+
return [
|
|
3389
|
+
options.capabilities.maxTools,
|
|
3390
|
+
options.cwd,
|
|
3391
|
+
refId(options.tools),
|
|
3392
|
+
refId(options.projectContext),
|
|
3393
|
+
refId(options.memory),
|
|
3394
|
+
refId(options.profile)
|
|
3395
|
+
].join("|");
|
|
3396
|
+
}
|
|
2359
3397
|
function buildSystemPrompt(options) {
|
|
2360
|
-
const
|
|
3398
|
+
const key = staticKey(options);
|
|
3399
|
+
if (key !== cachedStaticKey) {
|
|
3400
|
+
cachedStaticPrefix = composeStatic(options);
|
|
3401
|
+
cachedStaticKey = key;
|
|
3402
|
+
}
|
|
3403
|
+
const dynamic = composeDynamic(options);
|
|
3404
|
+
return dynamic ? `${cachedStaticPrefix}
|
|
3405
|
+
|
|
3406
|
+
${dynamic}` : cachedStaticPrefix;
|
|
3407
|
+
}
|
|
3408
|
+
function composeStatic(options) {
|
|
3409
|
+
const { capabilities, tools, cwd, profile, projectContext, memory } = options;
|
|
2361
3410
|
const sections = [
|
|
2362
3411
|
getCore(),
|
|
2363
3412
|
getTools(tools, capabilities),
|
|
@@ -2367,13 +3416,12 @@ function buildSystemPrompt(options) {
|
|
|
2367
3416
|
if (agentsBlock) sections.push(agentsBlock);
|
|
2368
3417
|
const invokeSkillsBlock = getInvokeSkills(cwd);
|
|
2369
3418
|
if (invokeSkillsBlock) sections.push(invokeSkillsBlock);
|
|
2370
|
-
const skillsBlock = getActiveSkills(cwd, activeSkills);
|
|
2371
|
-
if (skillsBlock) sections.push(skillsBlock);
|
|
2372
3419
|
if (projectContext) {
|
|
2373
3420
|
sections.push(formatContext(projectContext));
|
|
2374
3421
|
if (projectContext.git) {
|
|
2375
3422
|
sections.push(getGitGuidance());
|
|
2376
3423
|
}
|
|
3424
|
+
sections.push(getVerificationGuidance(projectContext.testing.hasTests));
|
|
2377
3425
|
}
|
|
2378
3426
|
const lensesBlock = getLenses(cwd);
|
|
2379
3427
|
if (lensesBlock) sections.push(lensesBlock);
|
|
@@ -2385,6 +3433,14 @@ function buildSystemPrompt(options) {
|
|
|
2385
3433
|
const learned = rulesToPrompt(profile);
|
|
2386
3434
|
if (learned) sections.push(learned);
|
|
2387
3435
|
}
|
|
3436
|
+
return sections.join("\n\n");
|
|
3437
|
+
}
|
|
3438
|
+
function composeDynamic(options) {
|
|
3439
|
+
const { cwd, activeSkills, repoMap, inPlanMode } = options;
|
|
3440
|
+
const sections = [];
|
|
3441
|
+
const skillsBlock = getActiveSkills(cwd, activeSkills);
|
|
3442
|
+
if (skillsBlock) sections.push(skillsBlock);
|
|
3443
|
+
if (repoMap && repoMap.length > 0) sections.push(repoMap);
|
|
2388
3444
|
if (inPlanMode) {
|
|
2389
3445
|
sections.push(getPlanModeAddendum());
|
|
2390
3446
|
}
|
|
@@ -2508,8 +3564,9 @@ function getInvokeSkills(cwd) {
|
|
|
2508
3564
|
}
|
|
2509
3565
|
function getActiveSkills(cwd, names) {
|
|
2510
3566
|
if (!names || names.size === 0) return null;
|
|
3567
|
+
const sortedNames = [...names].sort();
|
|
2511
3568
|
const bodies = [];
|
|
2512
|
-
for (const name of
|
|
3569
|
+
for (const name of sortedNames) {
|
|
2513
3570
|
try {
|
|
2514
3571
|
const skill = loadSkill(name, cwd);
|
|
2515
3572
|
bodies.push(skill.body);
|
|
@@ -2546,6 +3603,17 @@ function getGitGuidance() {
|
|
|
2546
3603
|
- Before committing, always show the user what will be committed.
|
|
2547
3604
|
- Never force-push or reset --hard without explicit permission.`;
|
|
2548
3605
|
}
|
|
3606
|
+
function getVerificationGuidance(hasTests) {
|
|
3607
|
+
if (!hasTests) {
|
|
3608
|
+
return `# verification
|
|
3609
|
+
- This project has no test suite. After a non-trivial edit, confirm the change and stop. Do not propose writing tests unless the user explicitly asks.
|
|
3610
|
+
- Typo and comment-only edits: call out that the change is trivial.`;
|
|
3611
|
+
}
|
|
3612
|
+
return `# verification
|
|
3613
|
+
- After a non-trivial edit, call Verify with the project's test command. Derive it from \`# project scan\` (framework, scripts.test) and \`# repo map\` (test file structure).
|
|
3614
|
+
- Skip Verify for typo or comment-only edits; call out that the change is trivial.
|
|
3615
|
+
- If Verify fails, debug from its output. Do not claim done until it passes (or the user accepts the failing state explicitly).`;
|
|
3616
|
+
}
|
|
2549
3617
|
function getPlanModeAddendum() {
|
|
2550
3618
|
return `## plan mode
|
|
2551
3619
|
|
|
@@ -2578,10 +3646,10 @@ date: ${(/* @__PURE__ */ new Date()).toISOString().split("T")[0]}`;
|
|
|
2578
3646
|
}
|
|
2579
3647
|
|
|
2580
3648
|
// src/context/scanner.ts
|
|
2581
|
-
import { existsSync as
|
|
3649
|
+
import { existsSync as existsSync7, readdirSync as readdirSync4, readFileSync as readFileSync7, statSync } from "fs";
|
|
2582
3650
|
import { execSync as execSync2 } from "child_process";
|
|
2583
|
-
import { join as
|
|
2584
|
-
import { homedir as
|
|
3651
|
+
import { join as join7, basename as basename4, extname } from "path";
|
|
3652
|
+
import { homedir as homedir7 } from "os";
|
|
2585
3653
|
var LANG_MAP = {
|
|
2586
3654
|
// scripting
|
|
2587
3655
|
".py": "python",
|
|
@@ -2886,7 +3954,8 @@ function scanProject(cwd) {
|
|
|
2886
3954
|
git: detectGit(cwd),
|
|
2887
3955
|
deps,
|
|
2888
3956
|
prism: detectPrismState(cwd),
|
|
2889
|
-
runtime: detectRuntime()
|
|
3957
|
+
runtime: detectRuntime(),
|
|
3958
|
+
testing: detectTesting(cwd, structure, deps)
|
|
2890
3959
|
};
|
|
2891
3960
|
}
|
|
2892
3961
|
function detectLanguage(filesByType) {
|
|
@@ -2911,10 +3980,10 @@ function detectFramework(depNames) {
|
|
|
2911
3980
|
return null;
|
|
2912
3981
|
}
|
|
2913
3982
|
function detectEntryPoint(cwd, language) {
|
|
2914
|
-
const pyproject =
|
|
2915
|
-
if (
|
|
3983
|
+
const pyproject = join7(cwd, "pyproject.toml");
|
|
3984
|
+
if (existsSync7(pyproject)) {
|
|
2916
3985
|
try {
|
|
2917
|
-
const text =
|
|
3986
|
+
const text = readFileSync7(pyproject, "utf-8");
|
|
2918
3987
|
const match = text.match(/\[project\.scripts\]\s*\n\w+\s*=\s*"([^"]+)"/);
|
|
2919
3988
|
if (match) {
|
|
2920
3989
|
return match[1].split(":")[0].replace(/\./g, "/") + ".py";
|
|
@@ -2922,10 +3991,10 @@ function detectEntryPoint(cwd, language) {
|
|
|
2922
3991
|
} catch {
|
|
2923
3992
|
}
|
|
2924
3993
|
}
|
|
2925
|
-
const pkgJson =
|
|
2926
|
-
if (
|
|
3994
|
+
const pkgJson = join7(cwd, "package.json");
|
|
3995
|
+
if (existsSync7(pkgJson)) {
|
|
2927
3996
|
try {
|
|
2928
|
-
const data = JSON.parse(
|
|
3997
|
+
const data = JSON.parse(readFileSync7(pkgJson, "utf-8"));
|
|
2929
3998
|
if (data.main) return data.main;
|
|
2930
3999
|
} catch {
|
|
2931
4000
|
}
|
|
@@ -2938,7 +4007,7 @@ function detectEntryPoint(cwd, language) {
|
|
|
2938
4007
|
rust: ["src/main.rs"]
|
|
2939
4008
|
};
|
|
2940
4009
|
for (const candidate of candidates[language || ""] || []) {
|
|
2941
|
-
if (
|
|
4010
|
+
if (existsSync7(join7(cwd, candidate))) return candidate;
|
|
2942
4011
|
}
|
|
2943
4012
|
return null;
|
|
2944
4013
|
}
|
|
@@ -2948,11 +4017,11 @@ function detectStructure(cwd) {
|
|
|
2948
4017
|
const configFiles = [];
|
|
2949
4018
|
let totalFiles = 0;
|
|
2950
4019
|
for (const cf of CONFIG_FILES) {
|
|
2951
|
-
if (
|
|
4020
|
+
if (existsSync7(join7(cwd, cf))) configFiles.push(cf);
|
|
2952
4021
|
}
|
|
2953
4022
|
try {
|
|
2954
4023
|
for (const entry of readdirSync4(cwd)) {
|
|
2955
|
-
const path =
|
|
4024
|
+
const path = join7(cwd, entry);
|
|
2956
4025
|
try {
|
|
2957
4026
|
const stat = statSync(path);
|
|
2958
4027
|
if (stat.isDirectory() && !entry.startsWith(".") && !IGNORE_DIRS.has(entry)) {
|
|
@@ -2974,7 +4043,7 @@ function countFiles(dir, counts, depth, maxDepth) {
|
|
|
2974
4043
|
try {
|
|
2975
4044
|
for (const entry of readdirSync4(dir)) {
|
|
2976
4045
|
if (IGNORE_DIRS.has(entry) || entry.startsWith(".")) continue;
|
|
2977
|
-
const path =
|
|
4046
|
+
const path = join7(dir, entry);
|
|
2978
4047
|
try {
|
|
2979
4048
|
const stat = statSync(path);
|
|
2980
4049
|
if (stat.isFile()) {
|
|
@@ -2992,7 +4061,7 @@ function countFiles(dir, counts, depth, maxDepth) {
|
|
|
2992
4061
|
}
|
|
2993
4062
|
}
|
|
2994
4063
|
function detectGit(cwd) {
|
|
2995
|
-
if (!
|
|
4064
|
+
if (!existsSync7(join7(cwd, ".git"))) return null;
|
|
2996
4065
|
try {
|
|
2997
4066
|
const branch = exec(cwd, "git branch --show-current").trim();
|
|
2998
4067
|
const status = exec(cwd, "git status --porcelain");
|
|
@@ -3019,18 +4088,18 @@ function detectGit(cwd) {
|
|
|
3019
4088
|
}
|
|
3020
4089
|
}
|
|
3021
4090
|
function detectDeps(cwd) {
|
|
3022
|
-
const reqTxt =
|
|
3023
|
-
if (
|
|
4091
|
+
const reqTxt = join7(cwd, "requirements.txt");
|
|
4092
|
+
if (existsSync7(reqTxt)) {
|
|
3024
4093
|
try {
|
|
3025
|
-
const lines =
|
|
4094
|
+
const lines = readFileSync7(reqTxt, "utf-8").split("\n").map((l) => l.trim()).filter((l) => l && !l.startsWith("#") && !l.startsWith("-")).map((l) => l.split(/[><=!~]/)[0].trim());
|
|
3026
4095
|
return { file: "requirements.txt", count: lines.length, names: lines };
|
|
3027
4096
|
} catch {
|
|
3028
4097
|
}
|
|
3029
4098
|
}
|
|
3030
|
-
const pyproject =
|
|
3031
|
-
if (
|
|
4099
|
+
const pyproject = join7(cwd, "pyproject.toml");
|
|
4100
|
+
if (existsSync7(pyproject)) {
|
|
3032
4101
|
try {
|
|
3033
|
-
const text =
|
|
4102
|
+
const text = readFileSync7(pyproject, "utf-8");
|
|
3034
4103
|
const names = [];
|
|
3035
4104
|
let inDeps = false;
|
|
3036
4105
|
for (const line of text.split("\n")) {
|
|
@@ -3048,10 +4117,10 @@ function detectDeps(cwd) {
|
|
|
3048
4117
|
} catch {
|
|
3049
4118
|
}
|
|
3050
4119
|
}
|
|
3051
|
-
const pkgJson =
|
|
3052
|
-
if (
|
|
4120
|
+
const pkgJson = join7(cwd, "package.json");
|
|
4121
|
+
if (existsSync7(pkgJson)) {
|
|
3053
4122
|
try {
|
|
3054
|
-
const data = JSON.parse(
|
|
4123
|
+
const data = JSON.parse(readFileSync7(pkgJson, "utf-8"));
|
|
3055
4124
|
const names = [
|
|
3056
4125
|
...Object.keys(data.dependencies || {}),
|
|
3057
4126
|
...Object.keys(data.devDependencies || {})
|
|
@@ -3065,11 +4134,11 @@ function detectDeps(cwd) {
|
|
|
3065
4134
|
function detectPrismState(_cwd) {
|
|
3066
4135
|
let learnedRules = 0;
|
|
3067
4136
|
try {
|
|
3068
|
-
const modelsDir =
|
|
3069
|
-
if (
|
|
4137
|
+
const modelsDir = join7(homedir7(), ".prism", "models");
|
|
4138
|
+
if (existsSync7(modelsDir)) {
|
|
3070
4139
|
for (const file of readdirSync4(modelsDir)) {
|
|
3071
4140
|
if (file.endsWith(".json")) {
|
|
3072
|
-
const data = JSON.parse(
|
|
4141
|
+
const data = JSON.parse(readFileSync7(join7(modelsDir, file), "utf-8"));
|
|
3073
4142
|
learnedRules += (data.rules || []).length;
|
|
3074
4143
|
}
|
|
3075
4144
|
}
|
|
@@ -3095,24 +4164,106 @@ function tryVersion(cmd) {
|
|
|
3095
4164
|
return null;
|
|
3096
4165
|
}
|
|
3097
4166
|
}
|
|
4167
|
+
function detectTesting(cwd, structure, deps) {
|
|
4168
|
+
const configs = new Set(structure.configFiles);
|
|
4169
|
+
const depNames = new Set(deps.names);
|
|
4170
|
+
const testFileCount = countTestFiles(cwd);
|
|
4171
|
+
let framework = null;
|
|
4172
|
+
if (depNames.has("vitest")) framework = "vitest";
|
|
4173
|
+
else if (depNames.has("jest")) framework = "jest";
|
|
4174
|
+
else if (depNames.has("mocha")) framework = "mocha";
|
|
4175
|
+
else if (depNames.has("@playwright/test") || depNames.has("playwright")) framework = "playwright";
|
|
4176
|
+
else if (depNames.has("pytest")) framework = "pytest";
|
|
4177
|
+
else if (configs.has("pyproject.toml") || configs.has("setup.py")) framework = "pytest";
|
|
4178
|
+
else if (configs.has("Cargo.toml")) framework = "cargo-test";
|
|
4179
|
+
else if (configs.has("go.mod")) framework = "go-test";
|
|
4180
|
+
let command = null;
|
|
4181
|
+
const pkgJsonPath = join7(cwd, "package.json");
|
|
4182
|
+
if (existsSync7(pkgJsonPath)) {
|
|
4183
|
+
try {
|
|
4184
|
+
const pkg = JSON.parse(readFileSync7(pkgJsonPath, "utf-8"));
|
|
4185
|
+
if (pkg.scripts && typeof pkg.scripts.test === "string") {
|
|
4186
|
+
command = pkg.scripts.test;
|
|
4187
|
+
}
|
|
4188
|
+
} catch {
|
|
4189
|
+
}
|
|
4190
|
+
}
|
|
4191
|
+
return { hasTests: testFileCount > 0, testFileCount, framework, command };
|
|
4192
|
+
}
|
|
4193
|
+
function countTestFiles(cwd) {
|
|
4194
|
+
const TEST_NAME = /(?:^|[._-])(?:test|spec)(?:[._-]|$)/i;
|
|
4195
|
+
const TEST_DIRS = /* @__PURE__ */ new Set(["tests", "test", "__tests__"]);
|
|
4196
|
+
const SKIP = /* @__PURE__ */ new Set([
|
|
4197
|
+
"node_modules",
|
|
4198
|
+
".git",
|
|
4199
|
+
".venv",
|
|
4200
|
+
"venv",
|
|
4201
|
+
"env",
|
|
4202
|
+
"__pycache__",
|
|
4203
|
+
"dist",
|
|
4204
|
+
"build",
|
|
4205
|
+
"out",
|
|
4206
|
+
"target",
|
|
4207
|
+
"_build",
|
|
4208
|
+
".next",
|
|
4209
|
+
".nuxt",
|
|
4210
|
+
".cache",
|
|
4211
|
+
"coverage",
|
|
4212
|
+
".nyc_output"
|
|
4213
|
+
]);
|
|
4214
|
+
let count = 0;
|
|
4215
|
+
const stack = [cwd];
|
|
4216
|
+
let budget = 1500;
|
|
4217
|
+
while (stack.length > 0 && budget-- > 0) {
|
|
4218
|
+
const dir = stack.pop();
|
|
4219
|
+
let entries;
|
|
4220
|
+
try {
|
|
4221
|
+
entries = readdirSync4(dir);
|
|
4222
|
+
} catch {
|
|
4223
|
+
continue;
|
|
4224
|
+
}
|
|
4225
|
+
for (const name of entries) {
|
|
4226
|
+
if (SKIP.has(name) || name.startsWith(".")) continue;
|
|
4227
|
+
const full = join7(dir, name);
|
|
4228
|
+
let s;
|
|
4229
|
+
try {
|
|
4230
|
+
s = statSync(full);
|
|
4231
|
+
} catch {
|
|
4232
|
+
continue;
|
|
4233
|
+
}
|
|
4234
|
+
if (s.isDirectory()) {
|
|
4235
|
+
stack.push(full);
|
|
4236
|
+
continue;
|
|
4237
|
+
}
|
|
4238
|
+
if (TEST_NAME.test(name)) {
|
|
4239
|
+
count++;
|
|
4240
|
+
continue;
|
|
4241
|
+
}
|
|
4242
|
+
const parent = basename4(dir);
|
|
4243
|
+
if (TEST_DIRS.has(parent)) count++;
|
|
4244
|
+
}
|
|
4245
|
+
}
|
|
4246
|
+
return count;
|
|
4247
|
+
}
|
|
3098
4248
|
|
|
3099
4249
|
// src/sessions/store.ts
|
|
3100
|
-
import { existsSync as
|
|
3101
|
-
import { join as
|
|
3102
|
-
import { homedir as
|
|
3103
|
-
|
|
4250
|
+
import { existsSync as existsSync8, mkdirSync as mkdirSync4, readFileSync as readFileSync8, writeFileSync as writeFileSync4, readdirSync as readdirSync5 } from "fs";
|
|
4251
|
+
import { join as join8 } from "path";
|
|
4252
|
+
import { homedir as homedir8 } from "os";
|
|
4253
|
+
import { randomBytes } from "crypto";
|
|
4254
|
+
var SESSIONS_DIR = join8(homedir8(), ".prism", "sessions");
|
|
3104
4255
|
function ensureDir3() {
|
|
3105
|
-
if (!
|
|
3106
|
-
|
|
4256
|
+
if (!existsSync8(SESSIONS_DIR)) {
|
|
4257
|
+
mkdirSync4(SESSIONS_DIR, { recursive: true });
|
|
3107
4258
|
}
|
|
3108
4259
|
}
|
|
3109
4260
|
function sessionPath(id) {
|
|
3110
|
-
return
|
|
4261
|
+
return join8(SESSIONS_DIR, `${id}.json`);
|
|
3111
4262
|
}
|
|
3112
4263
|
function createSession(model, provider, cwd) {
|
|
3113
4264
|
ensureDir3();
|
|
3114
4265
|
const now = /* @__PURE__ */ new Date();
|
|
3115
|
-
const id = now.toISOString().replace(/[:.]/g, "-").slice(0, 23);
|
|
4266
|
+
const id = now.toISOString().replace(/[:.]/g, "-").slice(0, 23) + "-" + randomBytes(3).toString("hex");
|
|
3116
4267
|
return {
|
|
3117
4268
|
id,
|
|
3118
4269
|
model,
|
|
@@ -3127,13 +4278,13 @@ function saveSession(session) {
|
|
|
3127
4278
|
ensureDir3();
|
|
3128
4279
|
session.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
3129
4280
|
const path = sessionPath(session.id);
|
|
3130
|
-
|
|
4281
|
+
writeFileSync4(path, JSON.stringify(session, null, 2), "utf-8");
|
|
3131
4282
|
}
|
|
3132
4283
|
function loadSession(id) {
|
|
3133
4284
|
const path = sessionPath(id);
|
|
3134
|
-
if (!
|
|
4285
|
+
if (!existsSync8(path)) return null;
|
|
3135
4286
|
try {
|
|
3136
|
-
return JSON.parse(
|
|
4287
|
+
return JSON.parse(readFileSync8(path, "utf-8"));
|
|
3137
4288
|
} catch {
|
|
3138
4289
|
return null;
|
|
3139
4290
|
}
|
|
@@ -3144,7 +4295,7 @@ function loadAllSorted() {
|
|
|
3144
4295
|
const sessions = [];
|
|
3145
4296
|
for (const file of files) {
|
|
3146
4297
|
try {
|
|
3147
|
-
sessions.push(JSON.parse(
|
|
4298
|
+
sessions.push(JSON.parse(readFileSync8(join8(SESSIONS_DIR, file), "utf-8")));
|
|
3148
4299
|
} catch {
|
|
3149
4300
|
continue;
|
|
3150
4301
|
}
|
|
@@ -3176,6 +4327,15 @@ var DESCRIPTION = "Spawn a subagent to handle a focused task. The subagent gets
|
|
|
3176
4327
|
function createAgentTool(opts) {
|
|
3177
4328
|
const subagentTools = opts.subagentTools.filter((t) => t.name !== "Agent");
|
|
3178
4329
|
const boundCwd = opts.cwd ?? process.cwd();
|
|
4330
|
+
const resolveRequested = (input) => {
|
|
4331
|
+
try {
|
|
4332
|
+
const requested = input.agent?.trim();
|
|
4333
|
+
const agentName = requested && requested.length > 0 ? requested : void 0;
|
|
4334
|
+
return resolveAgent(agentName, boundCwd);
|
|
4335
|
+
} catch {
|
|
4336
|
+
return null;
|
|
4337
|
+
}
|
|
4338
|
+
};
|
|
3179
4339
|
return buildTool({
|
|
3180
4340
|
name: "Agent",
|
|
3181
4341
|
description: DESCRIPTION,
|
|
@@ -3205,7 +4365,8 @@ function createAgentTool(opts) {
|
|
|
3205
4365
|
model: opts.model,
|
|
3206
4366
|
tools: subagentTools,
|
|
3207
4367
|
signal: context.signal,
|
|
3208
|
-
onProgress: opts.onProgress
|
|
4368
|
+
onProgress: opts.onProgress,
|
|
4369
|
+
cwd: context.cwd
|
|
3209
4370
|
});
|
|
3210
4371
|
if (!result.success) {
|
|
3211
4372
|
return {
|
|
@@ -3223,20 +4384,18 @@ ${result.output}`
|
|
|
3223
4384
|
// inherit can write through the parent's resolver; two inherit agents
|
|
3224
4385
|
// in the same batch could race on shared files. serialize those.
|
|
3225
4386
|
// unresolvable agents (unknown name, broken file) → assume unsafe.
|
|
3226
|
-
isConcurrencySafe: (input) =>
|
|
3227
|
-
|
|
3228
|
-
|
|
3229
|
-
|
|
3230
|
-
|
|
3231
|
-
|
|
3232
|
-
|
|
3233
|
-
|
|
3234
|
-
}
|
|
3235
|
-
|
|
3236
|
-
|
|
3237
|
-
|
|
3238
|
-
checkPermissions: () => ({ behavior: "allow" })
|
|
3239
|
-
// auto-allow agent spawning
|
|
4387
|
+
isConcurrencySafe: (input) => resolveRequested(input)?.permissions === "deny-writes",
|
|
4388
|
+
// a spawn is only read-only when the requested agent cannot write. inherit
|
|
4389
|
+
// agents write through the parent's resolver, so they are not read-only.
|
|
4390
|
+
isReadOnly: (input) => resolveRequested(input)?.permissions === "deny-writes",
|
|
4391
|
+
// deny-writes agents are sandboxed → auto-allow the spawn. inherit agents
|
|
4392
|
+
// can mutate state through the parent's resolver → prompt before spawning.
|
|
4393
|
+
checkPermissions: (input) => {
|
|
4394
|
+
const agent = resolveRequested(input);
|
|
4395
|
+
if (agent?.permissions === "deny-writes") return { behavior: "allow" };
|
|
4396
|
+
const label = agent?.name ?? input.agent ?? "agent";
|
|
4397
|
+
return { behavior: "ask", message: `spawn write-capable agent "${label}"` };
|
|
4398
|
+
}
|
|
3240
4399
|
});
|
|
3241
4400
|
}
|
|
3242
4401
|
|
|
@@ -3300,9 +4459,8 @@ follow the skill instructions.]` };
|
|
|
3300
4459
|
isConcurrencySafe: () => false,
|
|
3301
4460
|
// not read-only: the skill body lands in the conversation framed as
|
|
3302
4461
|
// "follow these instructions," which drives downstream Edit/Write/Bash
|
|
3303
|
-
// calls.
|
|
3304
|
-
//
|
|
3305
|
-
// false honors the operator's `require-permission: true` frontmatter.
|
|
4462
|
+
// calls. isReadOnly is a concurrency/UI hint; the `require-permission`
|
|
4463
|
+
// gate is enforced by checkPermissions below.
|
|
3306
4464
|
isReadOnly: () => false,
|
|
3307
4465
|
checkPermissions: (input) => {
|
|
3308
4466
|
try {
|
|
@@ -3319,21 +4477,20 @@ follow the skill instructions.]` };
|
|
|
3319
4477
|
|
|
3320
4478
|
// src/ui/bash.ts
|
|
3321
4479
|
import { execSync as execSync3 } from "child_process";
|
|
3322
|
-
var MAX_OUTPUT = 512 * 1024;
|
|
3323
|
-
var TIMEOUT_MS = 3e4;
|
|
3324
4480
|
function handleBashCommand(input, setMessages) {
|
|
3325
4481
|
if (!input.startsWith("!")) return false;
|
|
3326
4482
|
const cmd = input.slice(1).trim();
|
|
3327
4483
|
if (!cmd) return true;
|
|
3328
4484
|
setMessages((prev) => [...prev, { role: "tool_call", text: "", toolName: `! ${cmd}` }]);
|
|
4485
|
+
const { tuning } = loadConfig();
|
|
3329
4486
|
let output;
|
|
3330
4487
|
let isError = false;
|
|
3331
4488
|
try {
|
|
3332
4489
|
output = execSync3(cmd, {
|
|
3333
4490
|
cwd: process.cwd(),
|
|
3334
4491
|
encoding: "utf-8",
|
|
3335
|
-
timeout:
|
|
3336
|
-
maxBuffer:
|
|
4492
|
+
timeout: tuning.bash_timeout_ms,
|
|
4493
|
+
maxBuffer: tuning.bash_max_output_bytes,
|
|
3337
4494
|
stdio: ["pipe", "pipe", "pipe"]
|
|
3338
4495
|
}).trim() || "(no output)";
|
|
3339
4496
|
} catch (e) {
|
|
@@ -3613,14 +4770,17 @@ var OllamaProvider = class {
|
|
|
3613
4770
|
|
|
3614
4771
|
// src/completion/spec.ts
|
|
3615
4772
|
import { execSync as execSync4 } from "child_process";
|
|
3616
|
-
import { existsSync as
|
|
3617
|
-
import { join as
|
|
3618
|
-
import { homedir as
|
|
4773
|
+
import { existsSync as existsSync9, readFileSync as readFileSync9, writeFileSync as writeFileSync5, mkdirSync as mkdirSync5 } from "fs";
|
|
4774
|
+
import { join as join9 } from "path";
|
|
4775
|
+
import { homedir as homedir9 } from "os";
|
|
3619
4776
|
var FLAGS = [
|
|
3620
4777
|
{ flag: "--or", alias: "--openrouter", desc: "use OpenRouter provider", takesValue: "model-openrouter", positionalValue: true },
|
|
3621
4778
|
{ flag: "-c", alias: "--continue", desc: "resume last session in this directory" },
|
|
3622
4779
|
{ flag: "-r", alias: "--resume", desc: "resume a specific session by id", takesValue: "session-id" },
|
|
3623
4780
|
{ flag: "--max-tokens", desc: "max output tokens per response", takesValue: "number" },
|
|
4781
|
+
{ flag: "--max-files", desc: "repo-map walk cap (overrides [tuning].repomap_max_files)", takesValue: "number" },
|
|
4782
|
+
{ flag: "--max-lines", desc: "repo-map render cap (overrides [tuning].repomap_max_lines)", takesValue: "number" },
|
|
4783
|
+
{ flag: "--no-repomap", desc: "skip the repo-map retrieval pass for this session" },
|
|
3624
4784
|
{ flag: "--config", desc: "show config file path" },
|
|
3625
4785
|
{ flag: "--sessions", desc: "list recent sessions" },
|
|
3626
4786
|
{ flag: "--no-scan", desc: "skip the live project scan at startup" },
|
|
@@ -3664,13 +4824,13 @@ var FALLBACK_OPENROUTER_MODELS = [
|
|
|
3664
4824
|
{ id: "anthropic/claude-haiku-4.5", context_length: 2e5, supported_parameters: ["tools", "tool_choice"] },
|
|
3665
4825
|
{ id: "anthropic/claude-sonnet-4", context_length: 2e5, supported_parameters: ["tools", "tool_choice"] }
|
|
3666
4826
|
];
|
|
3667
|
-
var CACHE_DIR =
|
|
3668
|
-
var OR_CACHE_PATH =
|
|
4827
|
+
var CACHE_DIR = join9(homedir9(), ".prism", "cache");
|
|
4828
|
+
var OR_CACHE_PATH = join9(CACHE_DIR, "openrouter-models.json");
|
|
3669
4829
|
var TTL_MS = 24 * 60 * 60 * 1e3;
|
|
3670
4830
|
function readCache() {
|
|
3671
|
-
if (!
|
|
4831
|
+
if (!existsSync9(OR_CACHE_PATH)) return null;
|
|
3672
4832
|
try {
|
|
3673
|
-
const raw = JSON.parse(
|
|
4833
|
+
const raw = JSON.parse(readFileSync9(OR_CACHE_PATH, "utf-8"));
|
|
3674
4834
|
if (!Array.isArray(raw.models) || raw.models.length === 0 || typeof raw.models[0] === "string") {
|
|
3675
4835
|
return null;
|
|
3676
4836
|
}
|
|
@@ -3681,8 +4841,8 @@ function readCache() {
|
|
|
3681
4841
|
}
|
|
3682
4842
|
function writeCache(models) {
|
|
3683
4843
|
try {
|
|
3684
|
-
if (!
|
|
3685
|
-
|
|
4844
|
+
if (!existsSync9(CACHE_DIR)) mkdirSync5(CACHE_DIR, { recursive: true });
|
|
4845
|
+
writeFileSync5(OR_CACHE_PATH, JSON.stringify({ fetchedAt: Date.now(), models }), "utf-8");
|
|
3686
4846
|
} catch {
|
|
3687
4847
|
}
|
|
3688
4848
|
}
|
|
@@ -3785,6 +4945,34 @@ function inferCapabilities(modelId, meta) {
|
|
|
3785
4945
|
caps.parallelToolCalls = params.includes("tools") && params.includes("tool_choice");
|
|
3786
4946
|
return caps;
|
|
3787
4947
|
}
|
|
4948
|
+
function extractCachedTokens(u) {
|
|
4949
|
+
if (!u) return 0;
|
|
4950
|
+
return u.prompt_tokens_details?.cached_tokens ?? u.cache_read_input_tokens ?? u.prompt_cache_hit_tokens ?? 0;
|
|
4951
|
+
}
|
|
4952
|
+
function formatOpenRouterError(status, rawBody) {
|
|
4953
|
+
let message = rawBody;
|
|
4954
|
+
try {
|
|
4955
|
+
const parsed = JSON.parse(rawBody);
|
|
4956
|
+
if (parsed.error?.message) message = parsed.error.message;
|
|
4957
|
+
} catch {
|
|
4958
|
+
}
|
|
4959
|
+
if (status === 402) {
|
|
4960
|
+
const m = message.match(/requested up to (\d+) tokens, but can only afford (\d+)/);
|
|
4961
|
+
if (m) {
|
|
4962
|
+
return `openrouter: out of credits. requested ${m[1]} tokens, only ${m[2]} affordable.
|
|
4963
|
+
fix: lower max_tokens in config, or add credits at https://openrouter.ai/settings/credits`;
|
|
4964
|
+
}
|
|
4965
|
+
return "openrouter: out of credits. add credits at https://openrouter.ai/settings/credits, or lower max_tokens";
|
|
4966
|
+
}
|
|
4967
|
+
if (status === 401) {
|
|
4968
|
+
return "openrouter: invalid api key. check OPENROUTER_API_KEY or ~/.prism/config.toml";
|
|
4969
|
+
}
|
|
4970
|
+
if (status === 429) {
|
|
4971
|
+
return "openrouter: rate limited. try again in a moment";
|
|
4972
|
+
}
|
|
4973
|
+
const clamped = message.length > 240 ? message.slice(0, 240) + "..." : message;
|
|
4974
|
+
return `openrouter ${status}: ${clamped}`;
|
|
4975
|
+
}
|
|
3788
4976
|
var OpenRouterProvider = class {
|
|
3789
4977
|
name = "openrouter";
|
|
3790
4978
|
apiKey = "";
|
|
@@ -3829,7 +5017,7 @@ var OpenRouterProvider = class {
|
|
|
3829
5017
|
});
|
|
3830
5018
|
if (!res.ok) {
|
|
3831
5019
|
const errorText = await res.text().catch(() => res.statusText);
|
|
3832
|
-
yield { type: "error", error:
|
|
5020
|
+
yield { type: "error", error: formatOpenRouterError(res.status, errorText) };
|
|
3833
5021
|
return;
|
|
3834
5022
|
}
|
|
3835
5023
|
const reader = res.body?.getReader();
|
|
@@ -3841,6 +5029,7 @@ var OpenRouterProvider = class {
|
|
|
3841
5029
|
const messageId = crypto.randomUUID();
|
|
3842
5030
|
let inputTokens = 0;
|
|
3843
5031
|
let outputTokens = 0;
|
|
5032
|
+
let cachedInputTokens = 0;
|
|
3844
5033
|
const toolCalls = /* @__PURE__ */ new Map();
|
|
3845
5034
|
yield { type: "message_start", id: messageId };
|
|
3846
5035
|
let buffer = "";
|
|
@@ -3867,16 +5056,16 @@ var OpenRouterProvider = class {
|
|
|
3867
5056
|
}
|
|
3868
5057
|
if (choice.delta.tool_calls) {
|
|
3869
5058
|
for (const tc of choice.delta.tool_calls) {
|
|
3870
|
-
const
|
|
3871
|
-
if (!toolCalls.has(
|
|
5059
|
+
const key = tc.index ?? tc.id ?? 0;
|
|
5060
|
+
if (!toolCalls.has(key)) {
|
|
3872
5061
|
const id = tc.id || crypto.randomUUID();
|
|
3873
5062
|
const name = tc.function?.name || "";
|
|
3874
|
-
toolCalls.set(
|
|
5063
|
+
toolCalls.set(key, { id, name, args: "" });
|
|
3875
5064
|
if (name) {
|
|
3876
5065
|
yield { type: "tool_call_start", id, name };
|
|
3877
5066
|
}
|
|
3878
5067
|
}
|
|
3879
|
-
const existing = toolCalls.get(
|
|
5068
|
+
const existing = toolCalls.get(key);
|
|
3880
5069
|
if (tc.function?.name && !existing.name) {
|
|
3881
5070
|
existing.name = tc.function.name;
|
|
3882
5071
|
yield { type: "tool_call_start", id: existing.id, name: existing.name };
|
|
@@ -3897,13 +5086,14 @@ var OpenRouterProvider = class {
|
|
|
3897
5086
|
if (chunk.usage) {
|
|
3898
5087
|
inputTokens = chunk.usage.prompt_tokens;
|
|
3899
5088
|
outputTokens = chunk.usage.completion_tokens;
|
|
5089
|
+
cachedInputTokens = extractCachedTokens(chunk.usage);
|
|
3900
5090
|
}
|
|
3901
5091
|
}
|
|
3902
5092
|
}
|
|
3903
5093
|
const stopReason = toolCalls.size > 0 ? "tool_use" : "end_turn";
|
|
3904
5094
|
yield {
|
|
3905
5095
|
type: "message_end",
|
|
3906
|
-
usage: { inputTokens, outputTokens },
|
|
5096
|
+
usage: { inputTokens, outputTokens, cachedInputTokens },
|
|
3907
5097
|
stopReason
|
|
3908
5098
|
};
|
|
3909
5099
|
}
|
|
@@ -3922,7 +5112,7 @@ var OpenRouterProvider = class {
|
|
|
3922
5112
|
});
|
|
3923
5113
|
if (!res.ok) {
|
|
3924
5114
|
const errorText = await res.text().catch(() => res.statusText);
|
|
3925
|
-
throw new Error(
|
|
5115
|
+
throw new Error(formatOpenRouterError(res.status, errorText));
|
|
3926
5116
|
}
|
|
3927
5117
|
const data = await res.json();
|
|
3928
5118
|
const choice = data.choices?.[0];
|
|
@@ -3934,15 +5124,18 @@ var OpenRouterProvider = class {
|
|
|
3934
5124
|
if (choice.message.tool_calls) {
|
|
3935
5125
|
for (const tc of choice.message.tool_calls) {
|
|
3936
5126
|
let input = {};
|
|
5127
|
+
let invalidArgs = false;
|
|
3937
5128
|
try {
|
|
3938
|
-
input = JSON.parse(tc.function.arguments);
|
|
5129
|
+
input = tc.function.arguments ? JSON.parse(tc.function.arguments) : {};
|
|
3939
5130
|
} catch {
|
|
5131
|
+
invalidArgs = true;
|
|
3940
5132
|
}
|
|
3941
5133
|
content.push({
|
|
3942
5134
|
type: "tool_use",
|
|
3943
|
-
id: tc.id,
|
|
5135
|
+
id: tc.id || crypto.randomUUID(),
|
|
3944
5136
|
name: tc.function.name,
|
|
3945
|
-
input
|
|
5137
|
+
input,
|
|
5138
|
+
...invalidArgs ? { invalidArgs: true } : {}
|
|
3946
5139
|
});
|
|
3947
5140
|
}
|
|
3948
5141
|
}
|
|
@@ -3952,7 +5145,8 @@ var OpenRouterProvider = class {
|
|
|
3952
5145
|
content,
|
|
3953
5146
|
usage: {
|
|
3954
5147
|
inputTokens: data.usage?.prompt_tokens || 0,
|
|
3955
|
-
outputTokens: data.usage?.completion_tokens || 0
|
|
5148
|
+
outputTokens: data.usage?.completion_tokens || 0,
|
|
5149
|
+
cachedInputTokens: extractCachedTokens(data.usage)
|
|
3956
5150
|
},
|
|
3957
5151
|
stopReason: hasTools ? "tool_use" : "end_turn"
|
|
3958
5152
|
};
|
|
@@ -4046,126 +5240,463 @@ var OpenRouterProvider = class {
|
|
|
4046
5240
|
}
|
|
4047
5241
|
}
|
|
4048
5242
|
}
|
|
4049
|
-
return result;
|
|
5243
|
+
return result;
|
|
5244
|
+
}
|
|
5245
|
+
};
|
|
5246
|
+
|
|
5247
|
+
// src/ui/useModelSwitch.ts
|
|
5248
|
+
async function switchModel(newModel, session, setProvider, setModel, setCaps, setDisplayMessages) {
|
|
5249
|
+
const config = loadConfig();
|
|
5250
|
+
const isOpenRouter = newModel.includes("/");
|
|
5251
|
+
let newProvider;
|
|
5252
|
+
if (isOpenRouter) {
|
|
5253
|
+
const or = new OpenRouterProvider();
|
|
5254
|
+
await or.connect({ model: newModel, apiKey: config.openrouter.api_key });
|
|
5255
|
+
newProvider = or;
|
|
5256
|
+
} else {
|
|
5257
|
+
const ollama = new OllamaProvider();
|
|
5258
|
+
await ollama.connect({ model: newModel, baseUrl: config.ollama.base_url });
|
|
5259
|
+
newProvider = ollama;
|
|
5260
|
+
}
|
|
5261
|
+
setProvider(newProvider);
|
|
5262
|
+
setModel(newModel);
|
|
5263
|
+
setCaps(newProvider.getCapabilities());
|
|
5264
|
+
session.model = newModel;
|
|
5265
|
+
session.provider = newProvider.name;
|
|
5266
|
+
saveSession(session);
|
|
5267
|
+
setDisplayMessages((prev) => [...prev, { role: "tool_result", text: `switched to ${newModel}`, isError: false }]);
|
|
5268
|
+
}
|
|
5269
|
+
|
|
5270
|
+
// src/retrieval/repomap.ts
|
|
5271
|
+
import { readdirSync as readdirSync7, statSync as statSync3, lstatSync, readFileSync as readFileSync12 } from "fs";
|
|
5272
|
+
import { join as join12, relative } from "path";
|
|
5273
|
+
|
|
5274
|
+
// src/retrieval/languages.ts
|
|
5275
|
+
import { extname as extname2, basename as basename5 } from "path";
|
|
5276
|
+
var EXT_MAP = {
|
|
5277
|
+
// typescript / javascript
|
|
5278
|
+
".ts": "typescript",
|
|
5279
|
+
".mts": "typescript",
|
|
5280
|
+
".cts": "typescript",
|
|
5281
|
+
".tsx": "tsx",
|
|
5282
|
+
".js": "javascript",
|
|
5283
|
+
".mjs": "javascript",
|
|
5284
|
+
".cjs": "javascript",
|
|
5285
|
+
".jsx": "javascript",
|
|
5286
|
+
// scripting
|
|
5287
|
+
".py": "python",
|
|
5288
|
+
".pyw": "python",
|
|
5289
|
+
".pyx": "python",
|
|
5290
|
+
".rb": "ruby",
|
|
5291
|
+
".php": "php",
|
|
5292
|
+
".lua": "lua",
|
|
5293
|
+
".r": "r",
|
|
5294
|
+
".R": "r",
|
|
5295
|
+
".jl": "julia",
|
|
5296
|
+
".sh": "bash",
|
|
5297
|
+
".bash": "bash",
|
|
5298
|
+
".zsh": "bash",
|
|
5299
|
+
// systems
|
|
5300
|
+
".c": "c",
|
|
5301
|
+
".h": "c",
|
|
5302
|
+
".cpp": "cpp",
|
|
5303
|
+
".cc": "cpp",
|
|
5304
|
+
".cxx": "cpp",
|
|
5305
|
+
".hpp": "cpp",
|
|
5306
|
+
".hh": "cpp",
|
|
5307
|
+
".rs": "rust",
|
|
5308
|
+
".go": "go",
|
|
5309
|
+
".zig": "zig",
|
|
5310
|
+
// jvm
|
|
5311
|
+
".java": "java",
|
|
5312
|
+
".kt": "kotlin",
|
|
5313
|
+
".kts": "kotlin",
|
|
5314
|
+
".scala": "scala",
|
|
5315
|
+
".clj": "clojure",
|
|
5316
|
+
".cljs": "clojure",
|
|
5317
|
+
".cljc": "clojure",
|
|
5318
|
+
// dotnet
|
|
5319
|
+
".cs": "c-sharp",
|
|
5320
|
+
// mobile
|
|
5321
|
+
".swift": "swift",
|
|
5322
|
+
".dart": "dart",
|
|
5323
|
+
// functional
|
|
5324
|
+
".hs": "haskell",
|
|
5325
|
+
".ex": "elixir",
|
|
5326
|
+
".exs": "elixir",
|
|
5327
|
+
".erl": "erlang",
|
|
5328
|
+
".hrl": "erlang",
|
|
5329
|
+
".ml": "ocaml",
|
|
5330
|
+
".mli": "ocaml",
|
|
5331
|
+
// markup / data
|
|
5332
|
+
".html": "html",
|
|
5333
|
+
".htm": "html",
|
|
5334
|
+
".css": "css",
|
|
5335
|
+
".json": "json",
|
|
5336
|
+
".toml": "toml",
|
|
5337
|
+
".yaml": "yaml",
|
|
5338
|
+
".yml": "yaml",
|
|
5339
|
+
".md": "markdown",
|
|
5340
|
+
".mdx": "markdown",
|
|
5341
|
+
".sql": "sql",
|
|
5342
|
+
".graphql": "graphql",
|
|
5343
|
+
".gql": "graphql",
|
|
5344
|
+
".svelte": "svelte"
|
|
5345
|
+
};
|
|
5346
|
+
var FILENAME_MAP = {
|
|
5347
|
+
"Dockerfile": "dockerfile",
|
|
5348
|
+
"Containerfile": "dockerfile",
|
|
5349
|
+
"Makefile": "make",
|
|
5350
|
+
"makefile": "make",
|
|
5351
|
+
"GNUmakefile": "make"
|
|
5352
|
+
};
|
|
5353
|
+
function detectLanguage2(filePath) {
|
|
5354
|
+
const base = basename5(filePath);
|
|
5355
|
+
if (FILENAME_MAP[base]) return FILENAME_MAP[base];
|
|
5356
|
+
const ext = extname2(filePath).toLowerCase();
|
|
5357
|
+
return EXT_MAP[ext] ?? null;
|
|
5358
|
+
}
|
|
5359
|
+
function knownLanguages() {
|
|
5360
|
+
return /* @__PURE__ */ new Set([...Object.values(EXT_MAP), ...Object.values(FILENAME_MAP)]);
|
|
5361
|
+
}
|
|
5362
|
+
|
|
5363
|
+
// src/retrieval/treesitter.ts
|
|
5364
|
+
import { readFileSync as readFileSync10, existsSync as existsSync10 } from "fs";
|
|
5365
|
+
import { dirname, join as join10 } from "path";
|
|
5366
|
+
import { fileURLToPath } from "url";
|
|
5367
|
+
import { Parser, Language } from "web-tree-sitter";
|
|
5368
|
+
var initPromise = null;
|
|
5369
|
+
var grammars = /* @__PURE__ */ new Map();
|
|
5370
|
+
var resolvedWasmDir = null;
|
|
5371
|
+
function wasmDir() {
|
|
5372
|
+
if (resolvedWasmDir) return resolvedWasmDir;
|
|
5373
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
5374
|
+
const candidates = [
|
|
5375
|
+
join10(here, "wasm"),
|
|
5376
|
+
// dist/cli.js → dist/wasm/ (bundled)
|
|
5377
|
+
join10(here, "..", "wasm", "build"),
|
|
5378
|
+
// src/retrieval/foo.ts → wasm/build (dev, tsx)
|
|
5379
|
+
join10(here, "..", "..", "wasm", "build")
|
|
5380
|
+
// dist/cli.js fallback if dist/ is one level deeper
|
|
5381
|
+
];
|
|
5382
|
+
for (const c of candidates) {
|
|
5383
|
+
if (existsSync10(c)) {
|
|
5384
|
+
resolvedWasmDir = c;
|
|
5385
|
+
return c;
|
|
5386
|
+
}
|
|
5387
|
+
}
|
|
5388
|
+
throw new Error(
|
|
5389
|
+
`grammar wasm directory not found. checked: ${candidates.join(", ")}. run \`npm run build:wasms\` to build them, or \`npm run cp:wasms\` to copy from wasm/build to dist/wasm.`
|
|
5390
|
+
);
|
|
5391
|
+
}
|
|
5392
|
+
async function ensureInit() {
|
|
5393
|
+
if (!initPromise) initPromise = Parser.init();
|
|
5394
|
+
return initPromise;
|
|
5395
|
+
}
|
|
5396
|
+
async function loadGrammar(language) {
|
|
5397
|
+
if (grammars.has(language)) return grammars.get(language);
|
|
5398
|
+
await ensureInit();
|
|
5399
|
+
const path = join10(wasmDir(), `tree-sitter-${language}.wasm`);
|
|
5400
|
+
if (!existsSync10(path)) {
|
|
5401
|
+
grammars.set(language, null);
|
|
5402
|
+
return null;
|
|
5403
|
+
}
|
|
5404
|
+
try {
|
|
5405
|
+
const lang = await Language.load(readFileSync10(path));
|
|
5406
|
+
grammars.set(language, lang);
|
|
5407
|
+
return lang;
|
|
5408
|
+
} catch {
|
|
5409
|
+
grammars.set(language, null);
|
|
5410
|
+
return null;
|
|
5411
|
+
}
|
|
5412
|
+
}
|
|
5413
|
+
async function extractSymbols(filePath, source) {
|
|
5414
|
+
const language = detectLanguage2(filePath);
|
|
5415
|
+
if (!language) return null;
|
|
5416
|
+
const grammar = await loadGrammar(language);
|
|
5417
|
+
if (!grammar) return null;
|
|
5418
|
+
const parser = new Parser();
|
|
5419
|
+
parser.setLanguage(grammar);
|
|
5420
|
+
const tree = parser.parse(source);
|
|
5421
|
+
if (!tree) {
|
|
5422
|
+
parser.delete();
|
|
5423
|
+
return null;
|
|
5424
|
+
}
|
|
5425
|
+
const symbols = [];
|
|
5426
|
+
const imports = [];
|
|
5427
|
+
const root = tree.rootNode;
|
|
5428
|
+
collectFromNode(root, symbols, imports);
|
|
5429
|
+
tree.delete();
|
|
5430
|
+
parser.delete();
|
|
5431
|
+
return { language, symbols, imports };
|
|
5432
|
+
}
|
|
5433
|
+
function collectFromNode(root, symbols, imports) {
|
|
5434
|
+
for (let i = 0; i < root.childCount; i++) {
|
|
5435
|
+
const child = root.child(i);
|
|
5436
|
+
if (!child) continue;
|
|
5437
|
+
pushSymbolIfNamed(child, symbols);
|
|
5438
|
+
pushImportIfImport(child, imports);
|
|
5439
|
+
if (child.type === "export_statement") {
|
|
5440
|
+
for (let j = 0; j < child.childCount; j++) {
|
|
5441
|
+
const inner = child.child(j);
|
|
5442
|
+
if (inner) pushSymbolIfNamed(inner, symbols);
|
|
5443
|
+
}
|
|
5444
|
+
}
|
|
5445
|
+
}
|
|
5446
|
+
}
|
|
5447
|
+
function pushSymbolIfNamed(node, symbols) {
|
|
5448
|
+
const nameNode = node.childForFieldName("name");
|
|
5449
|
+
if (!nameNode) return;
|
|
5450
|
+
const firstLine = node.text.split("\n")[0] ?? "";
|
|
5451
|
+
symbols.push({
|
|
5452
|
+
kind: node.type,
|
|
5453
|
+
name: nameNode.text,
|
|
5454
|
+
line: node.startPosition.row + 1,
|
|
5455
|
+
signature: firstLine.length > 100 ? firstLine.slice(0, 100) + "..." : firstLine
|
|
5456
|
+
});
|
|
5457
|
+
}
|
|
5458
|
+
function pushImportIfImport(node, imports) {
|
|
5459
|
+
if (!node.type.includes("import") && node.type !== "use_declaration") return;
|
|
5460
|
+
const src = findStringDescendant(node);
|
|
5461
|
+
if (src) imports.push(src);
|
|
5462
|
+
}
|
|
5463
|
+
function findStringDescendant(node) {
|
|
5464
|
+
for (let i = 0; i < node.childCount; i++) {
|
|
5465
|
+
const child = node.child(i);
|
|
5466
|
+
if (!child) continue;
|
|
5467
|
+
if (child.type === "string" || child.type === "string_literal") {
|
|
5468
|
+
return child.text.replace(/^["'`]|["'`]$/g, "");
|
|
5469
|
+
}
|
|
5470
|
+
const nested = findStringDescendant(child);
|
|
5471
|
+
if (nested) return nested;
|
|
5472
|
+
}
|
|
5473
|
+
return null;
|
|
5474
|
+
}
|
|
5475
|
+
|
|
5476
|
+
// src/retrieval/cache.ts
|
|
5477
|
+
import { existsSync as existsSync11, mkdirSync as mkdirSync6, readFileSync as readFileSync11, writeFileSync as writeFileSync6, renameSync, readdirSync as readdirSync6, rmSync, statSync as statSync2 } from "fs";
|
|
5478
|
+
import { join as join11 } from "path";
|
|
5479
|
+
import { homedir as homedir10 } from "os";
|
|
5480
|
+
import { createHash as createHash3 } from "crypto";
|
|
5481
|
+
var ROOT = join11(homedir10(), ".prism", "cache", "trees");
|
|
5482
|
+
function cacheDir(projectId) {
|
|
5483
|
+
return join11(ROOT, projectId);
|
|
5484
|
+
}
|
|
5485
|
+
function hashPath(filePath) {
|
|
5486
|
+
return createHash3("sha256").update(filePath).digest("hex").slice(0, 16);
|
|
5487
|
+
}
|
|
5488
|
+
function entryFile(projectId, filePath) {
|
|
5489
|
+
return join11(cacheDir(projectId), `${hashPath(filePath)}.json`);
|
|
5490
|
+
}
|
|
5491
|
+
function getCached(projectId, filePath, mtime, size) {
|
|
5492
|
+
const path = entryFile(projectId, filePath);
|
|
5493
|
+
if (!existsSync11(path)) return null;
|
|
5494
|
+
try {
|
|
5495
|
+
const data = JSON.parse(readFileSync11(path, "utf-8"));
|
|
5496
|
+
if (data.path !== filePath) return null;
|
|
5497
|
+
if (data.mtime !== mtime) return null;
|
|
5498
|
+
if (data.size !== size) return null;
|
|
5499
|
+
return data;
|
|
5500
|
+
} catch {
|
|
5501
|
+
return null;
|
|
5502
|
+
}
|
|
5503
|
+
}
|
|
5504
|
+
function setCached(projectId, filePath, data) {
|
|
5505
|
+
const dir = cacheDir(projectId);
|
|
5506
|
+
if (!existsSync11(dir)) {
|
|
5507
|
+
try {
|
|
5508
|
+
mkdirSync6(dir, { recursive: true });
|
|
5509
|
+
} catch {
|
|
5510
|
+
return;
|
|
5511
|
+
}
|
|
4050
5512
|
}
|
|
4051
|
-
|
|
4052
|
-
|
|
4053
|
-
|
|
4054
|
-
|
|
4055
|
-
|
|
4056
|
-
|
|
4057
|
-
|
|
4058
|
-
|
|
4059
|
-
|
|
4060
|
-
|
|
4061
|
-
|
|
4062
|
-
|
|
4063
|
-
|
|
4064
|
-
|
|
4065
|
-
|
|
4066
|
-
ollama: { base_url: "http://localhost:11434" }
|
|
4067
|
-
};
|
|
4068
|
-
function loadConfig() {
|
|
4069
|
-
const config = { ...DEFAULTS };
|
|
4070
|
-
if (existsSync9(CONFIG_PATH)) {
|
|
5513
|
+
const entry = {
|
|
5514
|
+
path: filePath,
|
|
5515
|
+
mtime: data.mtime,
|
|
5516
|
+
size: data.size,
|
|
5517
|
+
language: data.language,
|
|
5518
|
+
symbols: data.symbols,
|
|
5519
|
+
imports: data.imports,
|
|
5520
|
+
cachedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
5521
|
+
};
|
|
5522
|
+
const final = entryFile(projectId, filePath);
|
|
5523
|
+
const tmp = `${final}.${process.pid}.tmp`;
|
|
5524
|
+
try {
|
|
5525
|
+
writeFileSync6(tmp, JSON.stringify(entry), "utf-8");
|
|
5526
|
+
renameSync(tmp, final);
|
|
5527
|
+
} catch {
|
|
4071
5528
|
try {
|
|
4072
|
-
|
|
4073
|
-
const parsed = parseToml(text);
|
|
4074
|
-
if (parsed.default_provider) config.default_provider = parsed.default_provider;
|
|
4075
|
-
if (parsed.default_model) config.default_model = parsed.default_model;
|
|
4076
|
-
if (parsed.openrouter?.api_key) config.openrouter.api_key = parsed.openrouter.api_key;
|
|
4077
|
-
if (parsed.anthropic?.api_key) config.anthropic.api_key = parsed.anthropic.api_key;
|
|
4078
|
-
if (parsed.openai?.api_key) config.openai.api_key = parsed.openai.api_key;
|
|
4079
|
-
if (parsed.google?.api_key) config.google.api_key = parsed.google.api_key;
|
|
4080
|
-
if (parsed.ollama?.base_url) config.ollama.base_url = parsed.ollama.base_url;
|
|
5529
|
+
rmSync(tmp, { force: true });
|
|
4081
5530
|
} catch {
|
|
4082
5531
|
}
|
|
4083
5532
|
}
|
|
4084
|
-
if (process.env.OPENROUTER_API_KEY) config.openrouter.api_key = process.env.OPENROUTER_API_KEY;
|
|
4085
|
-
if (process.env.ANTHROPIC_API_KEY) config.anthropic.api_key = process.env.ANTHROPIC_API_KEY;
|
|
4086
|
-
if (process.env.OPENAI_API_KEY) config.openai.api_key = process.env.OPENAI_API_KEY;
|
|
4087
|
-
if (process.env.GOOGLE_API_KEY) config.google.api_key = process.env.GOOGLE_API_KEY;
|
|
4088
|
-
if (process.env.OLLAMA_HOST) config.ollama.base_url = process.env.OLLAMA_HOST;
|
|
4089
|
-
return config;
|
|
4090
5533
|
}
|
|
4091
|
-
function initConfig() {
|
|
4092
|
-
if (existsSync9(CONFIG_PATH)) return;
|
|
4093
|
-
if (!existsSync9(PRISM_DIR)) {
|
|
4094
|
-
mkdirSync5(PRISM_DIR, { recursive: true });
|
|
4095
|
-
}
|
|
4096
|
-
const template = `# prism config
|
|
4097
|
-
# env vars override these values.
|
|
4098
|
-
|
|
4099
|
-
default_provider = "ollama"
|
|
4100
|
-
default_model = "deepseek-r1:14b"
|
|
4101
|
-
|
|
4102
|
-
[openrouter]
|
|
4103
|
-
api_key = ""
|
|
4104
|
-
|
|
4105
|
-
[anthropic]
|
|
4106
|
-
api_key = ""
|
|
4107
|
-
|
|
4108
|
-
[openai]
|
|
4109
|
-
api_key = ""
|
|
4110
|
-
|
|
4111
|
-
[google]
|
|
4112
|
-
api_key = ""
|
|
4113
5534
|
|
|
4114
|
-
|
|
4115
|
-
|
|
4116
|
-
|
|
4117
|
-
|
|
4118
|
-
|
|
4119
|
-
|
|
4120
|
-
|
|
4121
|
-
|
|
4122
|
-
|
|
4123
|
-
|
|
4124
|
-
|
|
4125
|
-
|
|
4126
|
-
|
|
4127
|
-
|
|
4128
|
-
|
|
4129
|
-
|
|
4130
|
-
|
|
4131
|
-
|
|
4132
|
-
|
|
5535
|
+
// src/retrieval/repomap.ts
|
|
5536
|
+
var IGNORE_DIRS2 = /* @__PURE__ */ new Set([
|
|
5537
|
+
"node_modules",
|
|
5538
|
+
".git",
|
|
5539
|
+
".venv",
|
|
5540
|
+
"venv",
|
|
5541
|
+
"env",
|
|
5542
|
+
"__pycache__",
|
|
5543
|
+
".mypy_cache",
|
|
5544
|
+
".pytest_cache",
|
|
5545
|
+
".ruff_cache",
|
|
5546
|
+
"dist",
|
|
5547
|
+
"build",
|
|
5548
|
+
"out",
|
|
5549
|
+
"target",
|
|
5550
|
+
"_build",
|
|
5551
|
+
".next",
|
|
5552
|
+
".nuxt",
|
|
5553
|
+
".svelte-kit",
|
|
5554
|
+
".astro",
|
|
5555
|
+
".cache",
|
|
5556
|
+
".parcel-cache",
|
|
5557
|
+
".turbo",
|
|
5558
|
+
"vendor",
|
|
5559
|
+
"deps",
|
|
5560
|
+
"_deps",
|
|
5561
|
+
"coverage",
|
|
5562
|
+
".nyc_output",
|
|
5563
|
+
".idea",
|
|
5564
|
+
".vscode",
|
|
5565
|
+
".fleet",
|
|
5566
|
+
// prism build artifact: wasm/sources/ holds cloned grammar repos (zig,
|
|
5567
|
+
// python, etc.); wasm/build/ holds the compiled wasms. neither is "the
|
|
5568
|
+
// project," both confuse the repo-map with generated parser code.
|
|
5569
|
+
"wasm"
|
|
5570
|
+
]);
|
|
5571
|
+
async function extractRepoMap(cwd, opts = {}) {
|
|
5572
|
+
const { tuning } = loadConfig();
|
|
5573
|
+
const maxFiles = opts.maxFiles ?? tuning.repomap_max_files;
|
|
5574
|
+
const maxSymbolsPerFile = opts.maxSymbolsPerFile ?? tuning.repomap_max_symbols_per_file;
|
|
5575
|
+
const ignore = /* @__PURE__ */ new Set([...IGNORE_DIRS2, ...opts.extraIgnore ?? []]);
|
|
5576
|
+
const supported = knownLanguages();
|
|
5577
|
+
const candidates = walkProject(cwd, ignore, supported, maxFiles);
|
|
5578
|
+
const projectId = getProjectId(cwd);
|
|
5579
|
+
const entries = [];
|
|
5580
|
+
let cacheHits = 0;
|
|
5581
|
+
let cacheMisses = 0;
|
|
5582
|
+
let parseFailures = 0;
|
|
5583
|
+
for (const filePath of candidates) {
|
|
5584
|
+
let mtime = 0;
|
|
5585
|
+
let size = 0;
|
|
5586
|
+
try {
|
|
5587
|
+
const st = statSync3(filePath);
|
|
5588
|
+
mtime = st.mtimeMs;
|
|
5589
|
+
size = st.size;
|
|
5590
|
+
} catch {
|
|
4133
5591
|
continue;
|
|
4134
5592
|
}
|
|
4135
|
-
const
|
|
4136
|
-
if (
|
|
4137
|
-
|
|
5593
|
+
const cached = getCached(projectId, filePath, mtime, size);
|
|
5594
|
+
if (cached) {
|
|
5595
|
+
cacheHits += 1;
|
|
5596
|
+
if (cached.symbols.length > 0) {
|
|
5597
|
+
entries.push({
|
|
5598
|
+
path: relative(cwd, filePath),
|
|
5599
|
+
language: cached.language,
|
|
5600
|
+
symbols: cached.symbols.slice(0, maxSymbolsPerFile)
|
|
5601
|
+
});
|
|
5602
|
+
}
|
|
4138
5603
|
continue;
|
|
4139
5604
|
}
|
|
4140
|
-
|
|
4141
|
-
|
|
4142
|
-
|
|
5605
|
+
cacheMisses += 1;
|
|
5606
|
+
let source;
|
|
5607
|
+
try {
|
|
5608
|
+
source = readFileSync12(filePath, "utf-8");
|
|
5609
|
+
} catch {
|
|
5610
|
+
parseFailures += 1;
|
|
5611
|
+
continue;
|
|
5612
|
+
}
|
|
5613
|
+
const result = await extractSymbols(filePath, source);
|
|
5614
|
+
if (!result) {
|
|
5615
|
+
parseFailures += 1;
|
|
5616
|
+
continue;
|
|
5617
|
+
}
|
|
5618
|
+
setCached(projectId, filePath, {
|
|
5619
|
+
mtime,
|
|
5620
|
+
size,
|
|
5621
|
+
language: result.language,
|
|
5622
|
+
symbols: result.symbols,
|
|
5623
|
+
imports: result.imports
|
|
5624
|
+
});
|
|
5625
|
+
if (result.symbols.length > 0) {
|
|
5626
|
+
entries.push({
|
|
5627
|
+
path: relative(cwd, filePath),
|
|
5628
|
+
language: result.language,
|
|
5629
|
+
symbols: result.symbols.slice(0, maxSymbolsPerFile)
|
|
5630
|
+
});
|
|
4143
5631
|
}
|
|
4144
5632
|
}
|
|
4145
|
-
return
|
|
5633
|
+
return {
|
|
5634
|
+
cwd,
|
|
5635
|
+
entries,
|
|
5636
|
+
filesWalked: candidates.length,
|
|
5637
|
+
cacheHits,
|
|
5638
|
+
cacheMisses,
|
|
5639
|
+
parseFailures
|
|
5640
|
+
};
|
|
4146
5641
|
}
|
|
4147
|
-
|
|
4148
|
-
|
|
4149
|
-
|
|
4150
|
-
const
|
|
4151
|
-
|
|
4152
|
-
let
|
|
4153
|
-
|
|
4154
|
-
const
|
|
4155
|
-
|
|
4156
|
-
|
|
4157
|
-
}
|
|
4158
|
-
|
|
4159
|
-
|
|
4160
|
-
|
|
5642
|
+
function formatRepoMap(data, opts = {}) {
|
|
5643
|
+
const maxLines = opts.maxLines ?? loadConfig().tuning.repomap_max_lines;
|
|
5644
|
+
if (data.entries.length === 0) return "";
|
|
5645
|
+
const lines = ["# repo map", ""];
|
|
5646
|
+
let truncated = 0;
|
|
5647
|
+
for (let i = 0; i < data.entries.length; i++) {
|
|
5648
|
+
const entry = data.entries[i];
|
|
5649
|
+
const block = [entry.path];
|
|
5650
|
+
for (const sym of entry.symbols) {
|
|
5651
|
+
const kind = sym.kind.replace(/_declaration$|_definition$/, "");
|
|
5652
|
+
block.push(` ${kind} ${sym.name}`);
|
|
5653
|
+
}
|
|
5654
|
+
if (lines.length + block.length + 1 > maxLines) {
|
|
5655
|
+
truncated = data.entries.length - i;
|
|
5656
|
+
break;
|
|
5657
|
+
}
|
|
5658
|
+
lines.push(...block);
|
|
5659
|
+
lines.push("");
|
|
4161
5660
|
}
|
|
4162
|
-
|
|
4163
|
-
|
|
4164
|
-
|
|
4165
|
-
|
|
4166
|
-
|
|
4167
|
-
|
|
4168
|
-
|
|
5661
|
+
if (truncated > 0) {
|
|
5662
|
+
lines.push(`...and ${truncated} more files (call Read or Grep to inspect)`);
|
|
5663
|
+
}
|
|
5664
|
+
return lines.join("\n").trimEnd();
|
|
5665
|
+
}
|
|
5666
|
+
function walkProject(cwd, ignore, supportedLangs, maxFiles) {
|
|
5667
|
+
const results = [];
|
|
5668
|
+
const stack = [cwd];
|
|
5669
|
+
while (stack.length > 0 && results.length < maxFiles) {
|
|
5670
|
+
const dir = stack.pop();
|
|
5671
|
+
let entries;
|
|
5672
|
+
try {
|
|
5673
|
+
entries = readdirSync7(dir);
|
|
5674
|
+
} catch {
|
|
5675
|
+
continue;
|
|
5676
|
+
}
|
|
5677
|
+
for (const name of entries) {
|
|
5678
|
+
if (ignore.has(name)) continue;
|
|
5679
|
+
if (name.startsWith(".")) continue;
|
|
5680
|
+
const path = join12(dir, name);
|
|
5681
|
+
let s;
|
|
5682
|
+
try {
|
|
5683
|
+
s = lstatSync(path);
|
|
5684
|
+
} catch {
|
|
5685
|
+
continue;
|
|
5686
|
+
}
|
|
5687
|
+
if (s.isSymbolicLink()) continue;
|
|
5688
|
+
if (s.isDirectory()) {
|
|
5689
|
+
stack.push(path);
|
|
5690
|
+
} else if (s.isFile()) {
|
|
5691
|
+
const lang = detectLanguage2(name);
|
|
5692
|
+
if (lang && supportedLangs.has(lang)) {
|
|
5693
|
+
results.push(path);
|
|
5694
|
+
if (results.length >= maxFiles) break;
|
|
5695
|
+
}
|
|
5696
|
+
}
|
|
5697
|
+
}
|
|
5698
|
+
}
|
|
5699
|
+
return results;
|
|
4169
5700
|
}
|
|
4170
5701
|
|
|
4171
5702
|
// src/ui/App.tsx
|
|
@@ -4197,20 +5728,24 @@ function rebuildDisplayMessages(messages) {
|
|
|
4197
5728
|
}
|
|
4198
5729
|
return display;
|
|
4199
5730
|
}
|
|
4200
|
-
function App({ provider: initProvider, model: initModel, tools: baseTools, capabilities: initCaps, session, initialMessages, projectContext: initProjectContext, memory }) {
|
|
5731
|
+
function App({ provider: initProvider, model: initModel, tools: baseTools, capabilities: initCaps, session, initialMessages, projectContext: initProjectContext, memory, repoMapOverride }) {
|
|
4201
5732
|
const [provider, setProvider] = useState3(initProvider);
|
|
4202
5733
|
const [model, setModel] = useState3(initModel);
|
|
4203
5734
|
const [caps, setCaps] = useState3(initCaps);
|
|
4204
5735
|
const { exit } = useApp();
|
|
4205
5736
|
const [displayMessages, setDisplayMessages] = useState3(() => rebuildDisplayMessages(initialMessages));
|
|
4206
5737
|
const [isLoading, setIsLoading] = useState3(false);
|
|
5738
|
+
const submittingRef = useRef3(false);
|
|
4207
5739
|
const [turnCount, setTurnCount] = useState3(0);
|
|
4208
5740
|
const [tokenInfo, setTokenInfo] = useState3("");
|
|
5741
|
+
const [spinnerPhase, setSpinnerPhase] = useState3("thinking");
|
|
5742
|
+
const [spinnerTool, setSpinnerTool] = useState3(void 0);
|
|
4209
5743
|
const [profile, setProfile] = useState3(() => loadProfile(model));
|
|
4210
5744
|
const [pendingPermission, setPendingPermission] = useState3(null);
|
|
4211
5745
|
const [abortController, setAbortController] = useState3(null);
|
|
4212
5746
|
const [inPlanMode, setInPlanMode] = useState3(false);
|
|
4213
5747
|
const [activeSkills, setActiveSkills] = useState3(/* @__PURE__ */ new Set());
|
|
5748
|
+
const [repoMap, setRepoMap] = useState3("");
|
|
4214
5749
|
const [projectContext] = useState3(() => initProjectContext ?? scanProject(process.cwd()));
|
|
4215
5750
|
const [messages] = useState3(() => initialMessages ? [...initialMessages] : []);
|
|
4216
5751
|
const [agentTool] = useState3(() => createAgentTool({
|
|
@@ -4235,6 +5770,25 @@ function App({ provider: initProvider, model: initModel, tools: baseTools, capab
|
|
|
4235
5770
|
}
|
|
4236
5771
|
}));
|
|
4237
5772
|
const [skillTool] = useState3(() => createSkillTool(process.cwd()));
|
|
5773
|
+
useEffect3(() => {
|
|
5774
|
+
if (repoMapOverride?.skip) return;
|
|
5775
|
+
let cancelled = false;
|
|
5776
|
+
(async () => {
|
|
5777
|
+
try {
|
|
5778
|
+
const data = await extractRepoMap(process.cwd(), {
|
|
5779
|
+
...repoMapOverride?.maxFiles ? { maxFiles: repoMapOverride.maxFiles } : {}
|
|
5780
|
+
});
|
|
5781
|
+
const formatted = formatRepoMap(data, {
|
|
5782
|
+
...repoMapOverride?.maxLines ? { maxLines: repoMapOverride.maxLines } : {}
|
|
5783
|
+
});
|
|
5784
|
+
if (!cancelled) setRepoMap(formatted);
|
|
5785
|
+
} catch {
|
|
5786
|
+
}
|
|
5787
|
+
})();
|
|
5788
|
+
return () => {
|
|
5789
|
+
cancelled = true;
|
|
5790
|
+
};
|
|
5791
|
+
}, []);
|
|
4238
5792
|
const tools = useMemo2(() => [...baseTools, agentTool, skillTool], [baseTools, agentTool, skillTool]);
|
|
4239
5793
|
const toolSchemas = useMemo2(() => tools.map((t) => toolToSchema(t)), [tools]);
|
|
4240
5794
|
const getSystemPrompt = useCallback3(() => {
|
|
@@ -4250,9 +5804,10 @@ function App({ provider: initProvider, model: initModel, tools: baseTools, capab
|
|
|
4250
5804
|
projectContext,
|
|
4251
5805
|
memory,
|
|
4252
5806
|
inPlanMode,
|
|
4253
|
-
activeSkills
|
|
5807
|
+
activeSkills,
|
|
5808
|
+
repoMap
|
|
4254
5809
|
});
|
|
4255
|
-
}, [caps, toolSchemas, profile, memory, inPlanMode, activeSkills]);
|
|
5810
|
+
}, [caps, toolSchemas, profile, memory, inPlanMode, activeSkills, repoMap]);
|
|
4256
5811
|
useInput3((input, key) => {
|
|
4257
5812
|
if (!isLoading && key.ctrl && input === "c") {
|
|
4258
5813
|
exit();
|
|
@@ -4267,11 +5822,13 @@ function App({ provider: initProvider, model: initModel, tools: baseTools, capab
|
|
|
4267
5822
|
const runModelLoop = useCallback3(async () => {
|
|
4268
5823
|
setTurnCount((prev) => prev + 1);
|
|
4269
5824
|
setIsLoading(true);
|
|
5825
|
+
setSpinnerPhase("thinking");
|
|
5826
|
+
setSpinnerTool(void 0);
|
|
4270
5827
|
const controller = new AbortController();
|
|
4271
5828
|
setAbortController(controller);
|
|
4272
5829
|
const askPermission = (toolName, description, id) => {
|
|
4273
|
-
return new Promise((
|
|
4274
|
-
setPendingPermission({ toolName, description, id, resolve:
|
|
5830
|
+
return new Promise((resolve7) => {
|
|
5831
|
+
setPendingPermission({ toolName, description, id, resolve: resolve7 });
|
|
4275
5832
|
});
|
|
4276
5833
|
};
|
|
4277
5834
|
let currentText = "";
|
|
@@ -4301,9 +5858,13 @@ function App({ provider: initProvider, model: initModel, tools: baseTools, capab
|
|
|
4301
5858
|
});
|
|
4302
5859
|
break;
|
|
4303
5860
|
case "tool_start":
|
|
5861
|
+
setSpinnerPhase("running");
|
|
5862
|
+
setSpinnerTool(event.name);
|
|
4304
5863
|
setDisplayMessages((prev) => [...prev, { role: "tool_call", text: "", toolName: event.name }]);
|
|
4305
5864
|
break;
|
|
4306
5865
|
case "tool_end":
|
|
5866
|
+
setSpinnerPhase("after-tool");
|
|
5867
|
+
setSpinnerTool(event.name);
|
|
4307
5868
|
setDisplayMessages((prev) => [...prev, { role: "tool_result", text: event.result, isError: event.isError }]);
|
|
4308
5869
|
currentText = "";
|
|
4309
5870
|
break;
|
|
@@ -4350,11 +5911,11 @@ function App({ provider: initProvider, model: initModel, tools: baseTools, capab
|
|
|
4350
5911
|
messages.push({ role: "user", content: [{ type: "text", text: "the user interrupted the current operation. stop what you were doing and ask what they want instead." }] });
|
|
4351
5912
|
}
|
|
4352
5913
|
session.messages = messages;
|
|
4353
|
-
saveSession(session);
|
|
4354
5914
|
setTimeout(() => {
|
|
4355
5915
|
setAbortController(null);
|
|
4356
5916
|
setIsLoading(false);
|
|
4357
5917
|
}, 0);
|
|
5918
|
+
setTimeout(() => saveSession(session), 0);
|
|
4358
5919
|
}, [provider, model, tools, messages, getSystemPrompt, session]);
|
|
4359
5920
|
const triggerSyntheticTurn = useCallback3((hiddenMsg) => {
|
|
4360
5921
|
messages.push({ role: "user", content: [{ type: "text", text: hiddenMsg }] });
|
|
@@ -4382,9 +5943,15 @@ function App({ provider: initProvider, model: initModel, tools: baseTools, capab
|
|
|
4382
5943
|
});
|
|
4383
5944
|
if (handled) return;
|
|
4384
5945
|
}
|
|
4385
|
-
|
|
4386
|
-
|
|
4387
|
-
|
|
5946
|
+
if (submittingRef.current) return;
|
|
5947
|
+
submittingRef.current = true;
|
|
5948
|
+
try {
|
|
5949
|
+
setDisplayMessages((prev) => [...prev, { role: "user", text: input }]);
|
|
5950
|
+
messages.push({ role: "user", content: [{ type: "text", text: input }] });
|
|
5951
|
+
await runModelLoop();
|
|
5952
|
+
} finally {
|
|
5953
|
+
submittingRef.current = false;
|
|
5954
|
+
}
|
|
4388
5955
|
}, [provider, model, tools, messages, getSystemPrompt, inPlanMode, triggerSyntheticTurn]);
|
|
4389
5956
|
return /* @__PURE__ */ jsxs8(Box8, { flexDirection: "column", padding: 1, children: [
|
|
4390
5957
|
/* @__PURE__ */ jsx8(
|
|
@@ -4419,7 +5986,9 @@ function App({ provider: initProvider, model: initModel, tools: baseTools, capab
|
|
|
4419
5986
|
onSubmit: handleSubmit,
|
|
4420
5987
|
isLoading,
|
|
4421
5988
|
inPlanMode,
|
|
4422
|
-
invokeSkills: invokeSkillSpecs
|
|
5989
|
+
invokeSkills: invokeSkillSpecs,
|
|
5990
|
+
phase: spinnerPhase,
|
|
5991
|
+
currentTool: spinnerTool
|
|
4423
5992
|
}
|
|
4424
5993
|
)
|
|
4425
5994
|
] });
|
|
@@ -4427,8 +5996,49 @@ function App({ provider: initProvider, model: initModel, tools: baseTools, capab
|
|
|
4427
5996
|
|
|
4428
5997
|
// src/tools/bash.ts
|
|
4429
5998
|
import { z as z4 } from "zod";
|
|
4430
|
-
import {
|
|
4431
|
-
|
|
5999
|
+
import { exec as exec2 } from "child_process";
|
|
6000
|
+
import { promisify } from "util";
|
|
6001
|
+
|
|
6002
|
+
// src/tools/sensitivePaths.ts
|
|
6003
|
+
import { resolve, relative as relative2, isAbsolute, basename as basename6, join as join13 } from "path";
|
|
6004
|
+
import { homedir as homedir11 } from "os";
|
|
6005
|
+
import { realpathSync } from "fs";
|
|
6006
|
+
var SECRET_NAME = /^(\.env(\..+)?|\.netrc|\.git-credentials|\.npmrc|id_(rsa|ed25519|ecdsa|dsa)|credentials)$/i;
|
|
6007
|
+
var SECRET_EXT = /\.(pem|key|p12|pfx|keystore)$/i;
|
|
6008
|
+
function expandHome(p) {
|
|
6009
|
+
if (p === "~") return homedir11();
|
|
6010
|
+
if (p.startsWith("~/")) return join13(homedir11(), p.slice(2));
|
|
6011
|
+
return p;
|
|
6012
|
+
}
|
|
6013
|
+
function realOrLexical(p) {
|
|
6014
|
+
try {
|
|
6015
|
+
return realpathSync.native(p);
|
|
6016
|
+
} catch {
|
|
6017
|
+
return p;
|
|
6018
|
+
}
|
|
6019
|
+
}
|
|
6020
|
+
function classifyRead(targetPath, cwd) {
|
|
6021
|
+
if (targetPath.startsWith("~") && targetPath !== "~" && !targetPath.startsWith("~/")) {
|
|
6022
|
+
return { allow: false, reason: "outside-project" };
|
|
6023
|
+
}
|
|
6024
|
+
const expanded = expandHome(targetPath);
|
|
6025
|
+
const abs = realOrLexical(isAbsolute(expanded) ? expanded : resolve(cwd, expanded));
|
|
6026
|
+
const root = realOrLexical(resolve(cwd));
|
|
6027
|
+
const rel = relative2(root, abs);
|
|
6028
|
+
const outside = rel.split(/[\\/]/)[0] === ".." || isAbsolute(rel);
|
|
6029
|
+
if (outside) return { allow: false, reason: "outside-project" };
|
|
6030
|
+
const name = basename6(abs);
|
|
6031
|
+
if (SECRET_NAME.test(name) || SECRET_EXT.test(name)) {
|
|
6032
|
+
return { allow: false, reason: "secret-name" };
|
|
6033
|
+
}
|
|
6034
|
+
return { allow: true, reason: "in-project" };
|
|
6035
|
+
}
|
|
6036
|
+
function readAskMessage(targetPath, reason) {
|
|
6037
|
+
return reason === "secret-name" ? `read possible secret file: ${targetPath}` : `read file outside the project: ${targetPath}`;
|
|
6038
|
+
}
|
|
6039
|
+
|
|
6040
|
+
// src/tools/bash.ts
|
|
6041
|
+
var execAsync = promisify(exec2);
|
|
4432
6042
|
var SAFE_COMMANDS = /* @__PURE__ */ new Set([
|
|
4433
6043
|
"ls",
|
|
4434
6044
|
"cat",
|
|
@@ -4478,6 +6088,14 @@ var DANGEROUS_PATTERNS = [
|
|
|
4478
6088
|
/\bmkfs\b/,
|
|
4479
6089
|
/\bkill\s+-9\b/
|
|
4480
6090
|
];
|
|
6091
|
+
var SHELL_METACHARS = /[;&|<>$`\n()]/;
|
|
6092
|
+
function isSimpleSafeCommand(command) {
|
|
6093
|
+
const cmd = command.trim();
|
|
6094
|
+
if (SHELL_METACHARS.test(cmd)) return false;
|
|
6095
|
+
if (DANGEROUS_PATTERNS.some((p) => p.test(cmd))) return false;
|
|
6096
|
+
const firstToken = cmd.split(/\s+/)[0] || "";
|
|
6097
|
+
return SAFE_COMMANDS.has(firstToken) || SAFE_COMMANDS.has(cmd);
|
|
6098
|
+
}
|
|
4481
6099
|
var inputSchema2 = z4.object({
|
|
4482
6100
|
command: z4.string().describe("the shell command to execute"),
|
|
4483
6101
|
description: z4.string().optional().describe("what this command does"),
|
|
@@ -4488,21 +6106,19 @@ var BashTool = buildTool({
|
|
|
4488
6106
|
description: "Execute a shell command and return its output.",
|
|
4489
6107
|
inputSchema: inputSchema2,
|
|
4490
6108
|
async call(input, context) {
|
|
6109
|
+
const maxOutput = loadConfig().tuning.bash_max_output_bytes;
|
|
4491
6110
|
const timeout = Math.min(input.timeout || 12e4, 6e5);
|
|
4492
6111
|
try {
|
|
4493
|
-
const
|
|
6112
|
+
const { stdout } = await execAsync(input.command, {
|
|
4494
6113
|
cwd: context.cwd,
|
|
4495
6114
|
timeout,
|
|
4496
|
-
maxBuffer:
|
|
4497
|
-
encoding: "utf-8",
|
|
4498
|
-
stdio: ["pipe", "pipe", "pipe"],
|
|
6115
|
+
maxBuffer: maxOutput,
|
|
4499
6116
|
env: { ...process.env }
|
|
4500
6117
|
});
|
|
4501
|
-
const
|
|
4502
|
-
|
|
4503
|
-
if (result.length > MAX_OUTPUT2) {
|
|
6118
|
+
const result = stdout.trim();
|
|
6119
|
+
if (result.length > maxOutput) {
|
|
4504
6120
|
return {
|
|
4505
|
-
content: result.slice(0,
|
|
6121
|
+
content: result.slice(0, maxOutput) + "\n\n[output truncated]"
|
|
4506
6122
|
};
|
|
4507
6123
|
}
|
|
4508
6124
|
return { content: result || "(no output)" };
|
|
@@ -4510,7 +6126,7 @@ var BashTool = buildTool({
|
|
|
4510
6126
|
const execError = error;
|
|
4511
6127
|
const stdout = execError.stdout?.toString().trim() || "";
|
|
4512
6128
|
const stderr = execError.stderr?.toString().trim() || "";
|
|
4513
|
-
const exitCode = execError.
|
|
6129
|
+
const exitCode = execError.code ?? 1;
|
|
4514
6130
|
let content = "";
|
|
4515
6131
|
if (stdout) content += stdout + "\n";
|
|
4516
6132
|
if (stderr) content += stderr + "\n";
|
|
@@ -4520,14 +6136,12 @@ Exit code: ${exitCode}`;
|
|
|
4520
6136
|
}
|
|
4521
6137
|
},
|
|
4522
6138
|
isConcurrencySafe(input) {
|
|
4523
|
-
|
|
4524
|
-
return SAFE_COMMANDS.has(cmd) || SAFE_COMMANDS.has(input.command.trim());
|
|
6139
|
+
return isSimpleSafeCommand(input.command);
|
|
4525
6140
|
},
|
|
4526
6141
|
isReadOnly(input) {
|
|
4527
|
-
|
|
4528
|
-
return SAFE_COMMANDS.has(cmd) || SAFE_COMMANDS.has(input.command.trim());
|
|
6142
|
+
return isSimpleSafeCommand(input.command);
|
|
4529
6143
|
},
|
|
4530
|
-
checkPermissions(input) {
|
|
6144
|
+
checkPermissions(input, context) {
|
|
4531
6145
|
for (const pattern of DANGEROUS_PATTERNS) {
|
|
4532
6146
|
if (pattern.test(input.command)) {
|
|
4533
6147
|
return {
|
|
@@ -4536,7 +6150,14 @@ Exit code: ${exitCode}`;
|
|
|
4536
6150
|
};
|
|
4537
6151
|
}
|
|
4538
6152
|
}
|
|
4539
|
-
if (
|
|
6153
|
+
if (isSimpleSafeCommand(input.command)) {
|
|
6154
|
+
const args = input.command.trim().split(/\s+/).slice(1);
|
|
6155
|
+
for (const arg of args) {
|
|
6156
|
+
if (arg.startsWith("-")) continue;
|
|
6157
|
+
if (!classifyRead(arg, context.cwd).allow) {
|
|
6158
|
+
return { behavior: "ask", message: `run: ${input.command}` };
|
|
6159
|
+
}
|
|
6160
|
+
}
|
|
4540
6161
|
return { behavior: "allow" };
|
|
4541
6162
|
}
|
|
4542
6163
|
return { behavior: "ask", message: `run: ${input.command}` };
|
|
@@ -4545,11 +6166,11 @@ Exit code: ${exitCode}`;
|
|
|
4545
6166
|
|
|
4546
6167
|
// src/tools/read.ts
|
|
4547
6168
|
import { z as z5 } from "zod";
|
|
4548
|
-
import { readFileSync as
|
|
4549
|
-
import { resolve, isAbsolute, extname as
|
|
6169
|
+
import { readFileSync as readFileSync16, statSync as statSync4 } from "fs";
|
|
6170
|
+
import { resolve as resolve2, isAbsolute as isAbsolute2, extname as extname4 } from "path";
|
|
4550
6171
|
|
|
4551
6172
|
// src/parsers/pdf.ts
|
|
4552
|
-
import { execFileSync
|
|
6173
|
+
import { execFileSync } from "child_process";
|
|
4553
6174
|
function parsePdf(filePath, pages) {
|
|
4554
6175
|
const args = ["-layout"];
|
|
4555
6176
|
if (pages) {
|
|
@@ -4558,7 +6179,7 @@ function parsePdf(filePath, pages) {
|
|
|
4558
6179
|
}
|
|
4559
6180
|
args.push(filePath, "-");
|
|
4560
6181
|
try {
|
|
4561
|
-
const output =
|
|
6182
|
+
const output = execFileSync("pdftotext", args, {
|
|
4562
6183
|
encoding: "utf-8",
|
|
4563
6184
|
timeout: 3e4,
|
|
4564
6185
|
maxBuffer: 5 * 1024 * 1024
|
|
@@ -4582,18 +6203,23 @@ function parsePageRange(range) {
|
|
|
4582
6203
|
}
|
|
4583
6204
|
|
|
4584
6205
|
// src/parsers/docx.ts
|
|
4585
|
-
import { readFileSync as
|
|
6206
|
+
import { readFileSync as readFileSync13 } from "fs";
|
|
4586
6207
|
async function parseDocx(filePath) {
|
|
4587
6208
|
const mammoth = await import("mammoth");
|
|
4588
|
-
const buffer =
|
|
6209
|
+
const buffer = readFileSync13(filePath);
|
|
4589
6210
|
const result = await mammoth.extractRawText({ buffer });
|
|
4590
6211
|
return result.value.trim() || "(no text content in document)";
|
|
4591
6212
|
}
|
|
4592
6213
|
|
|
4593
6214
|
// src/parsers/notebook.ts
|
|
4594
|
-
import { readFileSync as
|
|
6215
|
+
import { readFileSync as readFileSync14 } from "fs";
|
|
6216
|
+
function joinSource(s) {
|
|
6217
|
+
if (Array.isArray(s)) return s.join("");
|
|
6218
|
+
if (typeof s === "string") return s;
|
|
6219
|
+
return "";
|
|
6220
|
+
}
|
|
4595
6221
|
function parseNotebook(filePath) {
|
|
4596
|
-
const raw =
|
|
6222
|
+
const raw = readFileSync14(filePath, "utf-8");
|
|
4597
6223
|
const notebook = JSON.parse(raw);
|
|
4598
6224
|
if (!notebook.cells || notebook.cells.length === 0) {
|
|
4599
6225
|
return "(empty notebook)";
|
|
@@ -4601,7 +6227,7 @@ function parseNotebook(filePath) {
|
|
|
4601
6227
|
const parts = [];
|
|
4602
6228
|
for (let i = 0; i < notebook.cells.length; i++) {
|
|
4603
6229
|
const cell = notebook.cells[i];
|
|
4604
|
-
const source = cell.source
|
|
6230
|
+
const source = joinSource(cell.source);
|
|
4605
6231
|
if (cell.cell_type === "markdown") {
|
|
4606
6232
|
parts.push(`[cell ${i + 1} markdown]
|
|
4607
6233
|
${source}`);
|
|
@@ -4612,13 +6238,13 @@ ${source}`);
|
|
|
4612
6238
|
for (const out of cell.outputs) {
|
|
4613
6239
|
if (out.text) {
|
|
4614
6240
|
parts.push(`[output]
|
|
4615
|
-
${out.text
|
|
6241
|
+
${joinSource(out.text)}`);
|
|
4616
6242
|
}
|
|
4617
6243
|
if (out.data) {
|
|
4618
6244
|
for (const [mime, content] of Object.entries(out.data)) {
|
|
4619
6245
|
if (mime.startsWith("text/")) {
|
|
4620
6246
|
parts.push(`[output ${mime}]
|
|
4621
|
-
${content
|
|
6247
|
+
${joinSource(content)}`);
|
|
4622
6248
|
} else {
|
|
4623
6249
|
parts.push(`[output ${mime}] (binary content)`);
|
|
4624
6250
|
}
|
|
@@ -4635,8 +6261,8 @@ ${source}`);
|
|
|
4635
6261
|
}
|
|
4636
6262
|
|
|
4637
6263
|
// src/parsers/image.ts
|
|
4638
|
-
import { readFileSync as
|
|
4639
|
-
import { extname as
|
|
6264
|
+
import { readFileSync as readFileSync15 } from "fs";
|
|
6265
|
+
import { extname as extname3 } from "path";
|
|
4640
6266
|
var MIME_TYPES = {
|
|
4641
6267
|
".png": "image/png",
|
|
4642
6268
|
".jpg": "image/jpeg",
|
|
@@ -4647,9 +6273,9 @@ var MIME_TYPES = {
|
|
|
4647
6273
|
".bmp": "image/bmp"
|
|
4648
6274
|
};
|
|
4649
6275
|
function parseImage(filePath) {
|
|
4650
|
-
const ext =
|
|
6276
|
+
const ext = extname3(filePath).toLowerCase();
|
|
4651
6277
|
const mediaType = MIME_TYPES[ext] || "image/png";
|
|
4652
|
-
const buffer =
|
|
6278
|
+
const buffer = readFileSync15(filePath);
|
|
4653
6279
|
const base64 = buffer.toString("base64");
|
|
4654
6280
|
const sizeKB = Math.round(buffer.length / 1024);
|
|
4655
6281
|
return {
|
|
@@ -4659,7 +6285,7 @@ function parseImage(filePath) {
|
|
|
4659
6285
|
};
|
|
4660
6286
|
}
|
|
4661
6287
|
function isImageFile(filePath) {
|
|
4662
|
-
const ext =
|
|
6288
|
+
const ext = extname3(filePath).toLowerCase();
|
|
4663
6289
|
return ext in MIME_TYPES;
|
|
4664
6290
|
}
|
|
4665
6291
|
|
|
@@ -4676,16 +6302,16 @@ var ReadTool = buildTool({
|
|
|
4676
6302
|
description: "Read a file from the filesystem. Supports text, PDF, Word (.docx), Jupyter notebooks (.ipynb), and images.",
|
|
4677
6303
|
inputSchema: inputSchema3,
|
|
4678
6304
|
async call(input, context) {
|
|
4679
|
-
const filePath =
|
|
6305
|
+
const filePath = isAbsolute2(input.file_path) ? input.file_path : resolve2(context.cwd, input.file_path);
|
|
4680
6306
|
try {
|
|
4681
|
-
const stat =
|
|
6307
|
+
const stat = statSync4(filePath);
|
|
4682
6308
|
if (stat.isDirectory()) {
|
|
4683
6309
|
return { content: `error: "${filePath}" is a directory, not a file. use Bash with ls to list directory contents.`, isError: true };
|
|
4684
6310
|
}
|
|
4685
6311
|
} catch {
|
|
4686
6312
|
return { content: `error: file not found: ${filePath}`, isError: true };
|
|
4687
6313
|
}
|
|
4688
|
-
const ext =
|
|
6314
|
+
const ext = extname4(filePath).toLowerCase();
|
|
4689
6315
|
try {
|
|
4690
6316
|
switch (ext) {
|
|
4691
6317
|
case ".pdf":
|
|
@@ -4710,10 +6336,16 @@ var ReadTool = buildTool({
|
|
|
4710
6336
|
},
|
|
4711
6337
|
isConcurrencySafe: () => true,
|
|
4712
6338
|
isReadOnly: () => true,
|
|
4713
|
-
|
|
6339
|
+
// reads inside the project auto-allow; reads outside it (home dotfiles,
|
|
6340
|
+
// ~/.ssh, /etc) and in-project secret files prompt, so an injected agent
|
|
6341
|
+
// can't silently pull credentials into context.
|
|
6342
|
+
checkPermissions: (input, context) => {
|
|
6343
|
+
const c = classifyRead(input.file_path, context.cwd);
|
|
6344
|
+
return c.allow ? { behavior: "allow" } : { behavior: "ask", message: readAskMessage(input.file_path, c.reason) };
|
|
6345
|
+
}
|
|
4714
6346
|
});
|
|
4715
6347
|
function readTextFile(filePath, offset, limit) {
|
|
4716
|
-
const content =
|
|
6348
|
+
const content = readFileSync16(filePath, "utf-8");
|
|
4717
6349
|
const allLines = content.split("\n");
|
|
4718
6350
|
const start = (offset ?? 1) - 1;
|
|
4719
6351
|
const count = limit ?? MAX_LINES;
|
|
@@ -4730,8 +6362,8 @@ function readTextFile(filePath, offset, limit) {
|
|
|
4730
6362
|
|
|
4731
6363
|
// src/tools/edit.ts
|
|
4732
6364
|
import { z as z6 } from "zod";
|
|
4733
|
-
import {
|
|
4734
|
-
import { resolve as
|
|
6365
|
+
import { readFile, writeFile } from "fs/promises";
|
|
6366
|
+
import { resolve as resolve3, isAbsolute as isAbsolute3 } from "path";
|
|
4735
6367
|
var inputSchema4 = z6.object({
|
|
4736
6368
|
file_path: z6.string().describe("absolute path to the file to edit"),
|
|
4737
6369
|
old_string: z6.string().describe("the exact text to find and replace"),
|
|
@@ -4743,10 +6375,10 @@ var EditTool = buildTool({
|
|
|
4743
6375
|
description: "Replace exact string matches in a file. old_string must match exactly including whitespace.",
|
|
4744
6376
|
inputSchema: inputSchema4,
|
|
4745
6377
|
async call(input, context) {
|
|
4746
|
-
const filePath =
|
|
6378
|
+
const filePath = isAbsolute3(input.file_path) ? input.file_path : resolve3(context.cwd, input.file_path);
|
|
4747
6379
|
let content;
|
|
4748
6380
|
try {
|
|
4749
|
-
content =
|
|
6381
|
+
content = await readFile(filePath, "utf-8");
|
|
4750
6382
|
} catch {
|
|
4751
6383
|
return { content: `error: file not found: ${filePath}`, isError: true };
|
|
4752
6384
|
}
|
|
@@ -4770,7 +6402,7 @@ var EditTool = buildTool({
|
|
|
4770
6402
|
}
|
|
4771
6403
|
const updated = input.replace_all ? content.split(input.old_string).join(input.new_string) : content.replace(input.old_string, input.new_string);
|
|
4772
6404
|
try {
|
|
4773
|
-
|
|
6405
|
+
await writeFile(filePath, updated, "utf-8");
|
|
4774
6406
|
const replacements = input.replace_all ? content.split(input.old_string).length - 1 : 1;
|
|
4775
6407
|
return { content: `edited ${filePath} (${replacements} replacement${replacements > 1 ? "s" : ""})` };
|
|
4776
6408
|
} catch (error) {
|
|
@@ -4787,8 +6419,8 @@ var EditTool = buildTool({
|
|
|
4787
6419
|
|
|
4788
6420
|
// src/tools/write.ts
|
|
4789
6421
|
import { z as z7 } from "zod";
|
|
4790
|
-
import { writeFileSync as writeFileSync7, mkdirSync as
|
|
4791
|
-
import { resolve as
|
|
6422
|
+
import { writeFileSync as writeFileSync7, mkdirSync as mkdirSync7, existsSync as existsSync13 } from "fs";
|
|
6423
|
+
import { resolve as resolve4, isAbsolute as isAbsolute4, dirname as dirname2 } from "path";
|
|
4792
6424
|
var inputSchema5 = z7.object({
|
|
4793
6425
|
file_path: z7.string().describe("absolute path to the file to write"),
|
|
4794
6426
|
content: z7.string().describe("the content to write to the file")
|
|
@@ -4798,11 +6430,11 @@ var WriteTool = buildTool({
|
|
|
4798
6430
|
description: "Write content to a file. Creates the file if it does not exist. Overwrites if it does.",
|
|
4799
6431
|
inputSchema: inputSchema5,
|
|
4800
6432
|
async call(input, context) {
|
|
4801
|
-
const filePath =
|
|
6433
|
+
const filePath = isAbsolute4(input.file_path) ? input.file_path : resolve4(context.cwd, input.file_path);
|
|
4802
6434
|
try {
|
|
4803
|
-
const dir =
|
|
4804
|
-
if (!
|
|
4805
|
-
|
|
6435
|
+
const dir = dirname2(filePath);
|
|
6436
|
+
if (!existsSync13(dir)) {
|
|
6437
|
+
mkdirSync7(dir, { recursive: true });
|
|
4806
6438
|
}
|
|
4807
6439
|
writeFileSync7(filePath, input.content, "utf-8");
|
|
4808
6440
|
const lineCount = input.content.split("\n").length;
|
|
@@ -4821,8 +6453,8 @@ var WriteTool = buildTool({
|
|
|
4821
6453
|
|
|
4822
6454
|
// src/tools/glob.ts
|
|
4823
6455
|
import { z as z8 } from "zod";
|
|
4824
|
-
import { execFileSync as
|
|
4825
|
-
import { resolve as
|
|
6456
|
+
import { execFileSync as execFileSync2 } from "child_process";
|
|
6457
|
+
import { resolve as resolve5, isAbsolute as isAbsolute5 } from "path";
|
|
4826
6458
|
var inputSchema6 = z8.object({
|
|
4827
6459
|
pattern: z8.string().describe('glob pattern to match files (e.g. "**/*.ts", "src/**/*.py")'),
|
|
4828
6460
|
path: z8.string().optional().describe("directory to search in (default: cwd)")
|
|
@@ -4832,9 +6464,9 @@ var GlobTool = buildTool({
|
|
|
4832
6464
|
description: "Find files matching a glob pattern. Returns file paths sorted by modification time.",
|
|
4833
6465
|
inputSchema: inputSchema6,
|
|
4834
6466
|
async call(input, context) {
|
|
4835
|
-
const searchPath = input.path ?
|
|
6467
|
+
const searchPath = input.path ? isAbsolute5(input.path) ? input.path : resolve5(context.cwd, input.path) : context.cwd;
|
|
4836
6468
|
try {
|
|
4837
|
-
const
|
|
6469
|
+
const matchArgs = input.pattern.includes("/") ? ["-path", "*" + input.pattern.replace(/\*\*\//g, "").replace(/\*\*/g, "*")] : ["-name", input.pattern.replace(/\*\*\//g, "")];
|
|
4838
6470
|
const excludeDirs = [
|
|
4839
6471
|
"node_modules",
|
|
4840
6472
|
".git",
|
|
@@ -4855,9 +6487,9 @@ var GlobTool = buildTool({
|
|
|
4855
6487
|
for (const d of excludeDirs) {
|
|
4856
6488
|
excludeArgs.push("-not", "-path", `*/${d}/*`);
|
|
4857
6489
|
}
|
|
4858
|
-
const output =
|
|
6490
|
+
const output = execFileSync2(
|
|
4859
6491
|
"find",
|
|
4860
|
-
[searchPath, "-type", "f",
|
|
6492
|
+
[searchPath, "-type", "f", ...matchArgs, ...excludeArgs],
|
|
4861
6493
|
{
|
|
4862
6494
|
cwd: searchPath,
|
|
4863
6495
|
encoding: "utf-8",
|
|
@@ -4886,13 +6518,18 @@ var GlobTool = buildTool({
|
|
|
4886
6518
|
},
|
|
4887
6519
|
isConcurrencySafe: () => true,
|
|
4888
6520
|
isReadOnly: () => true,
|
|
4889
|
-
|
|
6521
|
+
// enumerating a directory outside the project prompts: listing ~/.ssh or
|
|
6522
|
+
// ~/.aws leaks the location of credentials even without reading them.
|
|
6523
|
+
checkPermissions: (input, context) => {
|
|
6524
|
+
const c = classifyRead(input.path ?? context.cwd, context.cwd);
|
|
6525
|
+
return c.allow ? { behavior: "allow" } : { behavior: "ask", message: readAskMessage(input.path ?? context.cwd, c.reason) };
|
|
6526
|
+
}
|
|
4890
6527
|
});
|
|
4891
6528
|
|
|
4892
6529
|
// src/tools/grep.ts
|
|
4893
6530
|
import { z as z9 } from "zod";
|
|
4894
|
-
import { execFileSync as
|
|
4895
|
-
import { resolve as
|
|
6531
|
+
import { execFileSync as execFileSync3 } from "child_process";
|
|
6532
|
+
import { resolve as resolve6, isAbsolute as isAbsolute6 } from "path";
|
|
4896
6533
|
var inputSchema7 = z9.object({
|
|
4897
6534
|
pattern: z9.string().describe("regex pattern to search for"),
|
|
4898
6535
|
path: z9.string().optional().describe("file or directory to search in (default: cwd)"),
|
|
@@ -4904,7 +6541,7 @@ var useRipgrep = null;
|
|
|
4904
6541
|
function hasRipgrep() {
|
|
4905
6542
|
if (useRipgrep !== null) return useRipgrep;
|
|
4906
6543
|
try {
|
|
4907
|
-
|
|
6544
|
+
execFileSync3("which", ["rg"], { stdio: "pipe" });
|
|
4908
6545
|
useRipgrep = true;
|
|
4909
6546
|
} catch {
|
|
4910
6547
|
useRipgrep = false;
|
|
@@ -4916,13 +6553,13 @@ var GrepTool = buildTool({
|
|
|
4916
6553
|
description: "Search file contents for a regex pattern. Uses ripgrep if available.",
|
|
4917
6554
|
inputSchema: inputSchema7,
|
|
4918
6555
|
async call(input, context) {
|
|
4919
|
-
const searchPath = input.path ?
|
|
6556
|
+
const searchPath = input.path ? isAbsolute6(input.path) ? input.path : resolve6(context.cwd, input.path) : context.cwd;
|
|
4920
6557
|
const mode = input.output_mode ?? "files_with_matches";
|
|
4921
6558
|
try {
|
|
4922
6559
|
const useRg = hasRipgrep();
|
|
4923
6560
|
const bin = useRg ? "rg" : "grep";
|
|
4924
6561
|
const args = useRg ? buildRgArgs(input.pattern, searchPath, mode, input.glob, input.context) : buildGrepArgs(input.pattern, searchPath, mode, input.glob, input.context);
|
|
4925
|
-
const output =
|
|
6562
|
+
const output = execFileSync3(bin, args, {
|
|
4926
6563
|
cwd: context.cwd,
|
|
4927
6564
|
encoding: "utf-8",
|
|
4928
6565
|
timeout: 3e4,
|
|
@@ -4953,7 +6590,12 @@ var GrepTool = buildTool({
|
|
|
4953
6590
|
},
|
|
4954
6591
|
isConcurrencySafe: () => true,
|
|
4955
6592
|
isReadOnly: () => true,
|
|
4956
|
-
|
|
6593
|
+
// searching outside the project (or a secret file) prompts: grep content mode
|
|
6594
|
+
// returns file contents, so an unbounded search is a secret-read channel.
|
|
6595
|
+
checkPermissions: (input, context) => {
|
|
6596
|
+
const c = classifyRead(input.path ?? context.cwd, context.cwd);
|
|
6597
|
+
return c.allow ? { behavior: "allow" } : { behavior: "ask", message: readAskMessage(input.path ?? context.cwd, c.reason) };
|
|
6598
|
+
}
|
|
4957
6599
|
});
|
|
4958
6600
|
function buildRgArgs(pattern, path, mode, glob, ctx) {
|
|
4959
6601
|
const args = [];
|
|
@@ -5002,6 +6644,7 @@ import TurndownService from "turndown";
|
|
|
5002
6644
|
// src/net/safeFetch.ts
|
|
5003
6645
|
import got from "got";
|
|
5004
6646
|
import { lookup as dnsLookup } from "dns";
|
|
6647
|
+
import { isIP } from "net";
|
|
5005
6648
|
|
|
5006
6649
|
// src/net/validate.ts
|
|
5007
6650
|
import ipaddr from "ipaddr.js";
|
|
@@ -5132,9 +6775,17 @@ var strictPolicy = {
|
|
|
5132
6775
|
|
|
5133
6776
|
// src/net/safeFetch.ts
|
|
5134
6777
|
async function safeFetch(rawUrl, policy) {
|
|
5135
|
-
const
|
|
5136
|
-
|
|
5137
|
-
|
|
6778
|
+
const stripBrackets = (h) => h.startsWith("[") && h.endsWith("]") ? h.slice(1, -1) : h;
|
|
6779
|
+
const assertUrlAllowed = (urlStr) => {
|
|
6780
|
+
const u = new URL(urlStr);
|
|
6781
|
+
validateScheme(urlStr, u.protocol, policy.allowedSchemes);
|
|
6782
|
+
validatePort(urlStr, u.port, policy.allowedPorts);
|
|
6783
|
+
const host = stripBrackets(u.hostname);
|
|
6784
|
+
if (isIP(host) !== 0) {
|
|
6785
|
+
validateIp(urlStr, host, policy.blockedIpRanges);
|
|
6786
|
+
}
|
|
6787
|
+
};
|
|
6788
|
+
assertUrlAllowed(rawUrl);
|
|
5138
6789
|
let lastResolvedIp = "";
|
|
5139
6790
|
const pinningLookup = (hostname, options, cb) => {
|
|
5140
6791
|
const optsArg = typeof options === "function" ? {} : options || {};
|
|
@@ -5167,7 +6818,18 @@ async function safeFetch(rawUrl, policy) {
|
|
|
5167
6818
|
followRedirect: policy.maxRedirects > 0,
|
|
5168
6819
|
dnsLookup: pinningLookup,
|
|
5169
6820
|
headers: { "user-agent": policy.userAgent },
|
|
5170
|
-
throwHttpErrors: true
|
|
6821
|
+
throwHttpErrors: true,
|
|
6822
|
+
hooks: {
|
|
6823
|
+
// re-validate scheme, port, and any literal-IP target on every redirect
|
|
6824
|
+
// hop. without this, a 302 to http://127.0.0.1:9/ or a forbidden port
|
|
6825
|
+
// would be followed (got's internal redirect doesn't re-run these checks,
|
|
6826
|
+
// and node skips the pinning lookup for IP literals).
|
|
6827
|
+
beforeRedirect: [
|
|
6828
|
+
(options) => {
|
|
6829
|
+
assertUrlAllowed(options.url.toString());
|
|
6830
|
+
}
|
|
6831
|
+
]
|
|
6832
|
+
}
|
|
5171
6833
|
});
|
|
5172
6834
|
} catch (err) {
|
|
5173
6835
|
if (err instanceof ForbiddenIpError || err instanceof ForbiddenSchemeError || err instanceof ForbiddenPortError) {
|
|
@@ -5209,20 +6871,20 @@ var turndown = new TurndownService({
|
|
|
5209
6871
|
headingStyle: "atx",
|
|
5210
6872
|
codeBlockStyle: "fenced"
|
|
5211
6873
|
});
|
|
5212
|
-
var
|
|
6874
|
+
var MAX_OUTPUT = 4e4;
|
|
5213
6875
|
function extractMarkdown(html) {
|
|
5214
6876
|
const $ = cheerio.load(html);
|
|
5215
6877
|
$("script, style, noscript, iframe").remove();
|
|
5216
6878
|
const $container = $("article").first().length ? $("article").first() : $("main").first().length ? $("main").first() : $("body");
|
|
5217
6879
|
$container.find("nav, footer, aside, [role=navigation], [role=banner], [role=contentinfo]").remove();
|
|
5218
6880
|
let md = turndown.turndown($container.html() || "");
|
|
5219
|
-
if (md.length >
|
|
5220
|
-
md = md.slice(0,
|
|
6881
|
+
if (md.length > MAX_OUTPUT) {
|
|
6882
|
+
md = md.slice(0, MAX_OUTPUT) + "\n\n[... content truncated ...]";
|
|
5221
6883
|
}
|
|
5222
6884
|
return md;
|
|
5223
6885
|
}
|
|
5224
6886
|
function truncate(s) {
|
|
5225
|
-
return s.length >
|
|
6887
|
+
return s.length > MAX_OUTPUT ? s.slice(0, MAX_OUTPUT) + "\n\n[... content truncated ...]" : s;
|
|
5226
6888
|
}
|
|
5227
6889
|
var WebFetchTool = buildTool({
|
|
5228
6890
|
name: "WebFetch",
|
|
@@ -5329,8 +6991,68 @@ var WebSearchTool = buildTool({
|
|
|
5329
6991
|
checkPermissions: () => ({ behavior: "allow" })
|
|
5330
6992
|
});
|
|
5331
6993
|
|
|
6994
|
+
// src/tools/verify.ts
|
|
6995
|
+
import { z as z12 } from "zod";
|
|
6996
|
+
import { exec as exec3 } from "child_process";
|
|
6997
|
+
import { promisify as promisify2 } from "util";
|
|
6998
|
+
var execAsync2 = promisify2(exec3);
|
|
6999
|
+
var inputSchema9 = z12.object({
|
|
7000
|
+
command: z12.string().describe("the verification command to run (e.g. `npx vitest run`, `pytest`, `cargo test`)"),
|
|
7001
|
+
description: z12.string().optional().describe("one-line description of what is being verified"),
|
|
7002
|
+
timeout: z12.number().optional().describe("timeout in milliseconds (default 60s, max 600s)")
|
|
7003
|
+
});
|
|
7004
|
+
var VerifyTool = buildTool({
|
|
7005
|
+
name: "Verify",
|
|
7006
|
+
description: "Run a project verification command (tests, typecheck, lint) and return its output. Derive the command from project scan and repo map. Use before claiming done after a non-trivial edit.",
|
|
7007
|
+
inputSchema: inputSchema9,
|
|
7008
|
+
async call(input, context) {
|
|
7009
|
+
const maxOutput = loadConfig().tuning.bash_max_output_bytes;
|
|
7010
|
+
const timeout = Math.min(input.timeout || 6e4, 6e5);
|
|
7011
|
+
try {
|
|
7012
|
+
const { stdout } = await execAsync2(input.command, {
|
|
7013
|
+
cwd: context.cwd,
|
|
7014
|
+
timeout,
|
|
7015
|
+
maxBuffer: maxOutput,
|
|
7016
|
+
env: { ...process.env }
|
|
7017
|
+
});
|
|
7018
|
+
const result = stdout.trim();
|
|
7019
|
+
if (result.length > maxOutput) {
|
|
7020
|
+
return { content: result.slice(0, maxOutput) + "\n\n[output truncated]" };
|
|
7021
|
+
}
|
|
7022
|
+
return { content: `verified: ${input.command}
|
|
7023
|
+
|
|
7024
|
+
${result || "(no output)"}` };
|
|
7025
|
+
} catch (error) {
|
|
7026
|
+
const execError = error;
|
|
7027
|
+
const stdout = execError.stdout?.toString().trim() || "";
|
|
7028
|
+
const stderr = execError.stderr?.toString().trim() || "";
|
|
7029
|
+
const exitCode = execError.code ?? 1;
|
|
7030
|
+
const parts = [
|
|
7031
|
+
`verification failed: ${input.command}`,
|
|
7032
|
+
"",
|
|
7033
|
+
stdout && stdout,
|
|
7034
|
+
stderr && stderr,
|
|
7035
|
+
`exit code: ${exitCode}`
|
|
7036
|
+
].filter(Boolean);
|
|
7037
|
+
return { content: parts.join("\n"), isError: true };
|
|
7038
|
+
}
|
|
7039
|
+
},
|
|
7040
|
+
// verification commands are observation, not mutation of the working tree.
|
|
7041
|
+
// they may have incidental side effects (cache writes, coverage reports)
|
|
7042
|
+
// but do not change source files the operator is authoring.
|
|
7043
|
+
isReadOnly() {
|
|
7044
|
+
return true;
|
|
7045
|
+
},
|
|
7046
|
+
isConcurrencySafe() {
|
|
7047
|
+
return false;
|
|
7048
|
+
},
|
|
7049
|
+
checkPermissions(input) {
|
|
7050
|
+
return { behavior: "ask", message: `verify: ${input.command}` };
|
|
7051
|
+
}
|
|
7052
|
+
});
|
|
7053
|
+
|
|
5332
7054
|
// src/cli.ts
|
|
5333
|
-
import { homedir as
|
|
7055
|
+
import { homedir as homedir13 } from "os";
|
|
5334
7056
|
|
|
5335
7057
|
// src/completion/bash.ts
|
|
5336
7058
|
function emitBash() {
|
|
@@ -5435,14 +7157,14 @@ compdef _prism prism
|
|
|
5435
7157
|
}
|
|
5436
7158
|
|
|
5437
7159
|
// src/completion/install.ts
|
|
5438
|
-
import { existsSync as
|
|
5439
|
-
import { join as
|
|
5440
|
-
import { homedir as
|
|
5441
|
-
import { basename as
|
|
5442
|
-
var FIRST_RUN_FLAG =
|
|
7160
|
+
import { existsSync as existsSync14, readFileSync as readFileSync17, appendFileSync, writeFileSync as writeFileSync8, mkdirSync as mkdirSync8 } from "fs";
|
|
7161
|
+
import { join as join14 } from "path";
|
|
7162
|
+
import { homedir as homedir12, platform } from "os";
|
|
7163
|
+
import { basename as basename7 } from "path";
|
|
7164
|
+
var FIRST_RUN_FLAG = join14(homedir12(), ".prism", ".completion-installed");
|
|
5443
7165
|
function detectShell() {
|
|
5444
7166
|
const sh = process.env.SHELL || "";
|
|
5445
|
-
const name =
|
|
7167
|
+
const name = basename7(sh);
|
|
5446
7168
|
if (name === "zsh") return "zsh";
|
|
5447
7169
|
if (name === "bash") return "bash";
|
|
5448
7170
|
return null;
|
|
@@ -5450,13 +7172,13 @@ function detectShell() {
|
|
|
5450
7172
|
function rcPathFor(shell) {
|
|
5451
7173
|
if (shell === "zsh") {
|
|
5452
7174
|
const zdotdir = process.env.ZDOTDIR;
|
|
5453
|
-
return
|
|
7175
|
+
return join14(zdotdir || homedir12(), ".zshrc");
|
|
5454
7176
|
}
|
|
5455
7177
|
if (platform() === "darwin") {
|
|
5456
|
-
const profile =
|
|
5457
|
-
if (
|
|
7178
|
+
const profile = join14(homedir12(), ".bash_profile");
|
|
7179
|
+
if (existsSync14(profile)) return profile;
|
|
5458
7180
|
}
|
|
5459
|
-
return
|
|
7181
|
+
return join14(homedir12(), ".bashrc");
|
|
5460
7182
|
}
|
|
5461
7183
|
var MARKER = "# prism shell completion";
|
|
5462
7184
|
function evalLineFor(shell) {
|
|
@@ -5469,8 +7191,8 @@ function installCompletion(requested) {
|
|
|
5469
7191
|
}
|
|
5470
7192
|
const rcPath = rcPathFor(shell);
|
|
5471
7193
|
const evalLine = evalLineFor(shell);
|
|
5472
|
-
if (
|
|
5473
|
-
const contents =
|
|
7194
|
+
if (existsSync14(rcPath)) {
|
|
7195
|
+
const contents = readFileSync17(rcPath, "utf-8");
|
|
5474
7196
|
if (contents.includes(evalLine)) {
|
|
5475
7197
|
return { shell, rcPath, status: "already-installed" };
|
|
5476
7198
|
}
|
|
@@ -5484,7 +7206,7 @@ ${evalLine}
|
|
|
5484
7206
|
}
|
|
5485
7207
|
function maybeAutoInstall() {
|
|
5486
7208
|
if (process.env.PRISM_NO_AUTO_COMPLETION) return null;
|
|
5487
|
-
if (
|
|
7209
|
+
if (existsSync14(FIRST_RUN_FLAG)) return null;
|
|
5488
7210
|
const shell = detectShell();
|
|
5489
7211
|
if (!shell) {
|
|
5490
7212
|
markFirstRunDone();
|
|
@@ -5500,24 +7222,26 @@ function maybeAutoInstall() {
|
|
|
5500
7222
|
}
|
|
5501
7223
|
function markFirstRunDone() {
|
|
5502
7224
|
try {
|
|
5503
|
-
const dir =
|
|
5504
|
-
if (!
|
|
7225
|
+
const dir = join14(homedir12(), ".prism");
|
|
7226
|
+
if (!existsSync14(dir)) mkdirSync8(dir, { recursive: true });
|
|
5505
7227
|
writeFileSync8(FIRST_RUN_FLAG, (/* @__PURE__ */ new Date()).toISOString(), "utf-8");
|
|
5506
7228
|
} catch {
|
|
5507
7229
|
}
|
|
5508
7230
|
}
|
|
5509
7231
|
|
|
5510
7232
|
// src/memory/lens.ts
|
|
5511
|
-
import { existsSync as
|
|
5512
|
-
import { join as
|
|
5513
|
-
var MAX_LENS_BYTES = 64 * 1024;
|
|
7233
|
+
import { existsSync as existsSync15, readFileSync as readFileSync18 } from "fs";
|
|
7234
|
+
import { join as join15 } from "path";
|
|
5514
7235
|
function loadLens(cwd) {
|
|
5515
|
-
const path =
|
|
5516
|
-
if (!
|
|
7236
|
+
const path = join15(cwd, "lens.md");
|
|
7237
|
+
if (!existsSync15(path)) return null;
|
|
5517
7238
|
try {
|
|
5518
|
-
const content =
|
|
5519
|
-
|
|
5520
|
-
|
|
7239
|
+
const content = readFileSync18(path, "utf-8");
|
|
7240
|
+
const maxBytes = loadConfig().tuning.lens_max_bytes;
|
|
7241
|
+
if (content.length > maxBytes) {
|
|
7242
|
+
return content.slice(0, maxBytes) + `
|
|
7243
|
+
|
|
7244
|
+
[truncated: lens.md exceeds ${maxBytes} bytes]`;
|
|
5521
7245
|
}
|
|
5522
7246
|
return content.trim() || null;
|
|
5523
7247
|
} catch {
|
|
@@ -5527,7 +7251,7 @@ function loadLens(cwd) {
|
|
|
5527
7251
|
|
|
5528
7252
|
// src/cli.ts
|
|
5529
7253
|
function shortenPath2(cwd) {
|
|
5530
|
-
const home =
|
|
7254
|
+
const home = homedir13();
|
|
5531
7255
|
let path = cwd.startsWith(home) ? "~" + cwd.slice(home.length) : cwd;
|
|
5532
7256
|
if (path.length > 50) {
|
|
5533
7257
|
const parts = path.split("/").filter(Boolean);
|
|
@@ -5582,6 +7306,10 @@ async function main() {
|
|
|
5582
7306
|
process.exit(0);
|
|
5583
7307
|
}
|
|
5584
7308
|
initConfig();
|
|
7309
|
+
const migrated = migrateConfig();
|
|
7310
|
+
if (migrated.length > 0) {
|
|
7311
|
+
console.log(`\x1B[2mconfig migrated: added [${migrated.join("], [")}] to ${getConfigPath()}\x1B[0m`);
|
|
7312
|
+
}
|
|
5585
7313
|
const config = loadConfig();
|
|
5586
7314
|
const KNOWN_FLAGS = /* @__PURE__ */ new Set([...allFlagTokens(), "--completion", "--complete", "--install-completion"]);
|
|
5587
7315
|
if (args.includes("--help") || args.includes("-h")) {
|
|
@@ -5600,6 +7328,11 @@ async function main() {
|
|
|
5600
7328
|
-c, --continue resume last session in this directory
|
|
5601
7329
|
-r, --resume <n|id> resume the nth recent session, or by full id (see --sessions)
|
|
5602
7330
|
--max-tokens <n> max output tokens per response (default: 10000)
|
|
7331
|
+
--max-files <n> repo-map walk cap (override [tuning].repomap_max_files)
|
|
7332
|
+
--max-lines <n> repo-map render cap (override [tuning].repomap_max_lines)
|
|
7333
|
+
--no-repomap skip the repo-map retrieval pass entirely
|
|
7334
|
+
--no-scan skip the live project scan
|
|
7335
|
+
--no-memory skip lens.md + persistent memo
|
|
5603
7336
|
--config show config file path
|
|
5604
7337
|
--sessions list recent sessions
|
|
5605
7338
|
-h, --help show this help`);
|
|
@@ -5620,6 +7353,24 @@ async function main() {
|
|
|
5620
7353
|
}
|
|
5621
7354
|
maxTokens = parsed;
|
|
5622
7355
|
}
|
|
7356
|
+
const parseFlagNumber = (flag, label) => {
|
|
7357
|
+
const idx = args.indexOf(flag);
|
|
7358
|
+
if (idx === -1) return void 0;
|
|
7359
|
+
const raw = args[idx + 1];
|
|
7360
|
+
if (!raw || raw.startsWith("-")) {
|
|
7361
|
+
console.error(`\x1B[31m${flag} requires a number (e.g. ${flag} ${label})\x1B[0m`);
|
|
7362
|
+
process.exit(1);
|
|
7363
|
+
}
|
|
7364
|
+
const parsed = parseInt(raw, 10);
|
|
7365
|
+
if (isNaN(parsed) || parsed <= 0) {
|
|
7366
|
+
console.error(`\x1B[31m${flag} must be a positive number, got: ${raw}\x1B[0m`);
|
|
7367
|
+
process.exit(1);
|
|
7368
|
+
}
|
|
7369
|
+
return parsed;
|
|
7370
|
+
};
|
|
7371
|
+
const maxFiles = parseFlagNumber("--max-files", "500");
|
|
7372
|
+
const maxLines = parseFlagNumber("--max-lines", "200");
|
|
7373
|
+
const skipRepoMap = args.includes("--no-repomap");
|
|
5623
7374
|
const valueFlags = valueTakingFlagTokens();
|
|
5624
7375
|
const isFlagValue = (i) => i > 0 && valueFlags.has(args[i - 1] || "");
|
|
5625
7376
|
const unknownFlags = args.filter((a, i) => a.startsWith("-") && !KNOWN_FLAGS.has(a) && !isFlagValue(i));
|
|
@@ -5737,7 +7488,7 @@ async function main() {
|
|
|
5737
7488
|
session = createSession(model, provider.name, cwd);
|
|
5738
7489
|
}
|
|
5739
7490
|
const capabilities = provider.getCapabilities();
|
|
5740
|
-
const tools = [BashTool, ReadTool, EditTool, WriteTool, GlobTool, GrepTool, WebFetchTool, WebSearchTool];
|
|
7491
|
+
const tools = [BashTool, ReadTool, EditTool, WriteTool, GlobTool, GrepTool, WebFetchTool, WebSearchTool, VerifyTool];
|
|
5741
7492
|
const skipScan = args.includes("--no-scan");
|
|
5742
7493
|
const skipMemory = args.includes("--no-memory");
|
|
5743
7494
|
let projectContext;
|
|
@@ -5769,6 +7520,21 @@ async function main() {
|
|
|
5769
7520
|
if (autoInstall) {
|
|
5770
7521
|
console.log(`\x1B[2mshell completion installed to ${autoInstall.rcPath}. restart your shell or run \`exec ${autoInstall.shell}\` to enable tab completion.\x1B[0m`);
|
|
5771
7522
|
}
|
|
7523
|
+
if (process.stdout.isTTY) {
|
|
7524
|
+
process.stdout.write("\x1B[>1u");
|
|
7525
|
+
const popKeyboardMode = () => {
|
|
7526
|
+
if (process.stdout.isTTY) process.stdout.write("\x1B[<u");
|
|
7527
|
+
};
|
|
7528
|
+
process.on("exit", popKeyboardMode);
|
|
7529
|
+
process.on("SIGINT", () => {
|
|
7530
|
+
popKeyboardMode();
|
|
7531
|
+
process.exit(130);
|
|
7532
|
+
});
|
|
7533
|
+
process.on("SIGTERM", () => {
|
|
7534
|
+
popKeyboardMode();
|
|
7535
|
+
process.exit(143);
|
|
7536
|
+
});
|
|
7537
|
+
}
|
|
5772
7538
|
const { waitUntilExit } = render(
|
|
5773
7539
|
React4.createElement(App, {
|
|
5774
7540
|
provider,
|
|
@@ -5778,7 +7544,8 @@ async function main() {
|
|
|
5778
7544
|
session,
|
|
5779
7545
|
initialMessages,
|
|
5780
7546
|
projectContext,
|
|
5781
|
-
memory
|
|
7547
|
+
memory,
|
|
7548
|
+
repoMapOverride: { maxFiles, maxLines, skip: skipRepoMap }
|
|
5782
7549
|
})
|
|
5783
7550
|
);
|
|
5784
7551
|
await waitUntilExit();
|