@illuminis/comprism 0.1.2 → 0.1.4
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 +98 -36
- package/out/agent/command.d.ts +75 -4
- package/out/agent/command.js +220 -25
- package/out/agent/render.d.ts +17 -2
- package/out/agent/render.js +157 -14
- package/out/agent/session.d.ts +141 -2
- package/out/agent/session.js +735 -149
- package/out/commands/agents.d.ts +2 -0
- package/out/commands/agents.js +79 -0
- package/out/commands/ask.d.ts +1 -1
- package/out/commands/ask.js +78 -11
- package/out/commands/commands-thin.js +79 -17
- package/out/commands/config.d.ts +1 -0
- package/out/commands/config.js +138 -0
- package/out/commands/cost.d.ts +1 -0
- package/out/commands/cost.js +167 -0
- package/out/commands/hooks.d.ts +1 -0
- package/out/commands/hooks.js +83 -0
- package/out/commands/install.d.ts +44 -1
- package/out/commands/install.js +198 -4
- package/out/commands/instructions.d.ts +1 -0
- package/out/commands/instructions.js +113 -0
- package/out/commands/integrations.d.ts +3 -0
- package/out/commands/integrations.js +215 -0
- package/out/commands/jobs.d.ts +5 -0
- package/out/commands/jobs.js +157 -0
- package/out/commands/login.js +188 -36
- package/out/commands/memory.d.ts +3 -0
- package/out/commands/memory.js +113 -0
- package/out/commands/permissions.d.ts +1 -0
- package/out/commands/permissions.js +94 -0
- package/out/commands/plugins.d.ts +4 -0
- package/out/commands/plugins.js +192 -0
- package/out/commands/privacy.d.ts +1 -0
- package/out/commands/privacy.js +57 -0
- package/out/commands/providerKey.d.ts +32 -0
- package/out/commands/providerKey.js +108 -0
- package/out/commands/repl.d.ts +8 -1
- package/out/commands/repl.js +1207 -118
- package/out/commands/report.d.ts +39 -0
- package/out/commands/report.js +115 -0
- package/out/commands/review.d.ts +5 -0
- package/out/commands/review.js +223 -0
- package/out/commands/sessions.d.ts +23 -0
- package/out/commands/sessions.js +115 -0
- package/out/commands/settings.d.ts +3 -1
- package/out/commands/settings.js +18 -16
- package/out/commands/skills.d.ts +21 -0
- package/out/commands/skills.js +207 -0
- package/out/commands/unattended.d.ts +7 -0
- package/out/commands/unattended.js +351 -0
- package/out/commands/update.d.ts +1 -0
- package/out/commands/update.js +123 -0
- package/out/commands/worktrees.d.ts +5 -0
- package/out/commands/worktrees.js +186 -0
- package/out/executor/browser.d.ts +14 -0
- package/out/executor/browser.js +270 -0
- package/out/executor/diagnostics.d.ts +2 -0
- package/out/executor/diagnostics.js +181 -0
- package/out/executor/files.js +270 -40
- package/out/executor/git.js +42 -29
- package/out/executor/hooks.d.ts +42 -58
- package/out/executor/hooks.js +89 -182
- package/out/executor/index.d.ts +21 -5
- package/out/executor/index.js +160 -14
- package/out/executor/paths.d.ts +6 -1
- package/out/executor/paths.js +34 -6
- package/out/executor/sandbox.d.ts +40 -0
- package/out/executor/sandbox.js +299 -0
- package/out/executor/shell.d.ts +49 -4
- package/out/executor/shell.js +302 -56
- package/out/executor/toolservers.d.ts +20 -0
- package/out/executor/toolservers.js +189 -0
- package/out/executor/worktree.d.ts +9 -0
- package/out/executor/worktree.js +119 -0
- package/out/graph/read-python.js +2 -1
- package/out/lib/attach.d.ts +56 -12
- package/out/lib/attach.js +230 -63
- package/out/lib/clipboard.d.ts +23 -0
- package/out/lib/clipboard.js +182 -0
- package/out/lib/commandlist.d.ts +20 -0
- package/out/lib/commandlist.js +58 -0
- package/out/lib/decision.d.ts +22 -0
- package/out/lib/decision.js +50 -0
- package/out/lib/fingerprint.d.ts +25 -0
- package/out/lib/fingerprint.js +58 -0
- package/out/lib/gateway.d.ts +186 -2
- package/out/lib/gateway.js +59 -4
- package/out/lib/history.d.ts +24 -0
- package/out/lib/history.js +137 -0
- package/out/lib/ide.d.ts +19 -0
- package/out/lib/ide.js +131 -0
- package/out/lib/keyboard.d.ts +95 -0
- package/out/lib/keyboard.js +383 -0
- package/out/lib/machine.d.ts +21 -0
- package/out/lib/machine.js +91 -0
- package/out/lib/notify.d.ts +4 -0
- package/out/lib/notify.js +52 -0
- package/out/lib/output.d.ts +48 -0
- package/out/lib/output.js +108 -0
- package/out/lib/project-ops.d.ts +19 -0
- package/out/lib/project-ops.js +146 -0
- package/out/lib/project.d.ts +28 -0
- package/out/lib/project.js +114 -0
- package/out/lib/prompt.js +15 -2
- package/out/lib/queue.d.ts +13 -0
- package/out/lib/queue.js +116 -0
- package/out/lib/readiness.d.ts +19 -0
- package/out/lib/readiness.js +170 -1
- package/out/lib/self.d.ts +23 -0
- package/out/lib/self.js +124 -0
- package/out/lib/sessions.d.ts +19 -0
- package/out/lib/sessions.js +221 -0
- package/out/lib/stdin.d.ts +32 -0
- package/out/lib/stdin.js +117 -0
- package/out/lib/store.d.ts +40 -0
- package/out/lib/store.js +138 -0
- package/out/lib/sync.d.ts +18 -0
- package/out/lib/sync.js +81 -0
- package/out/lib/ui.d.ts +2 -4
- package/out/lib/ui.js +31 -25
- package/out/lib/voice.js +24 -0
- package/out/postinstall.js +42 -14
- package/out/providers/anthropic.d.ts +22 -0
- package/out/providers/anthropic.js +80 -0
- package/out/providers/index.d.ts +8 -0
- package/out/providers/index.js +83 -0
- package/out/providers/openai.d.ts +11 -0
- package/out/providers/openai.js +57 -0
- package/out/thin.js +609 -21
- package/package.json +9 -49
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.readDiagnostics = readDiagnostics;
|
|
37
|
+
/**
|
|
38
|
+
* `read_diagnostics` in a terminal (manual 5.11): the project's own checker
|
|
39
|
+
* for the language of the changed file, run without installing anything.
|
|
40
|
+
*
|
|
41
|
+
* TypeScript's `tsc` for `.ts` and `.tsx`, Pyright for `.py`, each only when it
|
|
42
|
+
* is installed in the project or on this machine. When neither is, the result
|
|
43
|
+
* says the file could not be checked, so the agent never reads silence as
|
|
44
|
+
* "correct". One language at a time; others are added the same way.
|
|
45
|
+
*/
|
|
46
|
+
const fs = __importStar(require("fs"));
|
|
47
|
+
const path = __importStar(require("path"));
|
|
48
|
+
const paths_1 = require("./paths");
|
|
49
|
+
const shell_1 = require("./shell");
|
|
50
|
+
const self_1 = require("../lib/self");
|
|
51
|
+
/** A program from the project's own node_modules, walking up, then the PATH. */
|
|
52
|
+
function findTool(root, name) {
|
|
53
|
+
const exts = process.platform === "win32" ? [".cmd", ".exe", ""] : [""];
|
|
54
|
+
for (let dir = root;; dir = path.dirname(dir)) {
|
|
55
|
+
for (const e of exts) {
|
|
56
|
+
const p = path.join(dir, "node_modules", ".bin", name + e);
|
|
57
|
+
if (fs.existsSync(p))
|
|
58
|
+
return p;
|
|
59
|
+
}
|
|
60
|
+
if (path.dirname(dir) === dir)
|
|
61
|
+
break;
|
|
62
|
+
}
|
|
63
|
+
for (const d of (process.env.PATH ?? "").split(path.delimiter)) {
|
|
64
|
+
for (const e of exts) {
|
|
65
|
+
const p = path.join(d, name + e);
|
|
66
|
+
try {
|
|
67
|
+
if (fs.statSync(p).isFile())
|
|
68
|
+
return p;
|
|
69
|
+
}
|
|
70
|
+
catch { /* not here */ }
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
function slashed(p) {
|
|
76
|
+
return p.split(path.sep).join("/");
|
|
77
|
+
}
|
|
78
|
+
/** TypeScript's own script in the project's node_modules, walking up. Run
|
|
79
|
+
* with Node directly, so no `.cmd` launcher is involved on Windows. */
|
|
80
|
+
function findScript(root, parts) {
|
|
81
|
+
for (let dir = root;; dir = path.dirname(dir)) {
|
|
82
|
+
const p = path.join(dir, "node_modules", ...parts);
|
|
83
|
+
if (fs.existsSync(p))
|
|
84
|
+
return p;
|
|
85
|
+
if (path.dirname(dir) === dir)
|
|
86
|
+
return null;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
/** A tool as a program and its first arguments. */
|
|
90
|
+
function launcher(root, name, script) {
|
|
91
|
+
// The ready to run program cannot run another script, so it uses the
|
|
92
|
+
// project's own launcher, which the project's own Node runs.
|
|
93
|
+
const js = (0, self_1.isStandalone)() ? null : findScript(root, script);
|
|
94
|
+
if (js)
|
|
95
|
+
return [process.execPath, [js]];
|
|
96
|
+
const tool = findTool(root, name);
|
|
97
|
+
if (!tool)
|
|
98
|
+
return null;
|
|
99
|
+
if (process.platform === "win32" && /\.(cmd|bat)$/i.test(tool))
|
|
100
|
+
return ["cmd.exe", ["/d", "/c", tool]];
|
|
101
|
+
return [tool, []];
|
|
102
|
+
}
|
|
103
|
+
async function typescript(root, file, opts) {
|
|
104
|
+
const found0 = launcher(root, "tsc", ["typescript", "bin", "tsc"]);
|
|
105
|
+
if (!found0)
|
|
106
|
+
return null;
|
|
107
|
+
const [tsc, pre] = found0;
|
|
108
|
+
const hasConfig = fs.existsSync(path.join(root, "tsconfig.json"));
|
|
109
|
+
const args = hasConfig ? ["--noEmit", "--pretty", "false", "-p", "."]
|
|
110
|
+
: ["--noEmit", "--pretty", "false", "--strict", ...(file ? [file] : [])];
|
|
111
|
+
const r = await (0, shell_1.runProgram)(tsc, [...pre, ...args], { ...opts, root, timeoutMs: 120_000 });
|
|
112
|
+
const found = [];
|
|
113
|
+
for (const line of r.content.split("\n")) {
|
|
114
|
+
const m = /^(.+?)\((\d+),(\d+)\): (error|warning) (TS\d+: .*)$/.exec(line.trim());
|
|
115
|
+
if (m)
|
|
116
|
+
found.push({ file: slashed(path.relative(root, path.resolve(root, m[1]))), line: Number(m[2]),
|
|
117
|
+
col: Number(m[3]), severity: m[4], text: m[5] });
|
|
118
|
+
}
|
|
119
|
+
return found;
|
|
120
|
+
}
|
|
121
|
+
async function pyright(root, file, opts) {
|
|
122
|
+
const found0 = launcher(root, "pyright", ["pyright", "index.js"]);
|
|
123
|
+
if (!found0)
|
|
124
|
+
return null;
|
|
125
|
+
const [tool, pre] = found0;
|
|
126
|
+
const r = await (0, shell_1.runProgram)(tool, [...pre, "--outputjson", ...(file ? [file] : [])], { ...opts, root, timeoutMs: 120_000 });
|
|
127
|
+
const text = r.content.replace(/^exit -?\d+\n\n/, "");
|
|
128
|
+
try {
|
|
129
|
+
const data = JSON.parse(text.slice(text.indexOf("{")));
|
|
130
|
+
return (data.generalDiagnostics ?? []).map((d) => ({
|
|
131
|
+
file: slashed(path.relative(root, String(d.file))), line: Number(d.range?.start?.line ?? 0) + 1,
|
|
132
|
+
col: Number(d.range?.start?.character ?? 0) + 1, severity: String(d.severity), text: String(d.message),
|
|
133
|
+
}));
|
|
134
|
+
}
|
|
135
|
+
catch {
|
|
136
|
+
return [];
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
async function readDiagnostics(root, rel, severity, opts) {
|
|
140
|
+
const realRoot = fs.realpathSync(root);
|
|
141
|
+
const file = rel ? slashed(path.relative(realRoot, (0, paths_1.resolveInside)(realRoot, rel, true))) : null;
|
|
142
|
+
const ext = (file ?? "").toLowerCase().split(".").pop() ?? "";
|
|
143
|
+
const kinds = file ? (["ts", "tsx", "mts", "cts"].includes(ext) ? ["ts"] : ext === "py" ? ["py"] : [])
|
|
144
|
+
: ["ts", "py"];
|
|
145
|
+
if (!kinds.length) {
|
|
146
|
+
return { content: `No checker is set up for .${ext} files, so ${file} could not be checked. `
|
|
147
|
+
+ "Run the tests instead, and do not assume it is correct.", summary: "could not check",
|
|
148
|
+
facts: { checked: 0 } };
|
|
149
|
+
}
|
|
150
|
+
const found = [];
|
|
151
|
+
const missing = [];
|
|
152
|
+
let ran = 0;
|
|
153
|
+
for (const kind of kinds) {
|
|
154
|
+
const got = kind === "ts" ? await typescript(realRoot, file, opts) : await pyright(realRoot, file, opts);
|
|
155
|
+
if (got === null) {
|
|
156
|
+
missing.push(kind === "ts" ? "TypeScript's checker (tsc)" : "Pyright");
|
|
157
|
+
continue;
|
|
158
|
+
}
|
|
159
|
+
ran += 1;
|
|
160
|
+
// The changed file's own problems first, then what the change broke
|
|
161
|
+
// elsewhere: a wrong return type shows up where the function is used.
|
|
162
|
+
found.push(...got.filter((p) => !file || p.file === file), ...got.filter((p) => file && p.file !== file));
|
|
163
|
+
}
|
|
164
|
+
if (!ran) {
|
|
165
|
+
return {
|
|
166
|
+
content: `${missing.join(" and ")} ${missing.length > 1 ? "are" : "is"} not installed here, so `
|
|
167
|
+
+ `${file ?? "the project"} could not be checked. Run the tests instead, and do not claim the code is correct.`,
|
|
168
|
+
summary: "could not check", facts: { checked: 0 },
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
const wanted = severity === "all" ? found : found.filter((p) => p.severity === (severity || "error"));
|
|
172
|
+
const errors = found.filter((p) => p.severity === "error").length;
|
|
173
|
+
const lines = wanted.slice(0, 50).map((p) => `${p.file}:${p.line}:${p.col} ${p.severity} ${p.text}`);
|
|
174
|
+
if (wanted.length > 50)
|
|
175
|
+
lines.push(`[... ${wanted.length - 50} more left out]`);
|
|
176
|
+
return {
|
|
177
|
+
content: lines.length ? lines.join("\n") : `No ${severity === "all" ? "problems" : "errors"} in ${file ?? "the project"}.`,
|
|
178
|
+
summary: `${errors} errors`,
|
|
179
|
+
facts: { checked: 1, errors, warnings: found.length - errors },
|
|
180
|
+
};
|
|
181
|
+
}
|
package/out/executor/files.js
CHANGED
|
@@ -46,12 +46,16 @@ exports.performFileAction = performFileAction;
|
|
|
46
46
|
* writes is easy; one that refuses correctly is the one somebody can leave
|
|
47
47
|
* running.
|
|
48
48
|
*/
|
|
49
|
+
const child_process_1 = require("child_process");
|
|
50
|
+
const crypto_1 = require("crypto");
|
|
49
51
|
const fs = __importStar(require("fs"));
|
|
50
52
|
const path = __importStar(require("path"));
|
|
51
53
|
const paths_1 = require("./paths");
|
|
52
54
|
const MAX_FILE_CHARS = 60_000;
|
|
53
55
|
const MAX_RESULTS = 200;
|
|
54
56
|
const MAX_MAP_PATHS = 2_000;
|
|
57
|
+
/** Beyond this a file asked for whole is read as its first lines (manual 5.4). */
|
|
58
|
+
const MAX_WHOLE_LINES = 2_000;
|
|
55
59
|
const SKIP_DIRS = new Set([
|
|
56
60
|
"node_modules", ".git", "dist", "build", "out", ".next", ".nuxt", ".venv",
|
|
57
61
|
"venv", "env", "__pycache__", ".pytest_cache", ".mypy_cache", "coverage",
|
|
@@ -65,6 +69,8 @@ const TEXT_EXT = new Set([
|
|
|
65
69
|
"ini", "cfg", "conf", "md", "mdx", "txt", "rst", "csv", "tsv", "xml", "svg",
|
|
66
70
|
"vue", "svelte", "astro", "graphql", "gql", "proto", "lock", "prisma", "tf",
|
|
67
71
|
"r", "jl", "lua", "pl", "ex", "exs",
|
|
72
|
+
// Logs are the commonest large text file a person asks about (manual 6.13).
|
|
73
|
+
"log",
|
|
68
74
|
]);
|
|
69
75
|
function isText(name) {
|
|
70
76
|
const lower = name.toLowerCase();
|
|
@@ -78,7 +84,13 @@ function ok(content, summary) {
|
|
|
78
84
|
function fail(content) {
|
|
79
85
|
return { content, isError: true, summary: content.slice(0, 200) };
|
|
80
86
|
}
|
|
81
|
-
|
|
87
|
+
/** A project path as the agent sees it: forward slashes on every system.
|
|
88
|
+
* Windows would otherwise hand back `src\math.py`, which no pattern the model
|
|
89
|
+
* writes will ever match (manual 5.2). */
|
|
90
|
+
function slashed(rel) {
|
|
91
|
+
return rel.split(path.sep).join("/");
|
|
92
|
+
}
|
|
93
|
+
function walk(dir, root, out, limit, all = false) {
|
|
82
94
|
if (out.length >= limit)
|
|
83
95
|
return;
|
|
84
96
|
let entries;
|
|
@@ -93,19 +105,89 @@ function walk(dir, root, out, limit) {
|
|
|
93
105
|
return;
|
|
94
106
|
const full = path.join(dir, e.name);
|
|
95
107
|
if (e.isDirectory()) {
|
|
96
|
-
|
|
108
|
+
// Git's own folder is never project content, even when asked for all.
|
|
109
|
+
if (e.name === ".git" || (!all && SKIP_DIRS.has(e.name)))
|
|
97
110
|
continue;
|
|
98
111
|
// Not followed. A link pointing outside the project would otherwise pull
|
|
99
112
|
// the whole machine into a file map, and `resolveInside` would never see
|
|
100
113
|
// it because nobody asked for that path by name.
|
|
101
114
|
if (e.isSymbolicLink())
|
|
102
115
|
continue;
|
|
103
|
-
walk(full, root, out, limit);
|
|
116
|
+
walk(full, root, out, limit, all);
|
|
104
117
|
}
|
|
105
118
|
else if (e.isFile()) {
|
|
106
|
-
out.push(path.relative(root, full));
|
|
119
|
+
out.push(slashed(path.relative(root, full)));
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
/** Paths the service said to leave out of a search or listing, because a
|
|
124
|
+
* `deny` rule covers them (manual 4.11). The service decides; this only skips
|
|
125
|
+
* them, so nothing from a denied folder is read. The service still removes
|
|
126
|
+
* anything that slips through from what comes back. */
|
|
127
|
+
function denied(rel, exclude) {
|
|
128
|
+
if (!Array.isArray(exclude) || exclude.length === 0)
|
|
129
|
+
return false;
|
|
130
|
+
return exclude.some((raw) => {
|
|
131
|
+
const pattern = String(raw);
|
|
132
|
+
const base = pattern.endsWith("/**") ? pattern.slice(0, -3) : null;
|
|
133
|
+
if (base !== null && (rel === base || rel.startsWith(`${base}/`)))
|
|
134
|
+
return true;
|
|
135
|
+
return globToRegExp(pattern).test(rel);
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
/** What git itself would call this project's files: tracked, plus new files
|
|
139
|
+
* that `.gitignore` does not exclude. Git reads every ignore file, nested and
|
|
140
|
+
* global, so nothing here reimplements its rules. Null outside a repository,
|
|
141
|
+
* or when git is not installed, and the plain walk is used instead. */
|
|
142
|
+
function gitFiles(dir, root) {
|
|
143
|
+
let listed;
|
|
144
|
+
try {
|
|
145
|
+
// An argument list, never a shell string (COMMON.md, Windows quoting).
|
|
146
|
+
listed = (0, child_process_1.execFileSync)("git", ["ls-files", "-z", "--cached", "--others", "--exclude-standard"], {
|
|
147
|
+
cwd: dir, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"],
|
|
148
|
+
maxBuffer: 64 * 1024 * 1024, windowsHide: true,
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
catch {
|
|
152
|
+
return null;
|
|
153
|
+
}
|
|
154
|
+
const out = [];
|
|
155
|
+
for (const rel of listed.split("\0")) {
|
|
156
|
+
if (!rel)
|
|
157
|
+
continue;
|
|
158
|
+
// Dependency and build folders stay out even if someone committed them.
|
|
159
|
+
if (rel.split("/").some((part) => SKIP_DIRS.has(part)))
|
|
160
|
+
continue;
|
|
161
|
+
const full = path.join(dir, rel);
|
|
162
|
+
// A tracked file that was deleted, or a link, is not a file to read.
|
|
163
|
+
try {
|
|
164
|
+
if (!fs.lstatSync(full).isFile())
|
|
165
|
+
continue;
|
|
107
166
|
}
|
|
167
|
+
catch {
|
|
168
|
+
continue;
|
|
169
|
+
}
|
|
170
|
+
out.push(slashed(path.relative(root, full)));
|
|
171
|
+
}
|
|
172
|
+
return out;
|
|
173
|
+
}
|
|
174
|
+
/** The files under `dir` a search looks at. Ignored files, dependency folders
|
|
175
|
+
* and build output are left out unless `all` is asked for (manual 5.2). */
|
|
176
|
+
function projectFiles(dir, root, limit, all) {
|
|
177
|
+
// A search limited to one file searches that file (manual 5.3).
|
|
178
|
+
try {
|
|
179
|
+
if (fs.statSync(dir).isFile())
|
|
180
|
+
return [slashed(path.relative(root, dir))];
|
|
181
|
+
}
|
|
182
|
+
catch {
|
|
183
|
+
return [];
|
|
108
184
|
}
|
|
185
|
+
const listed = all ? null : gitFiles(dir, root);
|
|
186
|
+
if (listed)
|
|
187
|
+
return listed.sort().slice(0, limit);
|
|
188
|
+
const out = [];
|
|
189
|
+
walk(dir, root, out, limit, all);
|
|
190
|
+
return out.sort();
|
|
109
191
|
}
|
|
110
192
|
function globToRegExp(pattern) {
|
|
111
193
|
let out = "";
|
|
@@ -128,6 +210,43 @@ function globToRegExp(pattern) {
|
|
|
128
210
|
}
|
|
129
211
|
return new RegExp(`^${out}$`);
|
|
130
212
|
}
|
|
213
|
+
/** What each file held when the agent last read or wrote it, by real path, so
|
|
214
|
+
* an edit whose target has gone can say the file changed since it was read
|
|
215
|
+
* (manual 5.6). Only a fingerprint is kept, never the text. */
|
|
216
|
+
const seen = new Map();
|
|
217
|
+
function fingerprint(text) {
|
|
218
|
+
return (0, crypto_1.createHash)("sha256").update(text).digest("hex");
|
|
219
|
+
}
|
|
220
|
+
/** Lines taken out and put in by replacing `from` with `to`. */
|
|
221
|
+
function linesIn(text) {
|
|
222
|
+
if (!text)
|
|
223
|
+
return 0;
|
|
224
|
+
return text.split("\n").length - (text.endsWith("\n") ? 1 : 0);
|
|
225
|
+
}
|
|
226
|
+
/** Lines really taken out and put in: lines the edit repeated unchanged at
|
|
227
|
+
* either end, for context, are not counted as changed (manual 5.6). */
|
|
228
|
+
function trimmedCounts(from, to) {
|
|
229
|
+
const a = from.replace(/\n$/, "").split("\n");
|
|
230
|
+
const b = to.replace(/\n$/, "").split("\n");
|
|
231
|
+
if (!from)
|
|
232
|
+
return [0, linesIn(to)];
|
|
233
|
+
if (!to)
|
|
234
|
+
return [linesIn(from), 0];
|
|
235
|
+
let head = 0;
|
|
236
|
+
while (head < a.length && head < b.length && a[head] === b[head])
|
|
237
|
+
head += 1;
|
|
238
|
+
let tail = 0;
|
|
239
|
+
while (tail < a.length - head && tail < b.length - head
|
|
240
|
+
&& a[a.length - 1 - tail] === b[b.length - 1 - tail])
|
|
241
|
+
tail += 1;
|
|
242
|
+
return [a.length - head - tail, b.length - head - tail];
|
|
243
|
+
}
|
|
244
|
+
function lineCounts(from, to) {
|
|
245
|
+
const [removed, added] = trimmedCounts(from, to);
|
|
246
|
+
return { changed: 1, lines_removed: removed, lines_added: added };
|
|
247
|
+
}
|
|
248
|
+
const STALE = (rel) => `Refused, and nothing was written: the file changed since it was read. ${rel} no longer holds that text, `
|
|
249
|
+
+ "so somebody or something else edited it. Read it again and edit what is there now.";
|
|
131
250
|
function countOf(haystack, needle) {
|
|
132
251
|
if (!needle)
|
|
133
252
|
return 0;
|
|
@@ -152,40 +271,89 @@ function performFileAction(root, name, a) {
|
|
|
152
271
|
const size = fs.statSync(p).size;
|
|
153
272
|
return fail(`${rel} is not a text file (${size.toLocaleString()} bytes). Reading it would be noise rather than context.`);
|
|
154
273
|
}
|
|
155
|
-
|
|
274
|
+
const whole = fs.readFileSync(p, "utf8");
|
|
275
|
+
seen.set(p, fingerprint(whole));
|
|
276
|
+
const lines = whole.split("\n");
|
|
277
|
+
// A file ending in a newline has no line after it.
|
|
278
|
+
if (lines.length > 1 && lines[lines.length - 1] === "")
|
|
279
|
+
lines.pop();
|
|
280
|
+
const total = lines.length;
|
|
156
281
|
const offset = Number(a.offset ?? 0), limit = Number(a.limit ?? 0);
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
282
|
+
const ranged = offset > 0 || limit > 0;
|
|
283
|
+
// Small files are read whole. A large one asked for whole gets its
|
|
284
|
+
// first lines and is told how to reach the rest, so a 5,000 line file
|
|
285
|
+
// never lands in the conversation entire (manual 5.4).
|
|
286
|
+
const large = total > MAX_WHOLE_LINES || whole.length > MAX_FILE_CHARS;
|
|
287
|
+
const from = Math.min(Math.max(0, offset > 0 ? offset - 1 : 0), total);
|
|
288
|
+
let to = ranged ? (limit > 0 ? Math.min(total, from + limit) : total)
|
|
289
|
+
: large ? Math.min(total, MAX_WHOLE_LINES) : total;
|
|
290
|
+
let body = lines.slice(from, to).join("\n");
|
|
162
291
|
if (body.length > MAX_FILE_CHARS) {
|
|
163
|
-
|
|
292
|
+
// Cut on a line, so the range reported is the range given.
|
|
293
|
+
let size = 0, kept = from;
|
|
294
|
+
while (kept < to && size + lines[kept].length + 1 <= MAX_FILE_CHARS) {
|
|
295
|
+
size += lines[kept].length + 1;
|
|
296
|
+
kept += 1;
|
|
297
|
+
}
|
|
298
|
+
to = Math.max(kept, from + 1);
|
|
299
|
+
body = lines.slice(from, to).join("\n");
|
|
300
|
+
}
|
|
301
|
+
const n = (x) => x.toLocaleString("en-US");
|
|
302
|
+
if (to < total || from > 0) {
|
|
303
|
+
body += `\n\n[Lines ${n(from + 1)} to ${n(to)} of ${n(total)}. `
|
|
304
|
+
+ "Use grep to find what you need, then offset and limit to read just those lines.]";
|
|
164
305
|
}
|
|
165
|
-
return
|
|
306
|
+
return {
|
|
307
|
+
content: body,
|
|
308
|
+
summary: `read ${rel} lines ${n(from + 1)} to ${n(to)} of ${n(total)}`,
|
|
309
|
+
facts: { lines_from: total ? from + 1 : 0, lines_to: to, lines_total: total },
|
|
310
|
+
};
|
|
166
311
|
}
|
|
167
312
|
case "list_dir": {
|
|
168
313
|
const p = (0, paths_1.resolveInside)(realRoot, String(a.path ?? ""), true);
|
|
314
|
+
const here = slashed(path.relative(realRoot, p));
|
|
169
315
|
const rows = fs.readdirSync(p, { withFileTypes: true })
|
|
316
|
+
.filter((e) => !denied(here ? `${here}/${e.name}` : e.name, a.exclude))
|
|
170
317
|
.map((e) => (e.isDirectory() ? `${e.name}/` : e.name)).sort();
|
|
171
318
|
return ok(rows.join("\n") || "(empty)", `${rows.length} entries`);
|
|
172
319
|
}
|
|
173
320
|
case "file_map": {
|
|
174
321
|
const p = (0, paths_1.resolveInside)(realRoot, String(a.path ?? ""), true);
|
|
175
|
-
const found =
|
|
176
|
-
|
|
177
|
-
found.
|
|
178
|
-
const note = found.length
|
|
179
|
-
? `\n\n[...
|
|
180
|
-
return
|
|
322
|
+
const found = projectFiles(p, realRoot, Number.MAX_SAFE_INTEGER, Boolean(a.include_ignored))
|
|
323
|
+
.filter((f) => !denied(f, a.exclude));
|
|
324
|
+
const shown = found.slice(0, MAX_MAP_PATHS);
|
|
325
|
+
const note = found.length > shown.length
|
|
326
|
+
? `\n\n[... ${found.length - shown.length} more paths left out. Map a subfolder for the rest.]` : "";
|
|
327
|
+
return {
|
|
328
|
+
content: shown.join("\n") + note, summary: `${found.length} files`,
|
|
329
|
+
facts: { files: shown.length, left_out: found.length - shown.length },
|
|
330
|
+
};
|
|
181
331
|
}
|
|
182
332
|
case "glob": {
|
|
183
333
|
const p = (0, paths_1.resolveInside)(realRoot, String(a.path ?? ""), true);
|
|
184
|
-
const found =
|
|
185
|
-
|
|
186
|
-
const
|
|
187
|
-
const
|
|
188
|
-
|
|
334
|
+
const found = projectFiles(p, realRoot, Number.MAX_SAFE_INTEGER, Boolean(a.include_ignored))
|
|
335
|
+
.filter((f) => !denied(f, a.exclude));
|
|
336
|
+
const pattern = String(a.pattern ?? "");
|
|
337
|
+
const rx = globToRegExp(pattern);
|
|
338
|
+
// A bare name pattern ("calc.py", "*.py") means anywhere in the
|
|
339
|
+
// project, the way a person reads it, not only the top folder.
|
|
340
|
+
const anywhere = pattern.includes("/") ? null : globToRegExp(`**/${pattern}`);
|
|
341
|
+
const all = found.filter((f) => rx.test(f) || Boolean(anywhere?.test(f)));
|
|
342
|
+
// Newest first, as the tool description promises.
|
|
343
|
+
const mtime = (f) => { try {
|
|
344
|
+
return fs.statSync(path.join(realRoot, f)).mtimeMs;
|
|
345
|
+
}
|
|
346
|
+
catch {
|
|
347
|
+
return 0;
|
|
348
|
+
} };
|
|
349
|
+
const hits = all.map((f) => [f, mtime(f)])
|
|
350
|
+
.sort((x, y) => y[1] - x[1]).slice(0, MAX_RESULTS).map(([f]) => f);
|
|
351
|
+
const note = all.length > hits.length
|
|
352
|
+
? `\n\n[... ${all.length - hits.length} more files left out. Narrow the pattern.]` : "";
|
|
353
|
+
return {
|
|
354
|
+
content: (hits.join("\n") || "(no matches)") + note, summary: `${all.length} files`,
|
|
355
|
+
facts: { files: hits.length, left_out: all.length - hits.length },
|
|
356
|
+
};
|
|
189
357
|
}
|
|
190
358
|
case "grep": {
|
|
191
359
|
const p = (0, paths_1.resolveInside)(realRoot, String(a.path ?? ""), true);
|
|
@@ -196,18 +364,22 @@ function performFileAction(root, name, a) {
|
|
|
196
364
|
catch (e) {
|
|
197
365
|
return fail(`That is not a valid regular expression: ${e.message}`);
|
|
198
366
|
}
|
|
199
|
-
const
|
|
200
|
-
const
|
|
201
|
-
|
|
367
|
+
const globText = a.glob ? String(a.glob) : "";
|
|
368
|
+
const globRx = globText ? globToRegExp(globText) : null;
|
|
369
|
+
const globAnywhere = globText && !globText.includes("/") ? globToRegExp(`**/${globText}`) : null;
|
|
370
|
+
const files = projectFiles(p, realRoot, Number.MAX_SAFE_INTEGER, Boolean(a.include_ignored))
|
|
371
|
+
.filter((f) => !denied(f, a.exclude));
|
|
202
372
|
const context = Math.max(0, Math.min(Number(a.context_lines ?? 0), 5));
|
|
203
373
|
const hits = [];
|
|
374
|
+
// Every match is counted, even past the cap, so the agent is told how
|
|
375
|
+
// many it did not see and can narrow the search (manual 5.3).
|
|
376
|
+
let matches = 0;
|
|
377
|
+
const matchedFiles = new Set();
|
|
204
378
|
for (const rel of files) {
|
|
205
|
-
if (hits.length >= MAX_RESULTS)
|
|
206
|
-
break;
|
|
207
379
|
const leaf = path.basename(rel);
|
|
208
380
|
if (!isText(leaf) || (0, paths_1.isSecret)(leaf))
|
|
209
381
|
continue;
|
|
210
|
-
if (globRx && !globRx.test(rel))
|
|
382
|
+
if (globRx && !globRx.test(rel) && !globAnywhere?.test(rel))
|
|
211
383
|
continue;
|
|
212
384
|
let text;
|
|
213
385
|
try {
|
|
@@ -217,9 +389,13 @@ function performFileAction(root, name, a) {
|
|
|
217
389
|
continue;
|
|
218
390
|
}
|
|
219
391
|
const lines = text.split("\n");
|
|
220
|
-
for (let i = 0; i < lines.length
|
|
392
|
+
for (let i = 0; i < lines.length; i += 1) {
|
|
221
393
|
if (!rx.test(lines[i]))
|
|
222
394
|
continue;
|
|
395
|
+
matches += 1;
|
|
396
|
+
matchedFiles.add(rel);
|
|
397
|
+
if (hits.length >= MAX_RESULTS)
|
|
398
|
+
continue;
|
|
223
399
|
if (context === 0)
|
|
224
400
|
hits.push(`${rel}:${i + 1}:${lines[i]}`);
|
|
225
401
|
else {
|
|
@@ -228,8 +404,15 @@ function performFileAction(root, name, a) {
|
|
|
228
404
|
}
|
|
229
405
|
}
|
|
230
406
|
}
|
|
231
|
-
const
|
|
232
|
-
|
|
407
|
+
const leftOut = matches - hits.length;
|
|
408
|
+
const note = leftOut > 0
|
|
409
|
+
? `\n\n[Showing ${hits.length} of ${matches.toLocaleString("en-US")} matches in ${matchedFiles.size} files; ${leftOut.toLocaleString("en-US")} left out. Narrow the pattern, the folder or the glob.]`
|
|
410
|
+
: "";
|
|
411
|
+
return {
|
|
412
|
+
content: (hits.join("\n") || "(no matches)") + note,
|
|
413
|
+
summary: `${matches} matches`,
|
|
414
|
+
facts: { matches: hits.length, left_out: leftOut, files: matchedFiles.size },
|
|
415
|
+
};
|
|
233
416
|
}
|
|
234
417
|
// Bytes rather than text, and the ONLY caller is the server writing a
|
|
235
418
|
// document it just built. A Word file, a deck or a PDF is not text, so it
|
|
@@ -275,11 +458,21 @@ function performFileAction(root, name, a) {
|
|
|
275
458
|
// watching knows that a file they already had was just replaced. "wrote"
|
|
276
459
|
// reads identically in both cases and hides exactly the one that matters.
|
|
277
460
|
const replaced = fs.existsSync(p);
|
|
461
|
+
const lineCount = content.split("\n").length - (content.endsWith("\n") ? 1 : 0);
|
|
462
|
+
if (replaced && fs.readFileSync(p, "utf8") === content) {
|
|
463
|
+
seen.set(p, fingerprint(content));
|
|
464
|
+
return {
|
|
465
|
+
content: `No change: ${rel} already holds exactly this. Nothing was written.`,
|
|
466
|
+
summary: `no change ${rel}`, facts: { changed: 0, created: 0, lines: lineCount },
|
|
467
|
+
};
|
|
468
|
+
}
|
|
278
469
|
fs.writeFileSync(p, content, "utf8");
|
|
470
|
+
seen.set(p, fingerprint(content));
|
|
279
471
|
const size = `${content.length.toLocaleString()} characters`;
|
|
472
|
+
const facts = { changed: 1, created: replaced ? 0 : 1, lines: lineCount };
|
|
280
473
|
return replaced
|
|
281
|
-
? ok(`Rewrote ${rel}, replacing what was there (${size}).`, `rewrote ${rel}`)
|
|
282
|
-
: ok(`Created ${rel} (${size}).`, `created ${rel}`);
|
|
474
|
+
? { ...ok(`Rewrote ${rel}, replacing what was there (${size}).`, `rewrote ${rel}`), facts }
|
|
475
|
+
: { ...ok(`Created ${rel} (${size}).`, `created ${rel}`), facts };
|
|
283
476
|
}
|
|
284
477
|
case "edit_file": {
|
|
285
478
|
const rel = String(a.path ?? "");
|
|
@@ -288,6 +481,10 @@ function performFileAction(root, name, a) {
|
|
|
288
481
|
const oldText = String(a.old_text ?? "");
|
|
289
482
|
const n = countOf(text, oldText);
|
|
290
483
|
if (n === 0) {
|
|
484
|
+
// Read before, and different now: somebody else changed it.
|
|
485
|
+
const before = seen.get(p);
|
|
486
|
+
if (before && before !== fingerprint(text))
|
|
487
|
+
return fail(STALE(rel));
|
|
291
488
|
return fail(`Refused: that exact text is not in ${rel}. Read the file again — whitespace and indentation have to match exactly.`);
|
|
292
489
|
}
|
|
293
490
|
if (n > 1) {
|
|
@@ -300,8 +497,19 @@ function performFileAction(root, name, a) {
|
|
|
300
497
|
if (a.new_text === undefined || a.new_text === null) {
|
|
301
498
|
return fail(`Refused, and nothing was changed: edit_file was called for ${rel} with no replacement text. Send "new_text". To delete the matched section, pass an empty string.`);
|
|
302
499
|
}
|
|
303
|
-
|
|
304
|
-
|
|
500
|
+
const newText = String(a.new_text);
|
|
501
|
+
if (newText === oldText) {
|
|
502
|
+
return {
|
|
503
|
+
content: `No change: the replacement is the same as the text already in ${rel}. Nothing was written.`,
|
|
504
|
+
summary: `no change ${rel}`, facts: { changed: 0 },
|
|
505
|
+
};
|
|
506
|
+
}
|
|
507
|
+
// A function as the replacement, so a `$` in the new text is written
|
|
508
|
+
// as a `$` and never read as a pattern.
|
|
509
|
+
const next = text.replace(oldText, () => newText);
|
|
510
|
+
fs.writeFileSync(p, next, "utf8");
|
|
511
|
+
seen.set(p, fingerprint(next));
|
|
512
|
+
return { ...ok(`Edited ${rel}.`, `edited ${rel}`), facts: lineCounts(oldText, newText) };
|
|
305
513
|
}
|
|
306
514
|
case "multi_edit": {
|
|
307
515
|
const rel = String(a.path ?? "");
|
|
@@ -311,6 +519,11 @@ function performFileAction(root, name, a) {
|
|
|
311
519
|
for (let i = 0; i < edits.length; i += 1) {
|
|
312
520
|
const from = String(edits[i].old_text ?? "");
|
|
313
521
|
const n = countOf(next, from);
|
|
522
|
+
if (n === 0 && i === 0) {
|
|
523
|
+
const before = seen.get(p);
|
|
524
|
+
if (before && before !== fingerprint(next))
|
|
525
|
+
return fail(STALE(rel));
|
|
526
|
+
}
|
|
314
527
|
if (n !== 1) {
|
|
315
528
|
return fail(`Refused, and nothing was changed: edit ${i + 1} of ${edits.length} matches ${n} times in ${rel}. Every edit has to match exactly once.`);
|
|
316
529
|
}
|
|
@@ -320,10 +533,24 @@ function performFileAction(root, name, a) {
|
|
|
320
533
|
if (to === undefined || to === null) {
|
|
321
534
|
return fail(`Refused, and nothing was changed: edit ${i + 1} of ${edits.length} for ${rel} has no replacement text. Send "new_text". To delete the matched section, pass an empty string.`);
|
|
322
535
|
}
|
|
323
|
-
next = next.replace(from, String(to));
|
|
536
|
+
next = next.replace(from, () => String(to));
|
|
537
|
+
}
|
|
538
|
+
const original = fs.readFileSync(p, "utf8");
|
|
539
|
+
if (next === original) {
|
|
540
|
+
return {
|
|
541
|
+
content: `No change: those edits leave ${rel} exactly as it was. Nothing was written.`,
|
|
542
|
+
summary: `no change ${rel}`, facts: { changed: 0 },
|
|
543
|
+
};
|
|
324
544
|
}
|
|
325
545
|
fs.writeFileSync(p, next, "utf8");
|
|
326
|
-
|
|
546
|
+
seen.set(p, fingerprint(next));
|
|
547
|
+
const counted = edits.map((e) => trimmedCounts(String(e.old_text ?? ""), String(e.new_text ?? "")));
|
|
548
|
+
const removed = counted.reduce((t, c) => t + c[0], 0);
|
|
549
|
+
const added = counted.reduce((t, c) => t + c[1], 0);
|
|
550
|
+
return {
|
|
551
|
+
...ok(`Applied ${edits.length} edits to ${rel}.`, `${edits.length} edits`),
|
|
552
|
+
facts: { changed: 1, lines_removed: removed, lines_added: added },
|
|
553
|
+
};
|
|
327
554
|
}
|
|
328
555
|
case "create_dir": {
|
|
329
556
|
const rel = String(a.path ?? "");
|
|
@@ -348,8 +575,11 @@ function performFileAction(root, name, a) {
|
|
|
348
575
|
}
|
|
349
576
|
}
|
|
350
577
|
catch (err) {
|
|
351
|
-
|
|
352
|
-
|
|
578
|
+
// The whole refusal, path and real location included, however long the
|
|
579
|
+
// path (manual 2.4). An ordinary failure summary stops at 200 characters.
|
|
580
|
+
if (err instanceof paths_1.OutsideWorkspace) {
|
|
581
|
+
return { content: err.message, isError: true, summary: err.message.slice(0, 2000) };
|
|
582
|
+
}
|
|
353
583
|
const e = err;
|
|
354
584
|
if (e.code === "ENOENT")
|
|
355
585
|
return fail(`No such file: ${a.path ?? ""}`);
|