@illuminis/comprism 0.1.4 → 0.1.6
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/out/agent/command.js +5 -5
- package/out/agent/render.d.ts +39 -6
- package/out/agent/render.js +287 -28
- package/out/agent/session.d.ts +21 -1
- package/out/agent/session.js +81 -17
- package/out/commands/agents.js +3 -3
- package/out/commands/ask.js +20 -11
- package/out/commands/codemap.d.ts +1 -1
- package/out/commands/codemap.js +3 -3
- package/out/commands/commands-thin.js +4 -4
- package/out/commands/config.js +1 -1
- package/out/commands/cost.js +4 -4
- package/out/commands/hooks.js +1 -1
- package/out/commands/install.d.ts +1 -1
- package/out/commands/install.js +3 -2
- package/out/commands/instructions.js +1 -1
- package/out/commands/integrations.js +4 -4
- package/out/commands/keys.js +6 -6
- package/out/commands/login.js +97 -27
- package/out/commands/permissions.js +2 -2
- package/out/commands/plugins.js +3 -3
- package/out/commands/privacy.js +2 -2
- package/out/commands/repl.d.ts +4 -0
- package/out/commands/repl.js +563 -101
- package/out/commands/review.js +6 -6
- package/out/commands/settings.js +30 -123
- package/out/commands/skills.js +2 -2
- package/out/commands/unattended.js +6 -6
- package/out/commands/update.js +1 -1
- package/out/commands/welcome.d.ts +25 -1
- package/out/commands/welcome.js +65 -38
- package/out/commands/worktrees.d.ts +1 -1
- package/out/graph/sync.d.ts +1 -1
- package/out/graph/sync.js +6 -6
- package/out/lib/attach.js +4 -3
- package/out/lib/commandlist.d.ts +1 -1
- package/out/lib/commandlist.js +2 -2
- package/out/lib/config.d.ts +3 -2
- package/out/lib/config.js +7 -8
- package/out/lib/connection.js +1 -1
- package/out/lib/gateway.d.ts +9 -5
- package/out/lib/keyboard.d.ts +3 -0
- package/out/lib/keyboard.js +7 -0
- package/out/lib/lastlogin.d.ts +14 -0
- package/out/lib/lastlogin.js +105 -0
- package/out/lib/machine.js +1 -1
- package/out/lib/picker.d.ts +1 -0
- package/out/lib/picker.js +158 -0
- package/out/lib/project-ops.d.ts +1 -1
- package/out/lib/project-ops.js +4 -4
- package/out/lib/prompt.d.ts +3 -0
- package/out/lib/prompt.js +21 -0
- package/out/lib/queue.js +1 -1
- package/out/lib/readiness.d.ts +3 -30
- package/out/lib/readiness.js +13 -82
- package/out/lib/servicecommand.d.ts +14 -0
- package/out/lib/servicecommand.js +74 -0
- package/out/lib/sessions.js +3 -3
- package/out/lib/startcache.d.ts +13 -0
- package/out/lib/startcache.js +78 -0
- package/out/lib/ui.d.ts +11 -0
- package/out/lib/ui.js +28 -0
- package/out/lib/voice.d.ts +36 -0
- package/out/lib/voice.js +137 -2
- package/out/lib/words.d.ts +20 -0
- package/out/lib/words.js +27 -0
- package/out/machine/codemap/build.d.ts +45 -0
- package/out/machine/codemap/build.js +91 -0
- package/out/machine/codemap/facts.d.ts +47 -0
- package/out/machine/codemap/facts.js +12 -0
- package/out/machine/codemap/files.d.ts +45 -0
- package/out/machine/codemap/files.js +207 -0
- package/out/machine/codemap/read-locales.d.ts +29 -0
- package/out/machine/codemap/read-locales.js +246 -0
- package/out/machine/codemap/read-python.d.ts +11 -0
- package/out/machine/codemap/read-python.js +116 -0
- package/out/machine/codemap/read-typescript.d.ts +16 -0
- package/out/machine/codemap/read-typescript.js +292 -0
- package/out/machine/executor/browser.d.ts +14 -0
- package/out/machine/executor/browser.js +270 -0
- package/out/machine/executor/diagnostics.d.ts +2 -0
- package/out/machine/executor/diagnostics.js +181 -0
- package/out/machine/executor/documents.d.ts +40 -0
- package/out/machine/executor/documents.js +170 -0
- package/out/machine/executor/files.d.ts +2 -0
- package/out/machine/executor/files.js +590 -0
- package/out/machine/executor/git.d.ts +48 -0
- package/out/machine/executor/git.js +145 -0
- package/out/machine/executor/hooks.d.ts +51 -0
- package/out/machine/executor/hooks.js +154 -0
- package/out/machine/executor/index.d.ts +45 -0
- package/out/machine/executor/index.js +367 -0
- package/out/machine/executor/notebook.d.ts +2 -0
- package/out/machine/executor/notebook.js +147 -0
- package/out/machine/executor/paths.d.ts +20 -0
- package/out/machine/executor/paths.js +154 -0
- package/out/machine/executor/sandbox.d.ts +40 -0
- package/out/machine/executor/sandbox.js +299 -0
- package/out/machine/executor/shell.d.ts +86 -0
- package/out/machine/executor/shell.js +582 -0
- package/out/machine/executor/toolservers.d.ts +20 -0
- package/out/machine/executor/toolservers.js +189 -0
- package/out/machine/executor/worktree.d.ts +9 -0
- package/out/machine/executor/worktree.js +119 -0
- package/out/machine/folder/project.d.ts +28 -0
- package/out/machine/folder/project.js +114 -0
- package/out/machine/runtime/home.d.ts +2 -0
- package/out/machine/runtime/home.js +48 -0
- package/out/machine/runtime/needs.d.ts +30 -0
- package/out/machine/runtime/needs.js +89 -0
- package/out/machine/runtime/self.d.ts +23 -0
- package/out/machine/runtime/self.js +124 -0
- package/out/native/dictate +0 -0
- package/out/providers/index.js +2 -1
- package/out/thin.js +13 -12
- package/package.json +5 -4
|
@@ -0,0 +1,590 @@
|
|
|
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.performFileAction = performFileAction;
|
|
37
|
+
/**
|
|
38
|
+
* The file actions, on a real filesystem.
|
|
39
|
+
*
|
|
40
|
+
* Specification: docs/modules/CODING_AGENT_BUILD_SPECIFICATION.md, register
|
|
41
|
+
* items D1 to D16 and E6 to E8.
|
|
42
|
+
*
|
|
43
|
+
* The same twelve actions the browser performs, with the same refusals, against
|
|
44
|
+
* a real disk. **The refusals are the part worth reading**: an ambiguous edit,
|
|
45
|
+
* a path outside the project, a credentials file. An executor that reads and
|
|
46
|
+
* writes is easy; one that refuses correctly is the one somebody can leave
|
|
47
|
+
* running.
|
|
48
|
+
*/
|
|
49
|
+
const child_process_1 = require("child_process");
|
|
50
|
+
const crypto_1 = require("crypto");
|
|
51
|
+
const fs = __importStar(require("fs"));
|
|
52
|
+
const path = __importStar(require("path"));
|
|
53
|
+
const paths_1 = require("./paths");
|
|
54
|
+
const MAX_FILE_CHARS = 60_000;
|
|
55
|
+
const MAX_RESULTS = 200;
|
|
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;
|
|
59
|
+
const SKIP_DIRS = new Set([
|
|
60
|
+
"node_modules", ".git", "dist", "build", "out", ".next", ".nuxt", ".venv",
|
|
61
|
+
"venv", "env", "__pycache__", ".pytest_cache", ".mypy_cache", "coverage",
|
|
62
|
+
".turbo", ".cache", "target", "vendor", ".idea", "Pods", ".gradle",
|
|
63
|
+
".terraform", ".serverless", "site-packages",
|
|
64
|
+
]);
|
|
65
|
+
const TEXT_EXT = new Set([
|
|
66
|
+
"ts", "tsx", "js", "jsx", "mjs", "cjs", "py", "rb", "go", "rs", "java", "kt",
|
|
67
|
+
"swift", "c", "h", "cc", "cpp", "hpp", "cs", "php", "sh", "bash", "zsh",
|
|
68
|
+
"sql", "html", "css", "scss", "less", "json", "jsonc", "yaml", "yml", "toml",
|
|
69
|
+
"ini", "cfg", "conf", "md", "mdx", "txt", "rst", "csv", "tsv", "xml", "svg",
|
|
70
|
+
"vue", "svelte", "astro", "graphql", "gql", "proto", "lock", "prisma", "tf",
|
|
71
|
+
"r", "jl", "lua", "pl", "ex", "exs",
|
|
72
|
+
// Logs are the commonest large text file a person asks about (manual 6.13).
|
|
73
|
+
"log",
|
|
74
|
+
]);
|
|
75
|
+
function isText(name) {
|
|
76
|
+
const lower = name.toLowerCase();
|
|
77
|
+
if (!lower.includes("."))
|
|
78
|
+
return ["makefile", "dockerfile", "gitignore"].includes(lower);
|
|
79
|
+
return TEXT_EXT.has(lower.split(".").pop());
|
|
80
|
+
}
|
|
81
|
+
function ok(content, summary) {
|
|
82
|
+
return { content, summary: summary ?? content.split("\n")[0].slice(0, 200) };
|
|
83
|
+
}
|
|
84
|
+
function fail(content) {
|
|
85
|
+
return { content, isError: true, summary: content.slice(0, 200) };
|
|
86
|
+
}
|
|
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) {
|
|
94
|
+
if (out.length >= limit)
|
|
95
|
+
return;
|
|
96
|
+
let entries;
|
|
97
|
+
try {
|
|
98
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
99
|
+
}
|
|
100
|
+
catch {
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
for (const e of entries) {
|
|
104
|
+
if (out.length >= limit)
|
|
105
|
+
return;
|
|
106
|
+
const full = path.join(dir, e.name);
|
|
107
|
+
if (e.isDirectory()) {
|
|
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)))
|
|
110
|
+
continue;
|
|
111
|
+
// Not followed. A link pointing outside the project would otherwise pull
|
|
112
|
+
// the whole machine into a file map, and `resolveInside` would never see
|
|
113
|
+
// it because nobody asked for that path by name.
|
|
114
|
+
if (e.isSymbolicLink())
|
|
115
|
+
continue;
|
|
116
|
+
walk(full, root, out, limit, all);
|
|
117
|
+
}
|
|
118
|
+
else if (e.isFile()) {
|
|
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;
|
|
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 [];
|
|
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();
|
|
191
|
+
}
|
|
192
|
+
function globToRegExp(pattern) {
|
|
193
|
+
let out = "";
|
|
194
|
+
for (let i = 0; i < pattern.length; i += 1) {
|
|
195
|
+
const ch = pattern[i];
|
|
196
|
+
if (ch === "*") {
|
|
197
|
+
if (pattern[i + 1] === "*") {
|
|
198
|
+
out += ".*";
|
|
199
|
+
i += 1;
|
|
200
|
+
if (pattern[i + 1] === "/")
|
|
201
|
+
i += 1;
|
|
202
|
+
}
|
|
203
|
+
else
|
|
204
|
+
out += "[^/]*";
|
|
205
|
+
}
|
|
206
|
+
else if (ch === "?")
|
|
207
|
+
out += "[^/]";
|
|
208
|
+
else
|
|
209
|
+
out += ch.replace(/[.+^${}()|[\]\\]/g, "\\$&");
|
|
210
|
+
}
|
|
211
|
+
return new RegExp(`^${out}$`);
|
|
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.";
|
|
250
|
+
function countOf(haystack, needle) {
|
|
251
|
+
if (!needle)
|
|
252
|
+
return 0;
|
|
253
|
+
let n = 0, at = haystack.indexOf(needle);
|
|
254
|
+
while (at !== -1) {
|
|
255
|
+
n += 1;
|
|
256
|
+
at = haystack.indexOf(needle, at + needle.length);
|
|
257
|
+
}
|
|
258
|
+
return n;
|
|
259
|
+
}
|
|
260
|
+
function performFileAction(root, name, a) {
|
|
261
|
+
const realRoot = fs.realpathSync(root);
|
|
262
|
+
try {
|
|
263
|
+
switch (name) {
|
|
264
|
+
case "read_file": {
|
|
265
|
+
const rel = String(a.path ?? "");
|
|
266
|
+
if ((0, paths_1.isSecret)(rel)) {
|
|
267
|
+
return fail(`Refused: ${rel} holds credentials, and credentials are never read into a prompt.`);
|
|
268
|
+
}
|
|
269
|
+
const p = (0, paths_1.resolveInside)(realRoot, rel, true);
|
|
270
|
+
if (!isText(path.basename(p))) {
|
|
271
|
+
const size = fs.statSync(p).size;
|
|
272
|
+
return fail(`${rel} is not a text file (${size.toLocaleString()} bytes). Reading it would be noise rather than context.`);
|
|
273
|
+
}
|
|
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;
|
|
281
|
+
const offset = Number(a.offset ?? 0), limit = Number(a.limit ?? 0);
|
|
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");
|
|
291
|
+
if (body.length > MAX_FILE_CHARS) {
|
|
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.]";
|
|
305
|
+
}
|
|
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
|
+
};
|
|
311
|
+
}
|
|
312
|
+
case "list_dir": {
|
|
313
|
+
const p = (0, paths_1.resolveInside)(realRoot, String(a.path ?? ""), true);
|
|
314
|
+
const here = slashed(path.relative(realRoot, p));
|
|
315
|
+
const rows = fs.readdirSync(p, { withFileTypes: true })
|
|
316
|
+
.filter((e) => !denied(here ? `${here}/${e.name}` : e.name, a.exclude))
|
|
317
|
+
.map((e) => (e.isDirectory() ? `${e.name}/` : e.name)).sort();
|
|
318
|
+
return ok(rows.join("\n") || "(empty)", `${rows.length} entries`);
|
|
319
|
+
}
|
|
320
|
+
case "file_map": {
|
|
321
|
+
const p = (0, paths_1.resolveInside)(realRoot, String(a.path ?? ""), true);
|
|
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
|
+
};
|
|
331
|
+
}
|
|
332
|
+
case "glob": {
|
|
333
|
+
const p = (0, paths_1.resolveInside)(realRoot, String(a.path ?? ""), true);
|
|
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
|
+
};
|
|
357
|
+
}
|
|
358
|
+
case "grep": {
|
|
359
|
+
const p = (0, paths_1.resolveInside)(realRoot, String(a.path ?? ""), true);
|
|
360
|
+
let rx;
|
|
361
|
+
try {
|
|
362
|
+
rx = new RegExp(String(a.pattern ?? ""), a.case_sensitive ? "" : "i");
|
|
363
|
+
}
|
|
364
|
+
catch (e) {
|
|
365
|
+
return fail(`That is not a valid regular expression: ${e.message}`);
|
|
366
|
+
}
|
|
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));
|
|
372
|
+
const context = Math.max(0, Math.min(Number(a.context_lines ?? 0), 5));
|
|
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();
|
|
378
|
+
for (const rel of files) {
|
|
379
|
+
const leaf = path.basename(rel);
|
|
380
|
+
if (!isText(leaf) || (0, paths_1.isSecret)(leaf))
|
|
381
|
+
continue;
|
|
382
|
+
if (globRx && !globRx.test(rel) && !globAnywhere?.test(rel))
|
|
383
|
+
continue;
|
|
384
|
+
let text;
|
|
385
|
+
try {
|
|
386
|
+
text = fs.readFileSync(path.join(realRoot, rel), "utf8");
|
|
387
|
+
}
|
|
388
|
+
catch {
|
|
389
|
+
continue;
|
|
390
|
+
}
|
|
391
|
+
const lines = text.split("\n");
|
|
392
|
+
for (let i = 0; i < lines.length; i += 1) {
|
|
393
|
+
if (!rx.test(lines[i]))
|
|
394
|
+
continue;
|
|
395
|
+
matches += 1;
|
|
396
|
+
matchedFiles.add(rel);
|
|
397
|
+
if (hits.length >= MAX_RESULTS)
|
|
398
|
+
continue;
|
|
399
|
+
if (context === 0)
|
|
400
|
+
hits.push(`${rel}:${i + 1}:${lines[i]}`);
|
|
401
|
+
else {
|
|
402
|
+
const from = Math.max(0, i - context), to = Math.min(lines.length, i + context + 1);
|
|
403
|
+
hits.push(`${rel}:${i + 1}:\n${lines.slice(from, to).join("\n")}`);
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
}
|
|
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
|
+
};
|
|
416
|
+
}
|
|
417
|
+
// Bytes rather than text, and the ONLY caller is the server writing a
|
|
418
|
+
// document it just built. A Word file, a deck or a PDF is not text, so it
|
|
419
|
+
// cannot come down the ordinary write, and a person who asked for a deck
|
|
420
|
+
// wants it in the folder they are standing in rather than behind a link.
|
|
421
|
+
//
|
|
422
|
+
// Deliberately NOT offered to the model. It is issued by the loop after
|
|
423
|
+
// `make_document`, in the same way the project's instruction file is read
|
|
424
|
+
// without the model asking. A model that could write arbitrary bytes into
|
|
425
|
+
// a repository would be a way to put something in a file that no diff
|
|
426
|
+
// shows and nobody reviews.
|
|
427
|
+
case "write_bytes": {
|
|
428
|
+
const rel = String(a.path ?? "");
|
|
429
|
+
if (a.base64 === undefined || a.base64 === null) {
|
|
430
|
+
return fail(`Refused, and nothing was written: write_bytes was called for ${rel} with no content.`);
|
|
431
|
+
}
|
|
432
|
+
const p = (0, paths_1.resolveInside)(realRoot, rel, false);
|
|
433
|
+
fs.mkdirSync(path.dirname(p), { recursive: true });
|
|
434
|
+
const replaced = fs.existsSync(p);
|
|
435
|
+
const bytes = Buffer.from(String(a.base64), "base64");
|
|
436
|
+
fs.writeFileSync(p, bytes);
|
|
437
|
+
return ok(`${replaced ? "Replaced" : "Saved"} ${rel} (${bytes.length.toLocaleString()} bytes).`, `${replaced ? "replaced" : "saved"} ${rel}`);
|
|
438
|
+
}
|
|
439
|
+
case "write_file": {
|
|
440
|
+
const rel = String(a.path ?? "");
|
|
441
|
+
// A call that arrives WITHOUT content is a malformed call, not a request
|
|
442
|
+
// for an empty file. Treating the two as the same is how a whole study
|
|
443
|
+
// was lost on 11 September 2026: three of four builds saved a blank page,
|
|
444
|
+
// this returned "ok", and nobody found out until the files were measured.
|
|
445
|
+
// The model was not at fault - it was told the write succeeded, and on one
|
|
446
|
+
// run it tried three times and was told so three times.
|
|
447
|
+
//
|
|
448
|
+
// An empty file is still reachable, deliberately, with content: "".
|
|
449
|
+
if (a.content === undefined || a.content === null) {
|
|
450
|
+
return fail(`Refused, and nothing was written: write_file was called for ${rel} with no content at all. Send the file's full text in "content". To create a genuinely empty file, pass an empty string.`);
|
|
451
|
+
}
|
|
452
|
+
const p = (0, paths_1.resolveInside)(realRoot, rel, false);
|
|
453
|
+
fs.mkdirSync(path.dirname(p), { recursive: true });
|
|
454
|
+
const content = String(a.content);
|
|
455
|
+
// Whether this REPLACED something is checked before the write, because
|
|
456
|
+
// afterwards there is nothing left to tell from. Writing no longer stops
|
|
457
|
+
// to ask, so saying which of the two happened is the only way a person
|
|
458
|
+
// watching knows that a file they already had was just replaced. "wrote"
|
|
459
|
+
// reads identically in both cases and hides exactly the one that matters.
|
|
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
|
+
}
|
|
469
|
+
fs.writeFileSync(p, content, "utf8");
|
|
470
|
+
seen.set(p, fingerprint(content));
|
|
471
|
+
const size = `${content.length.toLocaleString()} characters`;
|
|
472
|
+
const facts = { changed: 1, created: replaced ? 0 : 1, lines: lineCount };
|
|
473
|
+
return replaced
|
|
474
|
+
? { ...ok(`Rewrote ${rel}, replacing what was there (${size}).`, `rewrote ${rel}`), facts }
|
|
475
|
+
: { ...ok(`Created ${rel} (${size}).`, `created ${rel}`), facts };
|
|
476
|
+
}
|
|
477
|
+
case "edit_file": {
|
|
478
|
+
const rel = String(a.path ?? "");
|
|
479
|
+
const p = (0, paths_1.resolveInside)(realRoot, rel, true);
|
|
480
|
+
const text = fs.readFileSync(p, "utf8");
|
|
481
|
+
const oldText = String(a.old_text ?? "");
|
|
482
|
+
const n = countOf(text, oldText);
|
|
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));
|
|
488
|
+
return fail(`Refused: that exact text is not in ${rel}. Read the file again — whitespace and indentation have to match exactly.`);
|
|
489
|
+
}
|
|
490
|
+
if (n > 1) {
|
|
491
|
+
return fail(`Refused: that text appears ${n} times in ${rel}, so it is not clear which one you mean. Include more surrounding lines to make the target unique.`);
|
|
492
|
+
}
|
|
493
|
+
// Missing replacement text used to DELETE the matched section and report
|
|
494
|
+
// "edited". Worse than the write case, because it destroys work that was
|
|
495
|
+
// already there. Deleting a section on purpose still works, with
|
|
496
|
+
// new_text set to an empty string.
|
|
497
|
+
if (a.new_text === undefined || a.new_text === null) {
|
|
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.`);
|
|
499
|
+
}
|
|
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) };
|
|
513
|
+
}
|
|
514
|
+
case "multi_edit": {
|
|
515
|
+
const rel = String(a.path ?? "");
|
|
516
|
+
const p = (0, paths_1.resolveInside)(realRoot, rel, true);
|
|
517
|
+
const edits = a.edits ?? [];
|
|
518
|
+
let next = fs.readFileSync(p, "utf8");
|
|
519
|
+
for (let i = 0; i < edits.length; i += 1) {
|
|
520
|
+
const from = String(edits[i].old_text ?? "");
|
|
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
|
+
}
|
|
527
|
+
if (n !== 1) {
|
|
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.`);
|
|
529
|
+
}
|
|
530
|
+
// Same rule as edit_file, checked before anything is written, so a bad
|
|
531
|
+
// edit halfway down a list cannot leave the file half changed.
|
|
532
|
+
const to = edits[i].new_text;
|
|
533
|
+
if (to === undefined || to === null) {
|
|
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.`);
|
|
535
|
+
}
|
|
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
|
+
};
|
|
544
|
+
}
|
|
545
|
+
fs.writeFileSync(p, next, "utf8");
|
|
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
|
+
};
|
|
554
|
+
}
|
|
555
|
+
case "create_dir": {
|
|
556
|
+
const rel = String(a.path ?? "");
|
|
557
|
+
fs.mkdirSync((0, paths_1.resolveInside)(realRoot, rel, false), { recursive: true });
|
|
558
|
+
return ok(`Created ${rel}.`, `created ${rel}`);
|
|
559
|
+
}
|
|
560
|
+
case "delete_file": {
|
|
561
|
+
const rel = String(a.path ?? "");
|
|
562
|
+
fs.unlinkSync((0, paths_1.resolveInside)(realRoot, rel, true));
|
|
563
|
+
return ok(`Deleted ${rel}.`, `deleted ${rel}`);
|
|
564
|
+
}
|
|
565
|
+
case "move_file": {
|
|
566
|
+
const rel = String(a.path ?? ""), to = String(a.to ?? "");
|
|
567
|
+
const from = (0, paths_1.resolveInside)(realRoot, rel, true);
|
|
568
|
+
const dest = (0, paths_1.resolveInside)(realRoot, to, false);
|
|
569
|
+
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
|
570
|
+
fs.renameSync(from, dest);
|
|
571
|
+
return ok(`Moved ${rel} to ${to}.`, `moved ${rel}`);
|
|
572
|
+
}
|
|
573
|
+
default:
|
|
574
|
+
return fail(`This client cannot do that: ${name}`);
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
catch (err) {
|
|
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
|
+
}
|
|
583
|
+
const e = err;
|
|
584
|
+
if (e.code === "ENOENT")
|
|
585
|
+
return fail(`No such file: ${a.path ?? ""}`);
|
|
586
|
+
if (e.code === "EACCES")
|
|
587
|
+
return fail(`Permission denied: ${a.path ?? ""}`);
|
|
588
|
+
return fail(`This action could not be completed: ${e.message}`);
|
|
589
|
+
}
|
|
590
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reading the repository, and recording work in it.
|
|
3
|
+
*
|
|
4
|
+
* Specification: docs/modules/CODING_AGENT_BUILD_SPECIFICATION.md, register
|
|
5
|
+
* items D23 to D27.
|
|
6
|
+
*
|
|
7
|
+
* **Pushing is the only thing here that sends anything anywhere**, and it is
|
|
8
|
+
* the only one the server has to have allowed first. Pull requests, reviews,
|
|
9
|
+
* checks and releases are not git operations at all: they go through GitHub's
|
|
10
|
+
* own API, on the server, because they need the tenant's credential and that
|
|
11
|
+
* must never be sent to a client.
|
|
12
|
+
*
|
|
13
|
+
* So the split is by what each thing needs. A push needs the repository, which
|
|
14
|
+
* is here. A pull request needs the credential, which is there.
|
|
15
|
+
*
|
|
16
|
+
* Every command goes through `runCommand`, so the folder boundary, the trimmed
|
|
17
|
+
* environment, the timeout and the output cap all apply here too rather than
|
|
18
|
+
* being reimplemented for git.
|
|
19
|
+
*/
|
|
20
|
+
import { RunResult, ShellOptions } from "./shell";
|
|
21
|
+
export declare function gitStatus(opts: ShellOptions): Promise<RunResult>;
|
|
22
|
+
export declare function gitDiff(opts: ShellOptions & {
|
|
23
|
+
path?: string;
|
|
24
|
+
staged?: boolean;
|
|
25
|
+
}): Promise<RunResult>;
|
|
26
|
+
export declare function gitLog(opts: ShellOptions & {
|
|
27
|
+
path?: string;
|
|
28
|
+
limit?: number;
|
|
29
|
+
}): Promise<RunResult>;
|
|
30
|
+
export declare function gitBranch(name: string, opts: ShellOptions): Promise<RunResult>;
|
|
31
|
+
export declare function gitCommit(message: string, opts: ShellOptions & {
|
|
32
|
+
paths?: string[];
|
|
33
|
+
}): Promise<RunResult>;
|
|
34
|
+
/** Send commits to the remote.
|
|
35
|
+
*
|
|
36
|
+
* Reached only after the server has allowed it: the branch is one the person
|
|
37
|
+
* named for this repository, it is not a branch the project is built from, and
|
|
38
|
+
* they have just confirmed it. This function does not re-decide any of that,
|
|
39
|
+
* and it must not: a client that could decide whether it may publish is a
|
|
40
|
+
* client that could grant itself permission.
|
|
41
|
+
*
|
|
42
|
+
* **There is no force push and there is no upstream rewrite.** Not as an
|
|
43
|
+
* option, not behind a flag. Everything on the remote stays recoverable by
|
|
44
|
+
* whoever is working from it.
|
|
45
|
+
*/
|
|
46
|
+
export declare function gitPush(branch: string, opts: ShellOptions & {
|
|
47
|
+
createRemote?: boolean;
|
|
48
|
+
}): Promise<RunResult>;
|