@dynamicagents/plugins 0.13.0 → 0.15.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +14 -14
- package/dist/claude-code/cards.d.ts +4 -0
- package/dist/claude-code/cards.js +22 -0
- package/dist/claude-code/config.d.ts +10 -83
- package/dist/claude-code/config.js +0 -2
- package/dist/claude-code/credentials.d.ts +32 -50
- package/dist/claude-code/credentials.js +115 -74
- package/dist/claude-code/daemon/index.d.ts +24 -0
- package/dist/claude-code/daemon/index.js +203 -0
- package/dist/claude-code/egress.d.ts +35 -117
- package/dist/claude-code/egress.js +80 -325
- package/dist/claude-code/events.d.ts +14 -17
- package/dist/claude-code/events.js +19 -15
- package/dist/claude-code/harness-wire.d.ts +45 -0
- package/dist/claude-code/harness-wire.js +47 -0
- package/dist/claude-code/index.d.ts +19 -29
- package/dist/claude-code/index.js +17 -25
- package/dist/claude-code/language-model.d.ts +19 -0
- package/dist/claude-code/language-model.js +71 -0
- package/dist/claude-code/model.d.ts +77 -70
- package/dist/claude-code/model.js +328 -304
- package/dist/claude-code/placeholder.d.ts +12 -0
- package/dist/claude-code/placeholder.js +12 -0
- package/dist/claude-code/spec.d.ts +4 -4
- package/dist/claude-code/spec.js +4 -4
- package/dist/claude-code/verified.d.ts +1 -1
- package/dist/claude-code/verified.js +1 -1
- package/dist/computer/client.d.ts +88 -0
- package/dist/computer/client.js +6 -0
- package/dist/computer/context.d.ts +7 -7
- package/dist/computer/context.js +14 -18
- package/dist/computer/gate.d.ts +4 -23
- package/dist/computer/gate.js +1 -37
- package/dist/computer/index.d.ts +44 -43
- package/dist/computer/index.js +29 -50
- package/dist/computer/paths.d.ts +5 -27
- package/dist/computer/paths.js +7 -30
- package/dist/computer/proxy.d.ts +17 -31
- package/dist/computer/proxy.js +9 -44
- package/dist/computer/render.d.ts +3 -40
- package/dist/computer/render.js +3 -36
- package/dist/computer/sandbox.d.ts +55 -0
- package/dist/computer/sandbox.js +236 -0
- package/dist/computer/shell.d.ts +36 -0
- package/dist/computer/shell.js +41 -0
- package/dist/computer/tools-exec.js +3 -9
- package/dist/computer/tools-file.d.ts +1 -1
- package/dist/computer/tools-file.js +1 -1
- package/dist/{repo → repo-github}/checkout.d.ts +3 -4
- package/dist/{repo → repo-github}/checkout.js +3 -4
- package/dist/{repo → repo-github}/context.d.ts +14 -32
- package/dist/{repo → repo-github}/context.js +46 -81
- package/dist/{repo → repo-github}/index.d.ts +30 -13
- package/dist/{repo → repo-github}/index.js +25 -6
- package/dist/repo-github/mcp.d.ts +95 -0
- package/dist/repo-github/mcp.js +107 -0
- package/dist/repo-github/sandbox-egress.d.ts +68 -0
- package/dist/repo-github/sandbox-egress.js +272 -0
- package/dist/repo-github/sandbox-git.d.ts +30 -0
- package/dist/repo-github/sandbox-git.js +113 -0
- package/dist/{repo → repo-github}/tools-checks.js +43 -4
- package/dist/{repo → repo-github}/tools-clone.js +2 -21
- package/dist/{repo → repo-github}/tools-pr.d.ts +0 -1
- package/dist/{repo → repo-github}/tools-pr.js +85 -20
- package/dist/{repo → repo-github}/tools-review.d.ts +6 -0
- package/dist/{repo → repo-github}/tools-review.js +62 -176
- package/dist/{repo → repo-github}/url.d.ts +11 -0
- package/dist/{repo → repo-github}/url.js +12 -1
- package/dist/{workspace → sandbox}/advisory.d.ts +16 -35
- package/dist/{workspace → sandbox}/advisory.js +2 -29
- package/dist/sandbox/agent-exec.d.ts +31 -0
- package/dist/sandbox/agent-exec.js +62 -0
- package/dist/sandbox/ca-trust.d.ts +9 -0
- package/dist/sandbox/ca-trust.js +21 -0
- package/dist/sandbox/constants.d.ts +22 -0
- package/dist/sandbox/constants.js +22 -0
- package/dist/sandbox/daemon/composite.d.ts +9 -0
- package/dist/sandbox/daemon/composite.js +35 -0
- package/dist/sandbox/daemon/index.d.ts +47 -0
- package/dist/sandbox/daemon/index.js +116 -0
- package/dist/sandbox/egress.d.ts +81 -0
- package/dist/sandbox/egress.js +109 -0
- package/dist/sandbox/exec.d.ts +57 -0
- package/dist/sandbox/exec.js +52 -0
- package/dist/sandbox/format.d.ts +2 -0
- package/dist/sandbox/format.js +7 -0
- package/dist/sandbox/header.d.ts +50 -0
- package/dist/sandbox/header.js +94 -0
- package/dist/sandbox/index.d.ts +17 -0
- package/dist/sandbox/index.js +17 -0
- package/dist/{workspace/install.d.ts → sandbox/install-plan.d.ts} +10 -1
- package/dist/{workspace/install.js → sandbox/install-plan.js} +22 -4
- package/dist/sandbox/name.d.ts +27 -0
- package/dist/sandbox/name.js +32 -0
- package/dist/sandbox/object.d.ts +244 -0
- package/dist/sandbox/object.js +1139 -0
- package/dist/scratch/index.d.ts +1 -9
- package/dist/scratch/index.js +2 -18
- package/package.json +26 -22
- package/dist/claude-code/github.d.ts +0 -39
- package/dist/claude-code/github.js +0 -82
- package/dist/claude-code/run.d.ts +0 -474
- package/dist/claude-code/run.js +0 -901
- package/dist/claude-code/session.d.ts +0 -131
- package/dist/claude-code/session.js +0 -285
- package/dist/workspace/ca-trust.d.ts +0 -95
- package/dist/workspace/ca-trust.js +0 -312
- package/dist/workspace/container-deps.d.ts +0 -54
- package/dist/workspace/container-deps.js +0 -224
- package/dist/workspace/container-fault.d.ts +0 -38
- package/dist/workspace/container-fault.js +0 -73
- package/dist/workspace/exec.d.ts +0 -94
- package/dist/workspace/exec.js +0 -153
- package/dist/workspace/format.d.ts +0 -36
- package/dist/workspace/format.js +0 -57
- package/dist/workspace/git-host.d.ts +0 -79
- package/dist/workspace/git-host.js +0 -226
- package/dist/workspace/git-identity.d.ts +0 -57
- package/dist/workspace/git-identity.js +0 -147
- package/dist/workspace/index.d.ts +0 -49
- package/dist/workspace/index.js +0 -58
- package/dist/workspace/install-job.d.ts +0 -121
- package/dist/workspace/install-job.js +0 -866
- package/dist/workspace/object.d.ts +0 -502
- package/dist/workspace/object.js +0 -1427
- package/dist/workspace/open.d.ts +0 -95
- package/dist/workspace/open.js +0 -63
- package/dist/workspace/read.d.ts +0 -16
- package/dist/workspace/read.js +0 -22
- package/dist/workspace/shell.d.ts +0 -45
- package/dist/workspace/shell.js +0 -80
- package/dist/workspace/sync.d.ts +0 -111
- package/dist/workspace/sync.js +0 -124
- package/dist/workspace/wake.d.ts +0 -19
- package/dist/workspace/wake.js +0 -10
- /package/dist/{repo → repo-github}/tools-checks.d.ts +0 -0
- /package/dist/{repo → repo-github}/tools-clone.d.ts +0 -0
- /package/dist/{repo → repo-github}/tools-worktree.d.ts +0 -0
- /package/dist/{repo → repo-github}/tools-worktree.js +0 -0
- /package/dist/{repo → repo-github}/tools-worktrees.d.ts +0 -0
- /package/dist/{repo → repo-github}/tools-worktrees.js +0 -0
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
import { RUN_DIR } from "../sandbox/constants.js";
|
|
2
|
+
import { sandboxNameFromRuntime } from "../sandbox/name.js";
|
|
3
|
+
import { routedWorkspace } from "./proxy.js";
|
|
4
|
+
/** After `timeout`'s TERM, how long before its KILL. */
|
|
5
|
+
const KILL_AFTER_S = 5;
|
|
6
|
+
/** `timeout`'s exits for a command it ended: TERM taken, or KILL after it was not. */
|
|
7
|
+
const TIMED_OUT = new Set([124, 137]);
|
|
8
|
+
/**
|
|
9
|
+
* A command's processes carry a marker, for `kill` to find them by — through
|
|
10
|
+
* the launch and after it, since `exec` keeps it in every command line.
|
|
11
|
+
* `timeout` ends the command where it runs at its limit, which a Worker-side
|
|
12
|
+
* signal cannot.
|
|
13
|
+
*/
|
|
14
|
+
const MARKER = "da-exec-";
|
|
15
|
+
const CANCELLED = `${RUN_DIR}/cancelled`;
|
|
16
|
+
/**
|
|
17
|
+
* The launch and `kill` are a handshake, because a command can still be on its
|
|
18
|
+
* way in — starting the container — when it is cancelled: `kill` marks the
|
|
19
|
+
* command cancelled before it signals, and the launch checks the mark before
|
|
20
|
+
* it starts. Either the command never starts, or it is running when `kill`
|
|
21
|
+
* signals it.
|
|
22
|
+
*/
|
|
23
|
+
const LAUNCH = [
|
|
24
|
+
`[ -e "${CANCELLED}/$0" ] && exit 130`,
|
|
25
|
+
`exec timeout --kill-after=${KILL_AFTER_S} "$2" sh -c "$1" "$0"`
|
|
26
|
+
].join("\n");
|
|
27
|
+
/** The marker arrives in two halves, so this script's own command line is not one `pkill` matches. */
|
|
28
|
+
const CANCEL = [
|
|
29
|
+
'm="$1$2"',
|
|
30
|
+
`mkdir -p ${CANCELLED} && : > "${CANCELLED}/$m"`,
|
|
31
|
+
'pkill -TERM -f "$m"',
|
|
32
|
+
"exit 0"
|
|
33
|
+
].join("\n");
|
|
34
|
+
/** What one `find` or `grep` returns at most, with no `limit` of its own. */
|
|
35
|
+
const MAX_FOUND = 10_000;
|
|
36
|
+
export function computerWorkspace(config, runtime) {
|
|
37
|
+
const host = (name) => config.binding.getByName(name);
|
|
38
|
+
return routedWorkspace({
|
|
39
|
+
name: () => sandboxNameFromRuntime(runtime?.()) ?? config.sandboxName(),
|
|
40
|
+
open: async (name) => sandboxClient(host(name)),
|
|
41
|
+
advisories: (name) => host(name).advisories()
|
|
42
|
+
}, config.cwd ?? "/workspace");
|
|
43
|
+
}
|
|
44
|
+
function missing(path) {
|
|
45
|
+
return Object.assign(new Error(`ENOENT: no such file or directory, ${path}`), { code: "ENOENT" });
|
|
46
|
+
}
|
|
47
|
+
/** The computer tools' client over one sandbox. */
|
|
48
|
+
export function sandboxClient(host) {
|
|
49
|
+
const readFile = async (path, encoding) => {
|
|
50
|
+
const bytes = await host.readFile(path);
|
|
51
|
+
if (bytes === null)
|
|
52
|
+
throw missing(path);
|
|
53
|
+
return encoding === "utf8"
|
|
54
|
+
? new TextDecoder().decode(bytes)
|
|
55
|
+
: new Blob([bytes]).stream();
|
|
56
|
+
};
|
|
57
|
+
const fs = {
|
|
58
|
+
readFile: readFile,
|
|
59
|
+
async stat(path) {
|
|
60
|
+
const stat = await host.stat(path);
|
|
61
|
+
if (!stat)
|
|
62
|
+
throw missing(path);
|
|
63
|
+
return {
|
|
64
|
+
size: stat.size,
|
|
65
|
+
mtime: stat.modifiedAt,
|
|
66
|
+
isFile: stat.type === "file",
|
|
67
|
+
isDirectory: stat.type === "directory",
|
|
68
|
+
isSymbolicLink: stat.type === "symlink"
|
|
69
|
+
};
|
|
70
|
+
},
|
|
71
|
+
async readdir(path, options = {}) {
|
|
72
|
+
const from = options.offset ?? 0;
|
|
73
|
+
return (await host.readDir(path))
|
|
74
|
+
.slice(from, options.limit === undefined ? undefined : from + options.limit)
|
|
75
|
+
.map((entry) => ({
|
|
76
|
+
name: entry.name,
|
|
77
|
+
isFile: entry.type === "file",
|
|
78
|
+
isDirectory: entry.type === "directory"
|
|
79
|
+
}));
|
|
80
|
+
},
|
|
81
|
+
find: (directory, pattern, options = {}) => find(host, directory, pattern, options),
|
|
82
|
+
grep: (pattern, path, options = {}) => grep(host, pattern, path, options),
|
|
83
|
+
writeFile: (path, content) => host.writeFile(path, content),
|
|
84
|
+
mkdir: (path) => host.mkdir(path),
|
|
85
|
+
rm: (path, options = {}) => host.rm(path, options.recursive ? { recursive: true } : {})
|
|
86
|
+
};
|
|
87
|
+
return {
|
|
88
|
+
fs,
|
|
89
|
+
runtime: {
|
|
90
|
+
async exec(command, options) {
|
|
91
|
+
const id = crypto.randomUUID();
|
|
92
|
+
const marker = `${MARKER}${id}`;
|
|
93
|
+
const startedAt = Date.now();
|
|
94
|
+
let killed = false;
|
|
95
|
+
const running = host.exec([
|
|
96
|
+
"sh",
|
|
97
|
+
"-c",
|
|
98
|
+
LAUNCH,
|
|
99
|
+
marker,
|
|
100
|
+
command,
|
|
101
|
+
// At least a second: `timeout 0` is no limit at all.
|
|
102
|
+
String(Math.max(1, Math.ceil(options.timeoutMs / 1000)))
|
|
103
|
+
], {
|
|
104
|
+
cwd: options.cwd,
|
|
105
|
+
...(options.env ? { env: options.env } : {}),
|
|
106
|
+
timeoutMs: options.timeoutMs + (KILL_AFTER_S + 5) * 1000
|
|
107
|
+
});
|
|
108
|
+
running.catch(() => undefined);
|
|
109
|
+
return {
|
|
110
|
+
async result() {
|
|
111
|
+
const result = await running;
|
|
112
|
+
const timedOut = TIMED_OUT.has(result.exitCode) &&
|
|
113
|
+
Date.now() - startedAt >= options.timeoutMs;
|
|
114
|
+
return {
|
|
115
|
+
...result,
|
|
116
|
+
status: killed || timedOut
|
|
117
|
+
? "cancelled"
|
|
118
|
+
: "completed"
|
|
119
|
+
};
|
|
120
|
+
},
|
|
121
|
+
async kill() {
|
|
122
|
+
killed = true;
|
|
123
|
+
await host.exec(["sh", "-c", CANCEL, "da-cancel", MARKER, id]);
|
|
124
|
+
},
|
|
125
|
+
[Symbol.dispose]() { }
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
},
|
|
129
|
+
[Symbol.dispose]() { }
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* `rg` over `target`, from where its globs anchor: a directory's own, with
|
|
134
|
+
* `include` applied, or a file's parent. The script prints that directory
|
|
135
|
+
* first, then `rg`'s output cut at `lines`, so a search of a whole checkout
|
|
136
|
+
* returns what the tool can show rather than everything.
|
|
137
|
+
*/
|
|
138
|
+
const RG_SCRIPT = [
|
|
139
|
+
't="$1"; include="$2"; lines="$3"; shift 3',
|
|
140
|
+
'[ -e "$t" ] || { echo "ENOENT" >&2; exit 2; }',
|
|
141
|
+
'if [ -d "$t" ]; then cd "$t" || exit 2; a=.',
|
|
142
|
+
' if [ -n "$include" ]; then set -- -g "/$include" "$@"; fi',
|
|
143
|
+
'else cd "$(dirname "$t")" || exit 2; a="./$(basename "$t")"; fi',
|
|
144
|
+
"pwd",
|
|
145
|
+
'rg "$@" -- "$a" < /dev/null | head -n "$lines"'
|
|
146
|
+
].join("\n");
|
|
147
|
+
async function rg(host, target, include, args, lines) {
|
|
148
|
+
const run = await host.exec([
|
|
149
|
+
"sh",
|
|
150
|
+
"-c",
|
|
151
|
+
RG_SCRIPT,
|
|
152
|
+
"rg",
|
|
153
|
+
target,
|
|
154
|
+
(include ?? "").replace(/^\/+/, ""),
|
|
155
|
+
String(lines),
|
|
156
|
+
...args
|
|
157
|
+
]);
|
|
158
|
+
if (run.exitCode === 2 && /ENOENT/.test(run.stderr))
|
|
159
|
+
throw missing(target);
|
|
160
|
+
const [cwd = target, ...out] = run.stdout.split("\n");
|
|
161
|
+
if (out.every((line) => line === "") && run.stderr.trim() !== "")
|
|
162
|
+
throw new Error(run.stderr.trim());
|
|
163
|
+
return { cwd, out: out.filter(Boolean) };
|
|
164
|
+
}
|
|
165
|
+
function absolute(cwd, printed) {
|
|
166
|
+
const rest = printed.replace(/^\.\//, "");
|
|
167
|
+
return cwd === "/" ? `/${rest}` : `${cwd}/${rest}`;
|
|
168
|
+
}
|
|
169
|
+
/** Hidden and ignored files too, as the old walk read them; `exclude` pruned. */
|
|
170
|
+
function scope(exclude) {
|
|
171
|
+
return [
|
|
172
|
+
"--hidden",
|
|
173
|
+
"--no-ignore",
|
|
174
|
+
"--sort",
|
|
175
|
+
"path",
|
|
176
|
+
...(exclude ?? []).flatMap((glob) => ["-g", `!${glob}`])
|
|
177
|
+
];
|
|
178
|
+
}
|
|
179
|
+
async function find(host, directory, pattern, options) {
|
|
180
|
+
const from = options.offset ?? 0;
|
|
181
|
+
const take = options.limit ?? MAX_FOUND;
|
|
182
|
+
const { cwd, out } = await rg(host, directory, pattern, ["--files", ...scope(options.exclude)], Math.min(from + take, MAX_FOUND));
|
|
183
|
+
// Files only: `rg --files` lists no directories.
|
|
184
|
+
return out
|
|
185
|
+
.slice(from, from + take)
|
|
186
|
+
.map((path) => ({ path: absolute(cwd, path), type: "file" }));
|
|
187
|
+
}
|
|
188
|
+
async function grep(host, pattern, path, options) {
|
|
189
|
+
const around = options.context ?? 0;
|
|
190
|
+
const from = options.offset ?? 0;
|
|
191
|
+
const take = options.limit ?? MAX_FOUND;
|
|
192
|
+
const { cwd, out } = await rg(host, path, options.include, [
|
|
193
|
+
"--json",
|
|
194
|
+
...scope(options.exclude),
|
|
195
|
+
...(options.regex ? [] : ["--fixed-strings"]),
|
|
196
|
+
...(options.ignoreCase ? ["--ignore-case"] : []),
|
|
197
|
+
...(around > 0 ? ["--context", String(around)] : []),
|
|
198
|
+
"-e",
|
|
199
|
+
pattern
|
|
200
|
+
],
|
|
201
|
+
// Enough for every match it can return to arrive whole: each brings at
|
|
202
|
+
// most its context, and opens and closes a file.
|
|
203
|
+
(from + take) * (2 * around + 3) + 16);
|
|
204
|
+
// Per file, every line rg printed, so each match takes its context from them.
|
|
205
|
+
const files = new Map();
|
|
206
|
+
const order = [];
|
|
207
|
+
for (const raw of out) {
|
|
208
|
+
let entry;
|
|
209
|
+
try {
|
|
210
|
+
entry = JSON.parse(raw);
|
|
211
|
+
}
|
|
212
|
+
catch {
|
|
213
|
+
continue;
|
|
214
|
+
}
|
|
215
|
+
if (entry.type !== "match" && entry.type !== "context")
|
|
216
|
+
continue;
|
|
217
|
+
const file = absolute(cwd, entry.data?.path?.text ?? "");
|
|
218
|
+
const line = entry.data?.line_number ?? 0;
|
|
219
|
+
const text = (entry.data?.lines?.text ?? "").replace(/\r?\n$/, "");
|
|
220
|
+
const lines = files.get(file) ?? [];
|
|
221
|
+
lines.push({ line, text, isMatch: entry.type === "match" });
|
|
222
|
+
files.set(file, lines);
|
|
223
|
+
if (entry.type === "match")
|
|
224
|
+
order.push({ file, line, text });
|
|
225
|
+
}
|
|
226
|
+
return order.slice(from, from + take).map(({ file, line, text }) => ({
|
|
227
|
+
path: file,
|
|
228
|
+
line,
|
|
229
|
+
text,
|
|
230
|
+
...(around > 0
|
|
231
|
+
? {
|
|
232
|
+
context: (files.get(file) ?? []).filter((other) => Math.abs(other.line - line) <= around)
|
|
233
|
+
}
|
|
234
|
+
: {})
|
|
235
|
+
}));
|
|
236
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Run a command under {@link file://./index.ts ComputerToolsConfig.shell} with its
|
|
3
|
+
* two output streams merged into one transcript, in the order they were
|
|
4
|
+
* written, or hand it back untouched when no shell is configured.
|
|
5
|
+
*
|
|
6
|
+
* `bash`'s consumer is **a model reading output**. A project's check is a chain
|
|
7
|
+
* — `wrangler types && prettier && eslint && tsc` — and *which tool spoke last*
|
|
8
|
+
* is how you know which one failed. A stdout block and a separate stderr block
|
|
9
|
+
* destroy that ordering, and a model that wants it back re-runs the whole gate
|
|
10
|
+
* as `npm run check > /tmp/out 2>&1; cat /tmp/out`. This is that workaround,
|
|
11
|
+
* done once, for free.
|
|
12
|
+
*
|
|
13
|
+
* One quoted argument, not string concatenation: the command is model-authored
|
|
14
|
+
* and routinely contains quotes of its own (`git commit -m "…"`), so anything
|
|
15
|
+
* less than `shellQuote` re-parses the model's quoting and mangles it. The
|
|
16
|
+
* redirect binds to the wrapper process, so it applies to everything the
|
|
17
|
+
* command writes however deeply nested.
|
|
18
|
+
*
|
|
19
|
+
* ## Why `-o pipefail`
|
|
20
|
+
*
|
|
21
|
+
* Without it a pipeline's exit status is its **last** stage's, so
|
|
22
|
+
* `npm run check | tail -100` reports `exit 0` for a gate that failed outright —
|
|
23
|
+
* and models pipe into `tail` constantly. A tool that reports success for a
|
|
24
|
+
* failed build is worse than one that reports nothing.
|
|
25
|
+
*
|
|
26
|
+
* The trade is worth stating. `pipefail` surfaces SIGPIPE too, so
|
|
27
|
+
* `ls big-dir | head -1` reports 141 rather than 0 — noisy, but *visible*. The
|
|
28
|
+
* tool description tells the model not to pipe into `head`/`tail` at all, since
|
|
29
|
+
* output is already truncated with both ends kept. It needs a shell that
|
|
30
|
+
* implements it — `bash`, `zsh`, `ksh`, not `sh`/dash.
|
|
31
|
+
*
|
|
32
|
+
* With no shell configured the two streams arrive separate, and
|
|
33
|
+
* {@link file://./render.ts renderResult} renders them as a labelled
|
|
34
|
+
* `--- stderr ---` block.
|
|
35
|
+
*/
|
|
36
|
+
export declare function withShellTranscript(command: string, shell: string | undefined): string;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { shellQuote } from "../sandbox/agent-exec.js";
|
|
2
|
+
/**
|
|
3
|
+
* Run a command under {@link file://./index.ts ComputerToolsConfig.shell} with its
|
|
4
|
+
* two output streams merged into one transcript, in the order they were
|
|
5
|
+
* written, or hand it back untouched when no shell is configured.
|
|
6
|
+
*
|
|
7
|
+
* `bash`'s consumer is **a model reading output**. A project's check is a chain
|
|
8
|
+
* — `wrangler types && prettier && eslint && tsc` — and *which tool spoke last*
|
|
9
|
+
* is how you know which one failed. A stdout block and a separate stderr block
|
|
10
|
+
* destroy that ordering, and a model that wants it back re-runs the whole gate
|
|
11
|
+
* as `npm run check > /tmp/out 2>&1; cat /tmp/out`. This is that workaround,
|
|
12
|
+
* done once, for free.
|
|
13
|
+
*
|
|
14
|
+
* One quoted argument, not string concatenation: the command is model-authored
|
|
15
|
+
* and routinely contains quotes of its own (`git commit -m "…"`), so anything
|
|
16
|
+
* less than `shellQuote` re-parses the model's quoting and mangles it. The
|
|
17
|
+
* redirect binds to the wrapper process, so it applies to everything the
|
|
18
|
+
* command writes however deeply nested.
|
|
19
|
+
*
|
|
20
|
+
* ## Why `-o pipefail`
|
|
21
|
+
*
|
|
22
|
+
* Without it a pipeline's exit status is its **last** stage's, so
|
|
23
|
+
* `npm run check | tail -100` reports `exit 0` for a gate that failed outright —
|
|
24
|
+
* and models pipe into `tail` constantly. A tool that reports success for a
|
|
25
|
+
* failed build is worse than one that reports nothing.
|
|
26
|
+
*
|
|
27
|
+
* The trade is worth stating. `pipefail` surfaces SIGPIPE too, so
|
|
28
|
+
* `ls big-dir | head -1` reports 141 rather than 0 — noisy, but *visible*. The
|
|
29
|
+
* tool description tells the model not to pipe into `head`/`tail` at all, since
|
|
30
|
+
* output is already truncated with both ends kept. It needs a shell that
|
|
31
|
+
* implements it — `bash`, `zsh`, `ksh`, not `sh`/dash.
|
|
32
|
+
*
|
|
33
|
+
* With no shell configured the two streams arrive separate, and
|
|
34
|
+
* {@link file://./render.ts renderResult} renders them as a labelled
|
|
35
|
+
* `--- stderr ---` block.
|
|
36
|
+
*/
|
|
37
|
+
export function withShellTranscript(command, shell) {
|
|
38
|
+
return shell
|
|
39
|
+
? `${shell} -o pipefail -c ${shellQuote(command)} 2>&1`
|
|
40
|
+
: command;
|
|
41
|
+
}
|
|
@@ -54,8 +54,7 @@ import { tool } from "ai";
|
|
|
54
54
|
import { z } from "zod";
|
|
55
55
|
import { withAbort } from "@dynamicagents/core";
|
|
56
56
|
import { renderResult } from "./render.js";
|
|
57
|
-
import {
|
|
58
|
-
import { withShellTranscript } from "../workspace/shell.js";
|
|
57
|
+
import { withShellTranscript } from "./shell.js";
|
|
59
58
|
/** Running one command in the container. */
|
|
60
59
|
/**
|
|
61
60
|
* Wait for a disposable resource without outliving `signal`.
|
|
@@ -96,7 +95,7 @@ export function execTools(ctx) {
|
|
|
96
95
|
*
|
|
97
96
|
* Two of these sentences are only true when a shell is configured, so the
|
|
98
97
|
* description says whichever is. `withShellTranscript` is a no-op without
|
|
99
|
-
* {@link file://./index.ts
|
|
98
|
+
* {@link file://./index.ts ComputerToolsConfig.shell}: no `2>&1`, so the
|
|
100
99
|
* streams arrive separate and {@link renderResult} labels them; no
|
|
101
100
|
* `-o pipefail`, so `/bin/sh` reports a pipeline's *last* stage. Promising
|
|
102
101
|
* a transcript and first-failure semantics there would be the lie
|
|
@@ -246,21 +245,16 @@ export function execTools(ctx) {
|
|
|
246
245
|
catch (err) {
|
|
247
246
|
if (abortSignal?.aborted)
|
|
248
247
|
return note(stopped(gateMsWaited, started));
|
|
249
|
-
const lost = execLostNote(err);
|
|
250
248
|
console.warn("[computer] bash failed", {
|
|
251
249
|
command,
|
|
252
250
|
gateMs: gateMsWaited,
|
|
253
251
|
durationMs: Date.now() - startedAtMs - gateMsWaited,
|
|
254
252
|
timeoutMs,
|
|
255
|
-
// Distinguished in the log for the same reason it is distinguished
|
|
256
|
-
// for the model: a container replacement and a broken command want
|
|
257
|
-
// different people looking at them.
|
|
258
|
-
...(lost ? { lost: true } : {}),
|
|
259
253
|
err: String(err)
|
|
260
254
|
});
|
|
261
255
|
// Returned, not thrown: a failed command is usually the model's to
|
|
262
256
|
// recover from, and it can only recover from what it is told.
|
|
263
|
-
return note(
|
|
257
|
+
return note(`error running command: ${String(err)}`);
|
|
264
258
|
}
|
|
265
259
|
}
|
|
266
260
|
})
|
|
@@ -3,7 +3,7 @@ import type { ComputerContext } from "./context.js";
|
|
|
3
3
|
/**
|
|
4
4
|
* Editing a file in place — the one file tool this plugin keeps. Think's
|
|
5
5
|
* `read`, `write` and `delete` reach the workspace through the agent's own
|
|
6
|
-
* `this.workspace`, which is `computerWorkspace` (`./
|
|
6
|
+
* `this.workspace`, which is `computerWorkspace` (`./sandbox.ts`).
|
|
7
7
|
*
|
|
8
8
|
* Not Think's `edit`, because that one is two calls through the workspace — a
|
|
9
9
|
* `readFile`, then a `writeFile` — and a lock inside the workspace cannot span
|
|
@@ -5,7 +5,7 @@ import { withFileLock } from "./file-lock.js";
|
|
|
5
5
|
/**
|
|
6
6
|
* Editing a file in place — the one file tool this plugin keeps. Think's
|
|
7
7
|
* `read`, `write` and `delete` reach the workspace through the agent's own
|
|
8
|
-
* `this.workspace`, which is `computerWorkspace` (`./
|
|
8
|
+
* `this.workspace`, which is `computerWorkspace` (`./sandbox.ts`).
|
|
9
9
|
*
|
|
10
10
|
* Not Think's `edit`, because that one is two calls through the workspace — a
|
|
11
11
|
* `readFile`, then a `writeFile` — and a lock inside the workspace cannot span
|
|
@@ -76,10 +76,9 @@ export declare function resolveDefaultBranch(plain: GitRunners["plain"], dir: st
|
|
|
76
76
|
* It is the *nearest* answer, not the only one. `repo_commit` names the identity
|
|
77
77
|
* on the commit itself, which is the one layer a stale config cannot outrank.
|
|
78
78
|
* Underneath, a repository this plugin never cloned falls back to whatever the
|
|
79
|
-
* shell's own git is configured with
|
|
80
|
-
*
|
|
81
|
-
*
|
|
82
|
-
* runner, so that is a property of a deployment, not of this plugin.
|
|
79
|
+
* shell's own git is configured with, or the environment a deployment gives
|
|
80
|
+
* its commands. `exec` is an arbitrary runner, so that is a property of a
|
|
81
|
+
* deployment, not of this plugin.
|
|
83
82
|
*
|
|
84
83
|
* Unchecked, like the config pins at the clone: git's identity is needed by the
|
|
85
84
|
* first commit, and a failure to write it fails that commit with a sentence far
|
|
@@ -33,10 +33,9 @@ export async function resolveDefaultBranch(plain, dir) {
|
|
|
33
33
|
* It is the *nearest* answer, not the only one. `repo_commit` names the identity
|
|
34
34
|
* on the commit itself, which is the one layer a stale config cannot outrank.
|
|
35
35
|
* Underneath, a repository this plugin never cloned falls back to whatever the
|
|
36
|
-
* shell's own git is configured with
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
* runner, so that is a property of a deployment, not of this plugin.
|
|
36
|
+
* shell's own git is configured with, or the environment a deployment gives
|
|
37
|
+
* its commands. `exec` is an arbitrary runner, so that is a property of a
|
|
38
|
+
* deployment, not of this plugin.
|
|
40
39
|
*
|
|
41
40
|
* Unchecked, like the config pins at the clone: git's identity is needed by the
|
|
42
41
|
* first commit, and a failure to write it fails that commit with a sentence far
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { ActionContext } from "@cloudflare/think";
|
|
2
|
+
import { type GithubMcpCall } from "./mcp.js";
|
|
2
3
|
import type { RepoCheckout, RepoConfig, RepoExec, RepoGitResult } from "./index.js";
|
|
3
4
|
/**
|
|
4
5
|
* How many items one GitHub list call asks for. GitHub's maximum, and its default
|
|
@@ -11,39 +12,12 @@ import type { RepoCheckout, RepoConfig, RepoExec, RepoGitResult } from "./index.
|
|
|
11
12
|
* the last page, and those are exactly the ones an agent was sent to act on.
|
|
12
13
|
*/
|
|
13
14
|
export declare const GITHUB_PAGE_SIZE = 100;
|
|
14
|
-
/**
|
|
15
|
-
* Where GraphQL lives, given where REST does.
|
|
16
|
-
*
|
|
17
|
-
* On github.com they share an origin and `/graphql` hangs off it. On GitHub
|
|
18
|
-
* Enterprise — which {@link RepoConfig.apiBase} exists for — REST is under
|
|
19
|
-
* `/api/v3` and GraphQL is its **sibling** at `/api/graphql`, not a path beneath
|
|
20
|
-
* it. Appending blindly yields `/api/v3/graphql`, which is a 404 on every
|
|
21
|
-
* Enterprise install and no failure at all on the public API, so the mistake
|
|
22
|
-
* survives every test written against github.com.
|
|
23
|
-
*/
|
|
24
|
-
export declare function graphqlEndpoint(apiBase: string): string;
|
|
25
|
-
/**
|
|
26
|
-
* Middle-out truncation, so both the head of a diff and its tail survive.
|
|
27
|
-
*
|
|
28
|
-
* Deliberately a **copy** of `/workspace`'s function of the same name, not an
|
|
29
|
-
* import. `npm run verify:exports` fails any subpath whose module graph
|
|
30
|
-
* reaches a sibling's directory, and its own advice for a shared helper is to
|
|
31
|
-
* duplicate it — installing `repo` must not drag `/workspace` (and therefore
|
|
32
|
-
* `@cloudflare/computer`) into a consumer's bundle. Fifteen lines is a cheaper
|
|
33
|
-
* price than that coupling, and it is the same reason `exec` is injected here
|
|
34
|
-
* rather than imported.
|
|
35
|
-
*
|
|
36
|
-
* The `half < 1` guard is not padding: without it a small `max` makes `half`
|
|
37
|
-
* zero or negative, and `slice(-0)` is `slice(0)` — the whole string — so the
|
|
38
|
-
* function would return *more* than it was given.
|
|
39
|
-
*/
|
|
40
|
-
export declare function truncateOutput(text: string, max: number): string;
|
|
41
15
|
/**
|
|
42
16
|
* What one container command produced — plus one bit the container did not.
|
|
43
17
|
*
|
|
44
|
-
* `exec` does not only *return* failures, it throws them:
|
|
45
|
-
*
|
|
46
|
-
*
|
|
18
|
+
* `exec` does not only *return* failures, it throws them: any call to a Durable
|
|
19
|
+
* Object can fail outright, a container lost mid-command among them. That is
|
|
20
|
+
* caught at the seam
|
|
47
21
|
* ({@link repoContext}'s `run`) and turned into a failed result, so every tool
|
|
48
22
|
* answers in its own `!success` branch — "could not read the status of /w/r: …"
|
|
49
23
|
* is a better sentence than any wrapper one level up could write, and a seventh
|
|
@@ -83,6 +57,16 @@ export interface RepoContext {
|
|
|
83
57
|
};
|
|
84
58
|
unreachable?: string;
|
|
85
59
|
}>;
|
|
60
|
+
/**
|
|
61
|
+
* One call to GitHub's hosted MCP server — every read this plugin makes.
|
|
62
|
+
* See {@link file://./mcp.ts GithubMcpCall}.
|
|
63
|
+
*/
|
|
64
|
+
mcp: GithubMcpCall;
|
|
65
|
+
/**
|
|
66
|
+
* One REST call, for what is left on it: opening a pull request and
|
|
67
|
+
* commenting. Both are writes, and both are `coding`'s rather than
|
|
68
|
+
* claude-coordinator's.
|
|
69
|
+
*/
|
|
86
70
|
github: (tool: string, path: string, init?: {
|
|
87
71
|
method: string;
|
|
88
72
|
body: unknown;
|
|
@@ -125,8 +109,6 @@ export declare const GITHUB_ACTION_TIMEOUT_MS = 120000;
|
|
|
125
109
|
* version of that fault.
|
|
126
110
|
*/
|
|
127
111
|
export declare function actionTask(ctx: ActionContext): string;
|
|
128
|
-
/** A hex SHA-256, so a key names a body without carrying it. */
|
|
129
|
-
export declare function sha256(text: string): Promise<string>;
|
|
130
112
|
export declare function repoContext(config: RepoConfig,
|
|
131
113
|
/**
|
|
132
114
|
* The running sub-agent's `runtime()`, read per command. Forwarded to every
|