@illuminis/comprism 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +15 -0
- package/README.md +281 -0
- package/out/agent/command.d.ts +86 -0
- package/out/agent/command.js +259 -0
- package/out/agent/render.d.ts +97 -0
- package/out/agent/render.js +255 -0
- package/out/agent/session.d.ts +175 -0
- package/out/agent/session.js +573 -0
- package/out/commands/ask.d.ts +1 -0
- package/out/commands/ask.js +146 -0
- package/out/commands/codemap.d.ts +2 -0
- package/out/commands/codemap.js +151 -0
- package/out/commands/commands-thin.d.ts +39 -0
- package/out/commands/commands-thin.js +182 -0
- package/out/commands/install.d.ts +163 -0
- package/out/commands/install.js +543 -0
- package/out/commands/keys.d.ts +55 -0
- package/out/commands/keys.js +344 -0
- package/out/commands/login.d.ts +9 -0
- package/out/commands/login.js +384 -0
- package/out/commands/repl.d.ts +1 -0
- package/out/commands/repl.js +752 -0
- package/out/commands/settings.d.ts +21 -0
- package/out/commands/settings.js +244 -0
- package/out/commands/welcome.d.ts +1 -0
- package/out/commands/welcome.js +196 -0
- package/out/executor/documents.d.ts +40 -0
- package/out/executor/documents.js +170 -0
- package/out/executor/files.d.ts +2 -0
- package/out/executor/files.js +360 -0
- package/out/executor/git.d.ts +48 -0
- package/out/executor/git.js +132 -0
- package/out/executor/hooks.d.ts +67 -0
- package/out/executor/hooks.js +247 -0
- package/out/executor/index.d.ts +29 -0
- package/out/executor/index.js +221 -0
- package/out/executor/notebook.d.ts +2 -0
- package/out/executor/notebook.js +147 -0
- package/out/executor/paths.d.ts +15 -0
- package/out/executor/paths.js +126 -0
- package/out/executor/shell.d.ts +41 -0
- package/out/executor/shell.js +336 -0
- package/out/graph/build.d.ts +45 -0
- package/out/graph/build.js +91 -0
- package/out/graph/facts.d.ts +47 -0
- package/out/graph/facts.js +12 -0
- package/out/graph/files.d.ts +45 -0
- package/out/graph/files.js +207 -0
- package/out/graph/read-locales.d.ts +29 -0
- package/out/graph/read-locales.js +246 -0
- package/out/graph/read-python.d.ts +11 -0
- package/out/graph/read-python.js +115 -0
- package/out/graph/read-typescript.d.ts +16 -0
- package/out/graph/read-typescript.js +292 -0
- package/out/graph/sync.d.ts +66 -0
- package/out/graph/sync.js +242 -0
- package/out/lib/attach.d.ts +62 -0
- package/out/lib/attach.js +228 -0
- package/out/lib/config.d.ts +93 -0
- package/out/lib/config.js +198 -0
- package/out/lib/connection.d.ts +73 -0
- package/out/lib/connection.js +188 -0
- package/out/lib/gateway.d.ts +239 -0
- package/out/lib/gateway.js +171 -0
- package/out/lib/prompt.d.ts +34 -0
- package/out/lib/prompt.js +108 -0
- package/out/lib/types.d.ts +417 -0
- package/out/lib/types.js +21 -0
- package/out/lib/ui.d.ts +114 -0
- package/out/lib/ui.js +265 -0
- package/out/lib/version.d.ts +24 -0
- package/out/lib/version.js +27 -0
- package/out/lib/voice.d.ts +50 -0
- package/out/lib/voice.js +218 -0
- package/out/postinstall.d.ts +2 -0
- package/out/postinstall.js +92 -0
- package/out/thin.d.ts +2 -0
- package/out/thin.js +259 -0
- package/package.json +101 -0
- package/scripts/read_python.py +270 -0
|
@@ -0,0 +1,247 @@
|
|
|
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.EXAMPLE = void 0;
|
|
37
|
+
exports.hooksFor = hooksFor;
|
|
38
|
+
exports.covers = covers;
|
|
39
|
+
exports.before = before;
|
|
40
|
+
exports.after = after;
|
|
41
|
+
/**
|
|
42
|
+
* Something of your own, run before or after the agent acts.
|
|
43
|
+
*
|
|
44
|
+
* Specification: docs/modules/CODING_AGENT_BUILD_SPECIFICATION.md, register
|
|
45
|
+
* item C18. What Claude Code calls hooks.
|
|
46
|
+
*
|
|
47
|
+
* ## Why this belongs on the client
|
|
48
|
+
*
|
|
49
|
+
* A hook is the customer's own command, running in their own project, on their
|
|
50
|
+
* own machine. Nothing about it is ours to run: the command is theirs, the
|
|
51
|
+
* environment is theirs, and the thing it usually wants to do is reformat a
|
|
52
|
+
* file that only exists there.
|
|
53
|
+
*
|
|
54
|
+
* So the configuration is read from the project itself rather than from our
|
|
55
|
+
* database. A hook that lives in the repository is reviewed like any other
|
|
56
|
+
* change, travels with a branch, and is the same for everybody who works there.
|
|
57
|
+
* A hook stored in our settings would be invisible in the repository and
|
|
58
|
+
* different for whoever configured it.
|
|
59
|
+
*
|
|
60
|
+
* ## What they are for, in practice
|
|
61
|
+
*
|
|
62
|
+
* The overwhelming case is "run the formatter after the agent edits a file", and
|
|
63
|
+
* the reason it earns a feature is that the alternative is telling the model to
|
|
64
|
+
* remember to do it, which it does four times out of five.
|
|
65
|
+
*
|
|
66
|
+
* ## What a hook may not do
|
|
67
|
+
*
|
|
68
|
+
* **It cannot approve anything and it cannot change the decision.** A hook that
|
|
69
|
+
* could turn a refusal into an approval would be a permission system in a file
|
|
70
|
+
* inside the repository, which is the wrong place for one: the repository is
|
|
71
|
+
* exactly what an attacker who has got that far already controls.
|
|
72
|
+
*
|
|
73
|
+
* A hook that fails is reported and does not stop the job, with one exception
|
|
74
|
+
* stated in `before`: a `before` hook exiting non-zero cancels that one action,
|
|
75
|
+
* because "do not let the agent touch generated files" is a real thing to want
|
|
76
|
+
* and refusing is the only way to say it.
|
|
77
|
+
*/
|
|
78
|
+
const fs = __importStar(require("fs"));
|
|
79
|
+
const path = __importStar(require("path"));
|
|
80
|
+
const shell_1 = require("./shell");
|
|
81
|
+
/** Where a project declares its hooks, in the order they are tried.
|
|
82
|
+
*
|
|
83
|
+
* The settings file comes first, because that is where every other thing a
|
|
84
|
+
* project says about its agent now lives, and keeping hooks somewhere else was
|
|
85
|
+
* one more folder for a newcomer to know about.
|
|
86
|
+
*
|
|
87
|
+
* `.illuminis/hooks.json` is still read, and last. It is what projects were
|
|
88
|
+
* told to write, and silently ignoring a file somebody wrote on our
|
|
89
|
+
* instructions would mean their formatter stops running with no message
|
|
90
|
+
* anywhere. Only one file is used: the first that has hooks in it wins, so a
|
|
91
|
+
* project that has moved does not get both. */
|
|
92
|
+
const CONFIGS = [
|
|
93
|
+
".completionprism/settings.json",
|
|
94
|
+
".completionprism/settings.local.json",
|
|
95
|
+
".comprism/settings.json",
|
|
96
|
+
".claude/settings.json",
|
|
97
|
+
".illuminis/hooks.json",
|
|
98
|
+
];
|
|
99
|
+
/** What this project asks for, or nothing.
|
|
100
|
+
*
|
|
101
|
+
* A malformed file is ignored with a warning rather than failing the job. A
|
|
102
|
+
* project whose hook configuration has a trailing comma should not be a project
|
|
103
|
+
* where the agent refuses to work, and the person who broke it is not usually
|
|
104
|
+
* the person now trying to get something done.
|
|
105
|
+
*/
|
|
106
|
+
function hooksFor(root) {
|
|
107
|
+
for (const name of CONFIGS) {
|
|
108
|
+
const file = path.join(root, name);
|
|
109
|
+
if (!fs.existsSync(file))
|
|
110
|
+
continue;
|
|
111
|
+
try {
|
|
112
|
+
const parsed = JSON.parse(fs.readFileSync(file, "utf8"));
|
|
113
|
+
const hooks = Array.isArray(parsed.hooks) ? parsed.hooks : [];
|
|
114
|
+
const usable = hooks.filter((h) => h && typeof h.command === "string" && h.command.trim()
|
|
115
|
+
&& (h.when === "before" || h.when === "after")
|
|
116
|
+
&& typeof h.on === "string" && h.on.trim());
|
|
117
|
+
// A settings file with no hooks section is the ordinary case, and it must
|
|
118
|
+
// not stop the older file being read. Only a file that actually declares
|
|
119
|
+
// hooks ends the search.
|
|
120
|
+
if (usable.length)
|
|
121
|
+
return usable;
|
|
122
|
+
}
|
|
123
|
+
catch (err) {
|
|
124
|
+
process.stderr.write(` ${name} could not be read (${err.message}), so no hooks ran.\n`);
|
|
125
|
+
return [];
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
return [];
|
|
129
|
+
}
|
|
130
|
+
/** Does this hook's matcher cover this action.
|
|
131
|
+
*
|
|
132
|
+
* Exported because it is the part with the rules in it, and a rule with no
|
|
133
|
+
* test is a rule that drifts.
|
|
134
|
+
*/
|
|
135
|
+
function covers(pattern, action) {
|
|
136
|
+
const p = String(pattern ?? "").trim();
|
|
137
|
+
if (!p || !action)
|
|
138
|
+
return false;
|
|
139
|
+
if (p === "*")
|
|
140
|
+
return true;
|
|
141
|
+
if (p === action)
|
|
142
|
+
return true;
|
|
143
|
+
if (p.includes("|") && !/[\\^$.?*+()[\]{}]/.test(p)) {
|
|
144
|
+
return p.split("|").some((one) => one.trim() === action);
|
|
145
|
+
}
|
|
146
|
+
if (!/[\\^$.?*+()[\]{}|]/.test(p))
|
|
147
|
+
return false;
|
|
148
|
+
try {
|
|
149
|
+
return new RegExp(`^(?:${p})$`).test(action);
|
|
150
|
+
}
|
|
151
|
+
catch {
|
|
152
|
+
// A pattern that will not compile matches NOTHING. The other direction,
|
|
153
|
+
// matching everything, would run somebody's script on every action because
|
|
154
|
+
// of a typo, which is the expensive way to be wrong.
|
|
155
|
+
process.stderr.write(` A hook pattern is not valid and was skipped: ${p}\n`);
|
|
156
|
+
return false;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
function matching(hooks, action, when) {
|
|
160
|
+
return hooks.filter((h) => h.when === when && covers(h.on, action));
|
|
161
|
+
}
|
|
162
|
+
/** The path the action is about, for a hook that wants to know. */
|
|
163
|
+
function targetOf(args) {
|
|
164
|
+
return String(args.path ?? args.to ?? "");
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* Run the hooks that come before one action.
|
|
168
|
+
*
|
|
169
|
+
* **A non-zero exit cancels the action.** This is the one place a hook changes
|
|
170
|
+
* what happens, and it is deliberate: "do not let the agent touch anything under
|
|
171
|
+
* generated/" is a real thing to want, and refusing is the only way for a
|
|
172
|
+
* project to say it.
|
|
173
|
+
*
|
|
174
|
+
* It can only ever REFUSE. There is no exit code that approves something the
|
|
175
|
+
* agent was not already allowed to do, because a permission system inside the
|
|
176
|
+
* repository would live exactly where an attacker who got that far already is.
|
|
177
|
+
*/
|
|
178
|
+
async function before(root, action, args, hooks = hooksFor(root)) {
|
|
179
|
+
const applicable = matching(hooks, action, "before");
|
|
180
|
+
let ran = 0;
|
|
181
|
+
for (const hook of applicable) {
|
|
182
|
+
const result = await (0, shell_1.runCommand)(hook.command, {
|
|
183
|
+
root,
|
|
184
|
+
timeoutMs: 60_000,
|
|
185
|
+
// What the hook is about, as environment rather than as arguments, so an
|
|
186
|
+
// existing script needs no wrapper to read it.
|
|
187
|
+
extraEnv: { ILLUMINIS_ACTION: action, ILLUMINIS_PATH: targetOf(args) },
|
|
188
|
+
});
|
|
189
|
+
ran += 1;
|
|
190
|
+
if (result.isError || !/^exit 0/.test(result.content)) {
|
|
191
|
+
return {
|
|
192
|
+
ran,
|
|
193
|
+
refusedBy: hook.command,
|
|
194
|
+
detail: `${hook.what ?? "A check in this project"} refused this: ${result.content.slice(0, 500)}`,
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
return { ran };
|
|
199
|
+
}
|
|
200
|
+
/**
|
|
201
|
+
* Run the hooks that come after one action.
|
|
202
|
+
*
|
|
203
|
+
* A failure here is reported and nothing else: the action has already happened,
|
|
204
|
+
* and refusing it afterwards is not a thing that can be done. The overwhelming
|
|
205
|
+
* case is a formatter, and a formatter that fails is worth knowing about and not
|
|
206
|
+
* worth losing the work over.
|
|
207
|
+
*/
|
|
208
|
+
async function after(root, action, args, hooks = hooksFor(root)) {
|
|
209
|
+
const applicable = matching(hooks, action, "after");
|
|
210
|
+
const failed = [];
|
|
211
|
+
let ran = 0;
|
|
212
|
+
for (const hook of applicable) {
|
|
213
|
+
const result = await (0, shell_1.runCommand)(hook.command, {
|
|
214
|
+
root, timeoutMs: 120_000,
|
|
215
|
+
extraEnv: { ILLUMINIS_ACTION: action, ILLUMINIS_PATH: targetOf(args) },
|
|
216
|
+
});
|
|
217
|
+
ran += 1;
|
|
218
|
+
if (result.isError || !/^exit 0/.test(result.content)) {
|
|
219
|
+
failed.push(`${hook.what ?? hook.command}: ${result.content.slice(0, 300)}`);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
return { ran, failed };
|
|
223
|
+
}
|
|
224
|
+
/** The example a person copies. Kept in code so the readme and the behavior
|
|
225
|
+
* cannot drift: this is what the reader actually gets. */
|
|
226
|
+
exports.EXAMPLE = JSON.stringify({
|
|
227
|
+
hooks: [
|
|
228
|
+
{
|
|
229
|
+
on: "write_file|edit_file|multi_edit",
|
|
230
|
+
when: "after",
|
|
231
|
+
command: "npm run lint --silent",
|
|
232
|
+
what: "Lint after anything is written",
|
|
233
|
+
},
|
|
234
|
+
{
|
|
235
|
+
on: "edit_file",
|
|
236
|
+
when: "after",
|
|
237
|
+
command: "npx prettier --write $ILLUMINIS_PATH",
|
|
238
|
+
what: "Format the file that was just changed",
|
|
239
|
+
},
|
|
240
|
+
{
|
|
241
|
+
on: "write_file",
|
|
242
|
+
when: "before",
|
|
243
|
+
command: "test \"${ILLUMINIS_PATH#generated/}\" = \"$ILLUMINIS_PATH\"",
|
|
244
|
+
what: "Nothing under generated/ is edited by hand",
|
|
245
|
+
},
|
|
246
|
+
],
|
|
247
|
+
}, null, 2);
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { RunResult, guessTestCommand, stopEverything } from "./shell";
|
|
2
|
+
export { stopEverything, guessTestCommand };
|
|
3
|
+
export type { RunResult };
|
|
4
|
+
/** Everything a machine with a shell can do. Sent to the server on hello, and
|
|
5
|
+
* the catalog offered to the model is narrowed to it, so a client is never
|
|
6
|
+
* told about an action it cannot perform. */
|
|
7
|
+
export declare const NATIVE_CAPABILITIES: string[];
|
|
8
|
+
export interface ExecutorOptions {
|
|
9
|
+
root: string;
|
|
10
|
+
/** Called with each chunk of a running command's output, so a terminal can
|
|
11
|
+
* show it as it happens rather than in one block at the end. */
|
|
12
|
+
onOutput?: (chunk: string) => void;
|
|
13
|
+
}
|
|
14
|
+
export declare class NativeExecutor {
|
|
15
|
+
readonly capabilities: string[];
|
|
16
|
+
readonly root: string;
|
|
17
|
+
private readonly onOutput?;
|
|
18
|
+
/** The test command, once it has been worked out or supplied. Remembered so a
|
|
19
|
+
* job that had to be told does not have to be told again. */
|
|
20
|
+
private testCommand?;
|
|
21
|
+
/** What this project asks to happen around the agent's actions, read once. */
|
|
22
|
+
private hooks?;
|
|
23
|
+
/** Whether the last test run passed. The grading signal the whole product
|
|
24
|
+
* rests on, kept so a session can report it when the job ends. */
|
|
25
|
+
lastTestsPassed?: boolean;
|
|
26
|
+
constructor(opts: ExecutorOptions);
|
|
27
|
+
execute(name: string, a: Record<string, unknown>): Promise<RunResult>;
|
|
28
|
+
private perform;
|
|
29
|
+
}
|
|
@@ -0,0 +1,221 @@
|
|
|
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.NativeExecutor = exports.NATIVE_CAPABILITIES = exports.guessTestCommand = exports.stopEverything = void 0;
|
|
37
|
+
/**
|
|
38
|
+
* The executor: one implementation of every action, for every native client.
|
|
39
|
+
*
|
|
40
|
+
* Specification: docs/modules/CODING_AGENT_BUILD_SPECIFICATION.md §7.1 and
|
|
41
|
+
* register item E2.
|
|
42
|
+
*
|
|
43
|
+
* The terminal runs this today. **The editor extension and the desktop app
|
|
44
|
+
* bundle this same file rather than writing their own**, and that is the point
|
|
45
|
+
* of it existing separately from the command line interface around it. Three
|
|
46
|
+
* implementations of "read a file" would be three different answers to what a
|
|
47
|
+
* file contains, and the one that differs is always the one nobody is looking at.
|
|
48
|
+
*
|
|
49
|
+
* It decides nothing. Whether an action is allowed, which model asked for it and
|
|
50
|
+
* what it cost are all the server's business. This performs and reports.
|
|
51
|
+
*/
|
|
52
|
+
const fs = __importStar(require("fs"));
|
|
53
|
+
const files_1 = require("./files");
|
|
54
|
+
const documents_1 = require("./documents");
|
|
55
|
+
const hooks = __importStar(require("./hooks"));
|
|
56
|
+
const notebook_1 = require("./notebook");
|
|
57
|
+
const git_1 = require("./git");
|
|
58
|
+
const shell_1 = require("./shell");
|
|
59
|
+
Object.defineProperty(exports, "guessTestCommand", { enumerable: true, get: function () { return shell_1.guessTestCommand; } });
|
|
60
|
+
Object.defineProperty(exports, "stopEverything", { enumerable: true, get: function () { return shell_1.stopEverything; } });
|
|
61
|
+
/** Everything a machine with a shell can do. Sent to the server on hello, and
|
|
62
|
+
* the catalog offered to the model is narrowed to it, so a client is never
|
|
63
|
+
* told about an action it cannot perform. */
|
|
64
|
+
exports.NATIVE_CAPABILITIES = [
|
|
65
|
+
"read_file", "read_notebook", "read_pdf", "list_dir", "file_map", "glob", "grep",
|
|
66
|
+
"write_file", "edit_file", "multi_edit", "create_dir", "delete_file",
|
|
67
|
+
"move_file", "apply_patch",
|
|
68
|
+
// Bytes, for a document the server built. Never offered to the model; the
|
|
69
|
+
// loop issues it after `make_document`, so a deck lands in the folder the
|
|
70
|
+
// person is standing in rather than behind a link in a web page they are not
|
|
71
|
+
// looking at.
|
|
72
|
+
"write_bytes",
|
|
73
|
+
"run_command", "run_tests", "run_background", "read_output", "stop_process",
|
|
74
|
+
"install_dependency",
|
|
75
|
+
"git_status", "git_diff", "git_log", "git_branch", "git_commit",
|
|
76
|
+
// Pushing is here because it needs the repository. Pull requests, reviews,
|
|
77
|
+
// checks and releases are NOT: they need the tenant's credential, so they run
|
|
78
|
+
// on the server and are never offered to a client.
|
|
79
|
+
"git_push",
|
|
80
|
+
"todo_write",
|
|
81
|
+
];
|
|
82
|
+
/** How a package is added, per ecosystem.
|
|
83
|
+
*
|
|
84
|
+
* Named rather than passed through as a raw command, because "install a
|
|
85
|
+
* dependency" is a thing a person can meaningfully approve and
|
|
86
|
+
* `npm i --silent --prefix ../.. something` is not. The approval is only worth
|
|
87
|
+
* asking for if what is being approved is legible. */
|
|
88
|
+
function installCommand(root, pkg, manager, dev) {
|
|
89
|
+
const has = (f) => fs.existsSync(`${root}/${f}`);
|
|
90
|
+
const m = manager
|
|
91
|
+
|| (has("pnpm-lock.yaml") ? "pnpm" : has("yarn.lock") ? "yarn"
|
|
92
|
+
: has("package.json") ? "npm" : has("Cargo.toml") ? "cargo"
|
|
93
|
+
: has("go.mod") ? "go" : has("pyproject.toml") || has("requirements.txt") ? "pip" : "");
|
|
94
|
+
switch (m) {
|
|
95
|
+
case "npm": return `npm install ${dev ? "--save-dev " : ""}${pkg}`;
|
|
96
|
+
case "pnpm": return `pnpm add ${dev ? "-D " : ""}${pkg}`;
|
|
97
|
+
case "yarn": return `yarn add ${dev ? "-D " : ""}${pkg}`;
|
|
98
|
+
case "pip": return `python -m pip install ${pkg}`;
|
|
99
|
+
case "cargo": return `cargo add ${pkg}${dev ? " --dev" : ""}`;
|
|
100
|
+
case "go": return `go get ${pkg}`;
|
|
101
|
+
default: return null;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
class NativeExecutor {
|
|
105
|
+
capabilities = exports.NATIVE_CAPABILITIES;
|
|
106
|
+
root;
|
|
107
|
+
onOutput;
|
|
108
|
+
/** The test command, once it has been worked out or supplied. Remembered so a
|
|
109
|
+
* job that had to be told does not have to be told again. */
|
|
110
|
+
testCommand;
|
|
111
|
+
/** What this project asks to happen around the agent's actions, read once. */
|
|
112
|
+
hooks;
|
|
113
|
+
/** Whether the last test run passed. The grading signal the whole product
|
|
114
|
+
* rests on, kept so a session can report it when the job ends. */
|
|
115
|
+
lastTestsPassed;
|
|
116
|
+
constructor(opts) {
|
|
117
|
+
this.root = fs.realpathSync(opts.root);
|
|
118
|
+
this.onOutput = opts.onOutput;
|
|
119
|
+
}
|
|
120
|
+
async execute(name, a) {
|
|
121
|
+
// What this project asks to happen around the agent's actions. Read once
|
|
122
|
+
// and kept: reading the file on every action would mean a project could
|
|
123
|
+
// change its own rules mid job, which is not a property anybody wants.
|
|
124
|
+
if (this.hooks === undefined)
|
|
125
|
+
this.hooks = hooks.hooksFor(this.root);
|
|
126
|
+
// A `before` hook may REFUSE this action, and that is the one place a
|
|
127
|
+
// project's own script changes what happens. It can only ever refuse: there
|
|
128
|
+
// is no exit code that approves something the agent was not already allowed
|
|
129
|
+
// to do, because a permission system inside the repository would live
|
|
130
|
+
// exactly where an attacker who got that far already is.
|
|
131
|
+
const gate = await hooks.before(this.root, name, a, this.hooks);
|
|
132
|
+
if (gate.refusedBy) {
|
|
133
|
+
return { isError: true, content: gate.detail ?? "A check in this project refused it." };
|
|
134
|
+
}
|
|
135
|
+
const shell = {
|
|
136
|
+
root: this.root,
|
|
137
|
+
cwd: typeof a.cwd === "string" ? a.cwd : undefined,
|
|
138
|
+
timeoutMs: typeof a.timeout_s === "number" ? a.timeout_s * 1000 : undefined,
|
|
139
|
+
onOutput: this.onOutput,
|
|
140
|
+
};
|
|
141
|
+
try {
|
|
142
|
+
const done = await this.perform(name, a, shell);
|
|
143
|
+
// And afterwards. A failure here is reported and nothing else: the action
|
|
144
|
+
// has already happened, and the overwhelming case is a formatter, which is
|
|
145
|
+
// worth knowing about and not worth losing the work over.
|
|
146
|
+
if (!done.isError) {
|
|
147
|
+
const ran = await hooks.after(this.root, name, a, this.hooks);
|
|
148
|
+
if (ran.failed.length) {
|
|
149
|
+
return {
|
|
150
|
+
...done,
|
|
151
|
+
content: `${done.content}\n\n[This project's own checks reported: ${ran.failed.join("; ")}]`,
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
return done;
|
|
156
|
+
}
|
|
157
|
+
catch (err) {
|
|
158
|
+
// Never thrown to the loop. A failure here is a RESULT the model reads and
|
|
159
|
+
// works around; an exception would end a job over something it could have
|
|
160
|
+
// handled itself, and the person would lose everything already done.
|
|
161
|
+
return {
|
|
162
|
+
isError: true,
|
|
163
|
+
content: `This action could not be completed: ${err?.message ?? err}`,
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
async perform(name, a, shell) {
|
|
168
|
+
{
|
|
169
|
+
switch (name) {
|
|
170
|
+
case "run_command":
|
|
171
|
+
return await (0, shell_1.runCommand)(String(a.command ?? ""), shell);
|
|
172
|
+
case "run_tests": {
|
|
173
|
+
const r = await (0, shell_1.runTests)({
|
|
174
|
+
...shell,
|
|
175
|
+
command: a.command || this.testCommand,
|
|
176
|
+
path: a.path,
|
|
177
|
+
});
|
|
178
|
+
if (r.command)
|
|
179
|
+
this.testCommand = r.command;
|
|
180
|
+
if (typeof r.passed === "boolean")
|
|
181
|
+
this.lastTestsPassed = r.passed;
|
|
182
|
+
return r;
|
|
183
|
+
}
|
|
184
|
+
case "run_background":
|
|
185
|
+
return (0, shell_1.startBackground)(String(a.command ?? ""), shell);
|
|
186
|
+
case "read_output":
|
|
187
|
+
return (0, shell_1.readOutput)(String(a.process_id ?? ""));
|
|
188
|
+
case "stop_process":
|
|
189
|
+
return (0, shell_1.stopProcess)(String(a.process_id ?? ""));
|
|
190
|
+
case "install_dependency": {
|
|
191
|
+
const cmd = installCommand(this.root, String(a.package ?? ""), a.manager, Boolean(a.dev));
|
|
192
|
+
if (!cmd) {
|
|
193
|
+
return {
|
|
194
|
+
isError: true,
|
|
195
|
+
content: "I could not work out how this project installs packages. Tell " +
|
|
196
|
+
"me which package manager it uses.",
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
return await (0, shell_1.runCommand)(cmd, shell);
|
|
200
|
+
}
|
|
201
|
+
case "apply_patch":
|
|
202
|
+
return await (0, documents_1.applyPatch)(String(a.patch ?? ""), shell);
|
|
203
|
+
case "read_pdf":
|
|
204
|
+
return await (0, documents_1.readPdf)(this.root, String(a.path ?? ""), a.pages, shell);
|
|
205
|
+
case "read_notebook":
|
|
206
|
+
return (0, notebook_1.readNotebook)(this.root, String(a.path ?? ""), a.include_output !== false);
|
|
207
|
+
case "git_status": return await (0, git_1.gitStatus)(shell);
|
|
208
|
+
case "git_diff": return await (0, git_1.gitDiff)({ ...shell, path: a.path, staged: Boolean(a.staged) });
|
|
209
|
+
case "git_log": return await (0, git_1.gitLog)({ ...shell, path: a.path, limit: a.limit });
|
|
210
|
+
case "git_branch": return await (0, git_1.gitBranch)(String(a.name ?? ""), shell);
|
|
211
|
+
case "git_commit": return await (0, git_1.gitCommit)(String(a.message ?? ""), { ...shell, paths: a.paths });
|
|
212
|
+
case "git_push": return await (0, git_1.gitPush)(String(a.branch ?? ""), {
|
|
213
|
+
...shell, createRemote: Boolean(a.create_remote),
|
|
214
|
+
});
|
|
215
|
+
default:
|
|
216
|
+
return (0, files_1.performFileAction)(this.root, name, a);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
exports.NativeExecutor = NativeExecutor;
|
|
@@ -0,0 +1,147 @@
|
|
|
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.readNotebook = readNotebook;
|
|
37
|
+
/**
|
|
38
|
+
* Reading a Jupyter notebook.
|
|
39
|
+
*
|
|
40
|
+
* Specification: docs/modules/CODING_AGENT_BUILD_SPECIFICATION.md, register
|
|
41
|
+
* item D4.
|
|
42
|
+
*
|
|
43
|
+
* A notebook is JSON on disk, and reading it with `read_file` gives you that
|
|
44
|
+
* JSON: source split into one string per line, escaped newlines, and base64
|
|
45
|
+
* images that can run to hundreds of kilobytes. A model handed that spends a
|
|
46
|
+
* fortune reading almost nothing.
|
|
47
|
+
*
|
|
48
|
+
* So this reads the cells. Code, markdown, and what each cell actually printed,
|
|
49
|
+
* because in a notebook the output IS the state: a cell that raised is the
|
|
50
|
+
* whole reason somebody is looking, and a variable's value three cells up is
|
|
51
|
+
* often the only record of what the data looked like.
|
|
52
|
+
*
|
|
53
|
+
* **Images are named, never included.** A plot is the point of many cells and
|
|
54
|
+
* it is also a quarter of a megabyte of base64. The cell says a plot was
|
|
55
|
+
* produced, which is the useful fact, at a cost of nine words.
|
|
56
|
+
*/
|
|
57
|
+
const fs = __importStar(require("fs"));
|
|
58
|
+
const paths_1 = require("./paths");
|
|
59
|
+
/** What one notebook may contribute. Beyond this it is cut and says so. */
|
|
60
|
+
const MAX_CHARS = 60_000;
|
|
61
|
+
/** What one cell's output may contribute. A cell that printed a whole dataframe
|
|
62
|
+
* is common, and the first thirty lines of it say the same thing as all of it. */
|
|
63
|
+
const MAX_OUTPUT_CHARS = 2_000;
|
|
64
|
+
/** Source arrives as one string per line, with the newlines still on them. */
|
|
65
|
+
function text(source) {
|
|
66
|
+
if (Array.isArray(source))
|
|
67
|
+
return source.join("");
|
|
68
|
+
return String(source ?? "");
|
|
69
|
+
}
|
|
70
|
+
function outputOf(cell) {
|
|
71
|
+
const parts = [];
|
|
72
|
+
for (const out of cell.outputs ?? []) {
|
|
73
|
+
const kind = String(out.output_type ?? "");
|
|
74
|
+
if (kind === "stream") {
|
|
75
|
+
parts.push(text(out.text));
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
if (kind === "error") {
|
|
79
|
+
// The most valuable thing in the file. A traceback is usually the reason
|
|
80
|
+
// somebody opened the notebook at all, so it is never trimmed away first.
|
|
81
|
+
const name = String(out.ename ?? "error");
|
|
82
|
+
const message = String(out.evalue ?? "");
|
|
83
|
+
const trace = Array.isArray(out.traceback) ? out.traceback.join("\n") : "";
|
|
84
|
+
parts.push(`${name}: ${message}\n${trace}`);
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
if (kind === "execute_result" || kind === "display_data") {
|
|
88
|
+
const data = (out.data ?? {});
|
|
89
|
+
if (data["text/plain"]) {
|
|
90
|
+
parts.push(text(data["text/plain"]));
|
|
91
|
+
}
|
|
92
|
+
// Named, not included. A plot is the point of the cell and also a quarter
|
|
93
|
+
// of a megabyte of base64; "a plot was produced" is the useful fact.
|
|
94
|
+
for (const mime of Object.keys(data)) {
|
|
95
|
+
if (mime.startsWith("image/"))
|
|
96
|
+
parts.push(`[${mime} produced, not shown]`);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
const joined = parts.join("\n").trim();
|
|
101
|
+
if (joined.length <= MAX_OUTPUT_CHARS)
|
|
102
|
+
return joined;
|
|
103
|
+
return `${joined.slice(0, MAX_OUTPUT_CHARS)}\n[... output truncated]`;
|
|
104
|
+
}
|
|
105
|
+
function readNotebook(root, path, includeOutput = true) {
|
|
106
|
+
let raw;
|
|
107
|
+
try {
|
|
108
|
+
raw = fs.readFileSync((0, paths_1.resolveInside)(root, path, true), "utf8");
|
|
109
|
+
}
|
|
110
|
+
catch (err) {
|
|
111
|
+
return { isError: true, content: `Could not read ${path}: ${err.message}` };
|
|
112
|
+
}
|
|
113
|
+
let parsed;
|
|
114
|
+
try {
|
|
115
|
+
parsed = JSON.parse(raw);
|
|
116
|
+
}
|
|
117
|
+
catch {
|
|
118
|
+
return {
|
|
119
|
+
isError: true,
|
|
120
|
+
content: `${path} is not a readable notebook. It may be corrupt, or it may not be a notebook at all.`,
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
const cells = parsed.cells ?? [];
|
|
124
|
+
if (!cells.length) {
|
|
125
|
+
return { content: `${path} has no cells in it.`, summary: "empty notebook" };
|
|
126
|
+
}
|
|
127
|
+
const lines = [`${path}: ${cells.length} cells`];
|
|
128
|
+
let used = lines[0].length;
|
|
129
|
+
for (let i = 0; i < cells.length; i += 1) {
|
|
130
|
+
const cell = cells[i];
|
|
131
|
+
const kind = cell.cell_type ?? "code";
|
|
132
|
+
// Numbered, because that is how a person refers to a cell and how an edit
|
|
133
|
+
// will have to name one later.
|
|
134
|
+
const header = `\n--- cell ${i + 1} (${kind}) ---`;
|
|
135
|
+
const source = text(cell.source).trimEnd();
|
|
136
|
+
const output = includeOutput && kind === "code" ? outputOf(cell) : "";
|
|
137
|
+
const block = [header, source, output ? `\n[output]\n${output}` : ""]
|
|
138
|
+
.filter(Boolean).join("\n");
|
|
139
|
+
if (used + block.length > MAX_CHARS) {
|
|
140
|
+
lines.push(`\n[... ${cells.length - i} more cells not read. Ask for a range if you need them.]`);
|
|
141
|
+
break;
|
|
142
|
+
}
|
|
143
|
+
lines.push(block);
|
|
144
|
+
used += block.length;
|
|
145
|
+
}
|
|
146
|
+
return { content: lines.join("\n"), summary: `read ${path} (${cells.length} cells)` };
|
|
147
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export declare class OutsideWorkspace extends Error {
|
|
2
|
+
readonly attempted: string;
|
|
3
|
+
constructor(attempted: string);
|
|
4
|
+
}
|
|
5
|
+
export declare function isSecret(p: string): boolean;
|
|
6
|
+
/**
|
|
7
|
+
* An absolute path inside the workspace, or a refusal.
|
|
8
|
+
*
|
|
9
|
+
* `mustExist` is false when the caller is about to CREATE the thing. A file that
|
|
10
|
+
* does not exist yet cannot be resolved, so the check is applied to the deepest
|
|
11
|
+
* parent that does exist: creating `src/new/deep/file.ts` is inside the project
|
|
12
|
+
* exactly when `src` is, and demanding the leaf exist first would make it
|
|
13
|
+
* impossible to write a new file at all.
|
|
14
|
+
*/
|
|
15
|
+
export declare function resolveInside(root: string, rel: string, mustExist?: boolean): string;
|