@dynamicagents/plugins 0.12.0 → 0.14.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.
Files changed (132) hide show
  1. package/README.md +58 -77
  2. package/dist/browser/index.d.ts +0 -2
  3. package/dist/browser/index.js +21 -27
  4. package/dist/claude-code/cards.d.ts +55 -0
  5. package/dist/claude-code/cards.js +283 -0
  6. package/dist/claude-code/config.d.ts +28 -63
  7. package/dist/claude-code/config.js +0 -11
  8. package/dist/claude-code/egress.d.ts +8 -1
  9. package/dist/claude-code/egress.js +42 -4
  10. package/dist/claude-code/events.d.ts +106 -9
  11. package/dist/claude-code/events.js +201 -56
  12. package/dist/claude-code/github.d.ts +39 -0
  13. package/dist/claude-code/github.js +82 -0
  14. package/dist/claude-code/index.d.ts +16 -90
  15. package/dist/claude-code/index.js +14 -276
  16. package/dist/claude-code/model.d.ts +152 -0
  17. package/dist/claude-code/model.js +366 -0
  18. package/dist/claude-code/run.d.ts +230 -116
  19. package/dist/claude-code/run.js +380 -167
  20. package/dist/claude-code/session.d.ts +131 -0
  21. package/dist/claude-code/session.js +288 -0
  22. package/dist/claude-code/spec.d.ts +46 -0
  23. package/dist/claude-code/spec.js +83 -0
  24. package/dist/claude-code/verified.d.ts +15 -0
  25. package/dist/claude-code/verified.js +15 -0
  26. package/dist/computer/context.d.ts +1 -5
  27. package/dist/computer/context.js +7 -10
  28. package/dist/computer/file-lock.js +5 -4
  29. package/dist/computer/gate.d.ts +4 -4
  30. package/dist/computer/gate.js +5 -5
  31. package/dist/computer/index.d.ts +40 -258
  32. package/dist/computer/index.js +71 -324
  33. package/dist/computer/paths.d.ts +36 -16
  34. package/dist/computer/paths.js +42 -29
  35. package/dist/computer/proxy.d.ts +45 -0
  36. package/dist/computer/proxy.js +234 -0
  37. package/dist/computer/render.d.ts +1 -40
  38. package/dist/computer/render.js +2 -68
  39. package/dist/computer/tools-exec.js +12 -13
  40. package/dist/computer/tools-file.d.ts +9 -1
  41. package/dist/computer/tools-file.js +22 -82
  42. package/dist/computer/{tools-find.d.ts → tools-grep.d.ts} +1 -1
  43. package/dist/computer/tools-grep.js +129 -0
  44. package/dist/repo/checkout.d.ts +4 -5
  45. package/dist/repo/checkout.js +31 -8
  46. package/dist/repo/context.d.ts +35 -9
  47. package/dist/repo/context.js +101 -49
  48. package/dist/repo/index.d.ts +73 -28
  49. package/dist/repo/index.js +112 -96
  50. package/dist/repo/tools-checks.d.ts +3 -0
  51. package/dist/repo/tools-checks.js +112 -0
  52. package/dist/repo/tools-clone.js +1 -1
  53. package/dist/repo/tools-pr.d.ts +14 -0
  54. package/dist/repo/{tools-forge.js → tools-pr.js} +53 -36
  55. package/dist/repo/tools-review.d.ts +9 -0
  56. package/dist/repo/tools-review.js +67 -47
  57. package/dist/repo/tools-worktree.js +144 -60
  58. package/dist/repo/tools-worktrees.d.ts +12 -0
  59. package/dist/repo/tools-worktrees.js +48 -0
  60. package/dist/repo/url.d.ts +2 -2
  61. package/dist/repo/url.js +7 -7
  62. package/dist/scratch/index.d.ts +4 -4
  63. package/dist/scratch/index.js +12 -11
  64. package/dist/{computer → workspace}/advisory.d.ts +4 -4
  65. package/dist/{computer → workspace}/advisory.js +2 -2
  66. package/dist/{computer/host → workspace}/ca-trust.d.ts +8 -1
  67. package/dist/{computer/host → workspace}/ca-trust.js +12 -3
  68. package/dist/{computer/host → workspace}/container-deps.d.ts +4 -0
  69. package/dist/{computer/host → workspace}/container-deps.js +26 -1
  70. package/dist/{computer/host → workspace}/container-fault.d.ts +3 -3
  71. package/dist/{computer/host → workspace}/container-fault.js +23 -8
  72. package/dist/workspace/deps-snapshot.d.ts +110 -0
  73. package/dist/workspace/deps-snapshot.js +424 -0
  74. package/dist/workspace/exec.d.ts +94 -0
  75. package/dist/workspace/exec.js +153 -0
  76. package/dist/workspace/format.d.ts +36 -0
  77. package/dist/workspace/format.js +57 -0
  78. package/dist/{computer/host → workspace}/git-host.d.ts +21 -4
  79. package/dist/{computer/host → workspace}/git-host.js +72 -2
  80. package/dist/{computer/host → workspace}/git-identity.d.ts +1 -1
  81. package/dist/{computer/host → workspace}/git-identity.js +1 -1
  82. package/dist/workspace/index.d.ts +44 -47
  83. package/dist/workspace/index.js +52 -112
  84. package/dist/{computer/host → workspace}/install-job.d.ts +53 -4
  85. package/dist/{computer/host → workspace}/install-job.js +346 -178
  86. package/dist/{computer → workspace}/install.d.ts +17 -8
  87. package/dist/{computer → workspace}/install.js +24 -6
  88. package/dist/{computer/host/workspace.d.ts → workspace/object.d.ts} +125 -27
  89. package/dist/{computer/host/workspace.js → workspace/object.js} +341 -58
  90. package/dist/workspace/open.d.ts +95 -0
  91. package/dist/workspace/open.js +63 -0
  92. package/dist/workspace/read.d.ts +16 -0
  93. package/dist/workspace/read.js +22 -0
  94. package/dist/{computer → workspace}/shell.d.ts +5 -5
  95. package/dist/{computer → workspace}/shell.js +6 -6
  96. package/dist/{computer/host → workspace}/wake.d.ts +3 -1
  97. package/package.json +17 -37
  98. package/dist/arc-agi/analysis.d.ts +0 -322
  99. package/dist/arc-agi/analysis.js +0 -802
  100. package/dist/arc-agi/client.d.ts +0 -82
  101. package/dist/arc-agi/client.js +0 -113
  102. package/dist/arc-agi/game-tools.d.ts +0 -24
  103. package/dist/arc-agi/game-tools.js +0 -26
  104. package/dist/arc-agi/index.d.ts +0 -40
  105. package/dist/arc-agi/index.js +0 -175
  106. package/dist/arc-agi/main-agent.d.ts +0 -24
  107. package/dist/arc-agi/main-agent.js +0 -62
  108. package/dist/arc-agi/recipe.d.ts +0 -48
  109. package/dist/arc-agi/recipe.js +0 -97
  110. package/dist/arc-agi/scorecard.d.ts +0 -118
  111. package/dist/arc-agi/scorecard.js +0 -163
  112. package/dist/arc-agi/soul.d.ts +0 -29
  113. package/dist/arc-agi/soul.js +0 -79
  114. package/dist/arc-agi/store.d.ts +0 -153
  115. package/dist/arc-agi/store.js +0 -215
  116. package/dist/arc-agi/tools.d.ts +0 -36
  117. package/dist/arc-agi/tools.js +0 -574
  118. package/dist/arc-agi/types.d.ts +0 -211
  119. package/dist/arc-agi/types.js +0 -19
  120. package/dist/claude-code/recipe.d.ts +0 -53
  121. package/dist/claude-code/recipe.js +0 -100
  122. package/dist/computer/read.d.ts +0 -133
  123. package/dist/computer/read.js +0 -200
  124. package/dist/computer/tools-find.js +0 -216
  125. package/dist/recall/index.d.ts +0 -105
  126. package/dist/recall/index.js +0 -181
  127. package/dist/repo/tools-forge.d.ts +0 -4
  128. package/dist/triage/index.d.ts +0 -360
  129. package/dist/triage/index.js +0 -311
  130. /package/dist/{computer/host → workspace}/sync.d.ts +0 -0
  131. /package/dist/{computer/host → workspace}/sync.js +0 -0
  132. /package/dist/{computer/host → workspace}/wake.js +0 -0
@@ -0,0 +1,45 @@
1
+ import type { WorkspaceLike } from "@cloudflare/think";
2
+ import type { WorkspaceHost } from "../workspace/open.js";
3
+ import type { ComputerConfig } from "./index.js";
4
+ /**
5
+ * The container's workspace, as the agent's own `this.workspace`.
6
+ *
7
+ * Think's `read`, `write`, `edit` and `delete` run against whatever an agent's
8
+ * workspace is, and `computer()`'s tools replace only the ones it names. So an
9
+ * agent with a container and Think's default workspace would run `bash` in the
10
+ * container and `write` in its own SQLite, and nothing would say so. An agent
11
+ * that installs `computer` sets this instead:
12
+ *
13
+ * ```ts
14
+ * override workspace = computerWorkspace(config, () => this.pluginContext().runtime());
15
+ * ```
16
+ *
17
+ * and `computer(config).tools(ctx)` refuses to start without it — and reaches
18
+ * the workspace through it, so the tools and Think's file tools cannot resolve
19
+ * two different ones.
20
+ *
21
+ * Built on the workspace's own filesystem, not on `@cloudflare/computer`'s
22
+ * Think adapter: that one's `glob` and `readDir` read every entry, and its
23
+ * `writeFile` knows nothing of the paths and the states this plugin refuses.
24
+ */
25
+ /**
26
+ * Where a {@link computerWorkspace} sends each call, carried on it under
27
+ * {@link ROUTE}. `computer()`'s tools route through this rather than through
28
+ * their own config, which is what makes a workspace built from another config,
29
+ * or without its runtime, a wrong workspace for both rather than a split.
30
+ */
31
+ export interface WorkspaceRoute {
32
+ /** The workspace the running turn names. Read per call. */
33
+ name(): string;
34
+ host(name: string): DurableObjectStub<WorkspaceHost>;
35
+ }
36
+ /** The route of a workspace {@link computerWorkspace} built, or `undefined`. */
37
+ export declare function workspaceRoute(workspace: unknown): WorkspaceRoute | undefined;
38
+ /** Whether `workspace` is one {@link computerWorkspace} built. */
39
+ export declare function isComputerWorkspace(workspace: unknown): boolean;
40
+ /**
41
+ * @param runtime The running sub-agent's `runtime()`, read per call — how a
42
+ * sub-agent's file tools reach the workspace its parent prepared. A parent's
43
+ * is `undefined`, and {@link ComputerConfig.workspaceName} names it.
44
+ */
45
+ export declare function computerWorkspace(config: ComputerConfig, runtime?: () => unknown): WorkspaceLike;
@@ -0,0 +1,234 @@
1
+ var __addDisposableResource = (this && this.__addDisposableResource) || function (env, value, async) {
2
+ if (value !== null && value !== void 0) {
3
+ if (typeof value !== "object" && typeof value !== "function") throw new TypeError("Object expected.");
4
+ var dispose, inner;
5
+ if (async) {
6
+ if (!Symbol.asyncDispose) throw new TypeError("Symbol.asyncDispose is not defined.");
7
+ dispose = value[Symbol.asyncDispose];
8
+ }
9
+ if (dispose === void 0) {
10
+ if (!Symbol.dispose) throw new TypeError("Symbol.dispose is not defined.");
11
+ dispose = value[Symbol.dispose];
12
+ if (async) inner = dispose;
13
+ }
14
+ if (typeof dispose !== "function") throw new TypeError("Object not disposable.");
15
+ if (inner) dispose = function() { try { inner.call(this); } catch (e) { return Promise.reject(e); } };
16
+ env.stack.push({ value: value, dispose: dispose, async: async });
17
+ }
18
+ else if (async) {
19
+ env.stack.push({ async: true });
20
+ }
21
+ return value;
22
+ };
23
+ var __disposeResources = (this && this.__disposeResources) || (function (SuppressedError) {
24
+ return function (env) {
25
+ function fail(e) {
26
+ env.error = env.hasError ? new SuppressedError(e, env.error, "An error was suppressed during disposal.") : e;
27
+ env.hasError = true;
28
+ }
29
+ var r, s = 0;
30
+ function next() {
31
+ while (r = env.stack.pop()) {
32
+ try {
33
+ if (!r.async && s === 1) return s = 0, env.stack.push(r), Promise.resolve().then(next);
34
+ if (r.dispose) {
35
+ var result = r.dispose.call(r.value);
36
+ if (r.async) return s |= 2, Promise.resolve(result).then(next, function(e) { fail(e); return next(); });
37
+ }
38
+ else s |= 1;
39
+ }
40
+ catch (e) {
41
+ fail(e);
42
+ }
43
+ }
44
+ if (s === 1) return env.hasError ? Promise.reject(env.error) : Promise.resolve();
45
+ if (env.hasError) throw env.error;
46
+ }
47
+ return next();
48
+ };
49
+ })(typeof SuppressedError === "function" ? SuppressedError : function (error, suppressed, message) {
50
+ var e = new Error(message);
51
+ return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e;
52
+ });
53
+ import { withFileLock } from "./file-lock.js";
54
+ import { writeGate } from "./gate.js";
55
+ import { openWorkspaceFs, workspaceNameFromRuntime } from "../workspace/open.js";
56
+ import { guardPath, WALK_SKIPS } from "./paths.js";
57
+ /**
58
+ * The key the route is carried under. `Symbol.for`, so a second copy of this
59
+ * module in one bundle still recognises the first's workspaces.
60
+ */
61
+ const ROUTE = Symbol.for("@dynamicagents/plugins/computer:workspace");
62
+ /**
63
+ * The most one `glob` returns. Its caller is Think's `find`, which shows 200
64
+ * and says when there were more — so one past that is all it can use, and
65
+ * reading the rest of a large checkout into the object would buy nothing.
66
+ */
67
+ const GLOB_LIMIT = 201;
68
+ /** The route of a workspace {@link computerWorkspace} built, or `undefined`. */
69
+ export function workspaceRoute(workspace) {
70
+ if (typeof workspace !== "object" || workspace === null)
71
+ return undefined;
72
+ return workspace[ROUTE];
73
+ }
74
+ /** Whether `workspace` is one {@link computerWorkspace} built. */
75
+ export function isComputerWorkspace(workspace) {
76
+ return workspaceRoute(workspace) !== undefined;
77
+ }
78
+ /**
79
+ * Missing, as the filesystem says it across the boundary. The code does not
80
+ * always survive RPC, so the message is read too.
81
+ */
82
+ function isMissing(err) {
83
+ if (!err || typeof err !== "object")
84
+ return false;
85
+ const { code, message } = err;
86
+ if (code === "ENOENT")
87
+ return true;
88
+ return typeof message === "string" && /ENOENT|no such/i.test(message);
89
+ }
90
+ /** `null` for a path that is not there, which is what Think's tools expect. */
91
+ async function orNull(read) {
92
+ try {
93
+ return await read;
94
+ }
95
+ catch (err) {
96
+ if (isMissing(err))
97
+ return null;
98
+ throw err;
99
+ }
100
+ }
101
+ /**
102
+ * `stat`, with a missing path answered as `null`.
103
+ *
104
+ * Asked of the stub's own `statOrNull` where it has one, the way
105
+ * {@link file://../workspace/read.ts pathExists} asks for `exists`: Think's
106
+ * `read` stats before it reads, so a missing file is an ordinary answer here,
107
+ * and a `stat` that throws across the boundary leaves the object an unhandled
108
+ * rejection to log for it.
109
+ */
110
+ function statOrNull(fs, path) {
111
+ // Called as a method, never through `.call`: on an RPC stub `.call` is read
112
+ // as a remote method.
113
+ const remote = fs;
114
+ if (typeof remote.statOrNull === "function")
115
+ return remote.statOrNull(path);
116
+ return orNull(fs.stat(path));
117
+ }
118
+ async function drain(stream) {
119
+ return new Uint8Array(await new Response(stream).arrayBuffer());
120
+ }
121
+ function fileInfo(entry) {
122
+ const type = entry.isDirectory ? "directory" : "file";
123
+ return {
124
+ path: entry.path,
125
+ name: entry.path.slice(entry.path.lastIndexOf("/") + 1),
126
+ type,
127
+ // Generic on purpose: Think's `read` sniffs a generic type from the bytes,
128
+ // and an extension is not evidence of what a file holds.
129
+ mimeType: type === "directory" ? "inode/directory" : "application/octet-stream",
130
+ size: entry.size ?? 0,
131
+ createdAt: entry.mtime ?? 0,
132
+ updatedAt: entry.mtime ?? 0
133
+ };
134
+ }
135
+ /** The directory a glob walks from, and the pattern relative to it. */
136
+ function splitGlob(pattern, cwd) {
137
+ const absolute = pattern.startsWith("/") ? pattern : `${cwd}/${pattern}`;
138
+ const wildcard = absolute.search(/[*?[{]/);
139
+ const cut = wildcard === -1
140
+ ? absolute.lastIndexOf("/")
141
+ : absolute.lastIndexOf("/", wildcard);
142
+ return {
143
+ dir: cut <= 0 ? "/" : absolute.slice(0, cut),
144
+ rest: absolute.slice(cut + 1)
145
+ };
146
+ }
147
+ /**
148
+ * @param runtime The running sub-agent's `runtime()`, read per call — how a
149
+ * sub-agent's file tools reach the workspace its parent prepared. A parent's
150
+ * is `undefined`, and {@link ComputerConfig.workspaceName} names it.
151
+ */
152
+ export function computerWorkspace(config, runtime) {
153
+ const cwd = config.cwd ?? "/workspace";
154
+ // Resolved per call, never memoized: the name belongs to the turn, and a
155
+ // stale one would route this caller's writes into another's files.
156
+ const named = () => workspaceNameFromRuntime(runtime?.()) ?? config.workspaceName();
157
+ const host = (name) => config.binding.get(config.binding.idFromName(name));
158
+ /**
159
+ * One call, against the workspace this turn names. Opened without starting a
160
+ * container — see `WorkspaceHost.__getWorkspaceFsStub`.
161
+ */
162
+ const withFs = async (path, verb, body) => {
163
+ const env_1 = { stack: [], error: void 0, hasError: false };
164
+ try {
165
+ const refusal = guardPath(path, verb);
166
+ if (refusal)
167
+ throw new Error(refusal);
168
+ const name = named();
169
+ const ws = __addDisposableResource(env_1, await openWorkspaceFs(host(name)), false);
170
+ return await body(ws.fs, name);
171
+ }
172
+ catch (e_1) {
173
+ env_1.error = e_1;
174
+ env_1.hasError = true;
175
+ }
176
+ finally {
177
+ __disposeResources(env_1);
178
+ }
179
+ };
180
+ /**
181
+ * A write, refused while the workspace cannot keep it. Thrown rather than
182
+ * returned: Think's tools report a thrown error to the model, and a write
183
+ * that returned would read as one that landed.
184
+ */
185
+ const withWritableFs = (path, verb, body) => withFs(path, verb, async (fs, name) => {
186
+ // Fails open, for the reason the tools' own write gate does: a read of
187
+ // another Durable Object's state must not take out a working tool.
188
+ const lost = await host(name)
189
+ .advisories()
190
+ .then(writeGate, () => undefined);
191
+ if (lost)
192
+ throw new Error(lost);
193
+ return body(fs, name);
194
+ });
195
+ const workspace = {
196
+ readFile: (path) =>
197
+ // Whole, never truncated: Think's `edit` writes back what it read.
198
+ withFs(path, "read", (fs) => orNull(fs.readFile(path, "utf8"))),
199
+ readFileBytes: (path) => withFs(path, "read", async (fs) => {
200
+ const stream = await orNull(fs.readFile(path));
201
+ return stream === null ? null : drain(stream);
202
+ }),
203
+ stat: (path) => withFs(path, "read", async (fs) => {
204
+ const stat = await statOrNull(fs, path);
205
+ return stat === null ? null : fileInfo({ ...stat, path });
206
+ }),
207
+ readDir: (dir = cwd, opts) => withFs(dir, "list", async (fs) => (await fs.readdir(dir, opts)).map((entry) => fileInfo({
208
+ ...entry,
209
+ path: dir === "/" ? `/${entry.name}` : `${dir}/${entry.name}`
210
+ }))),
211
+ // Pruned rather than filtered: at a repository root either skipped
212
+ // directory outnumbers everything else.
213
+ glob: (pattern) => {
214
+ const { dir, rest } = splitGlob(pattern, cwd);
215
+ return withFs(dir, "find", async (fs) => (await fs.find(dir, rest, {
216
+ limit: GLOB_LIMIT,
217
+ exclude: WALK_SKIPS.map((segment) => `**/${segment}`)
218
+ })).map((entry) => fileInfo({ path: entry.path, isDirectory: entry.type === "dir" })));
219
+ },
220
+ // Locked per file, so a write never lands inside the `edit` tool's read and
221
+ // write of the same file — see `./file-lock.ts`.
222
+ writeFile: (path, content) => withWritableFs(path, "write", (fs, name) => withFileLock(name, path, () => fs.writeFile(path, content))),
223
+ mkdir: (path, opts) => withWritableFs(path, "write", (fs) => fs.mkdir(path, opts)),
224
+ // Locked too: a delete landing between an edit's read and its write would
225
+ // be undone by the write, and both would report success.
226
+ rm: (path, opts) => withWritableFs(path, "delete", (fs, name) => withFileLock(name, path, () => fs.rm(path, opts)))
227
+ // No `writeFileBytes`. Think writes through it on its own account —
228
+ // evicted media, projected skills — and none of that belongs in a
229
+ // repository's checkout. Without it Think skips both, with a warning.
230
+ };
231
+ const route = { name: named, host };
232
+ Object.defineProperty(workspace, ROUTE, { value: route });
233
+ return workspace;
234
+ }
@@ -1,28 +1,10 @@
1
1
  import type { WorkspaceClient, WorkspaceRuntimeStatus } from "@cloudflare/computer";
2
- /**
3
- * Middle-out truncation, so both the first error and the final summary survive.
4
- *
5
- * The guard is not defensive padding. Without it a `max` at or below the marker's
6
- * own length makes `half` zero or negative, and `slice(-0)` is `slice(0)` — the
7
- * *whole* string — so the function returns more than it was given: 500 characters
8
- * in, 543 out at `max: 80`. A silent inversion of the one thing it does,
9
- * reachable from a public config field.
10
- */
11
- export declare function truncateOutput(text: string, max: number): string;
12
- /**
13
- * A byte count in the form a model can act on.
14
- *
15
- * A listing that says a file is 4.2 MB tells the model the read it is about to do
16
- * comes back with a hole in the middle — the one thing it cannot infer from the
17
- * truncated result itself.
18
- */
19
- export declare function humanBytes(bytes: number): string;
20
2
  /**
21
3
  * One `fs.grep` hit, derived from the client rather than imported.
22
4
  *
23
5
  * `@cloudflare/computer` declares `WorkspaceGrepMatch` but does not re-export it,
24
6
  * so there is no name to import. Reading it back off the method keeps this correct
25
- * by construction — the same structural approach {@link readBounded} takes to `fs`.
7
+ * by construction.
26
8
  */
27
9
  type GrepMatch = Awaited<ReturnType<WorkspaceClient["fs"]["grep"]>>[number];
28
10
  /**
@@ -70,8 +52,6 @@ export declare function renderGrepMatches(matches: GrepMatch[], maxChars: number
70
52
  shown: number;
71
53
  capped: boolean;
72
54
  };
73
- /** Elapsed time in the shape a sentence wants. */
74
- export declare function humanMs(ms: number): string;
75
55
  /**
76
56
  * Render a completed command the way a terminal would, minus the noise.
77
57
  *
@@ -129,23 +109,4 @@ export declare function renderResult(result: {
129
109
  export declare function syncPendingNote(sync: {
130
110
  status: "complete" | "pending";
131
111
  } | undefined): string | undefined;
132
- /**
133
- * Why a command has nothing to show for itself.
134
- *
135
- * {@link renderResult} states a non-`completed` status on its verdict line;
136
- * {@link computerExec} has no verdict line, only the four fields its caller
137
- * branches on. A *killed* process writes nothing, so without this a `git clone`
138
- * that hit the ten-minute ceiling reaches the model through
139
- * `@dynamicagents/plugins/repo` as `clone failed:` with an empty reason — which reads
140
- * like a bug in the plugin rather than a limit it can work within.
141
- *
142
- * Only `cancelled`. A `failed` status is an ordinary non-zero exit, where the
143
- * command's own stderr is the better explanation and this would be noise on top
144
- * of it.
145
- *
146
- * Both plausible causes are named because the exit code cannot separate them:
147
- * 137 is SIGKILL, which is what the timeout sends *and* what the kernel sends a
148
- * container that ran out of memory.
149
- */
150
- export declare function cancelledNote(status: WorkspaceRuntimeStatus | undefined, exitCode: number, timeoutMs: number): string | undefined;
151
112
  export {};
@@ -1,3 +1,4 @@
1
+ import { truncateOutput } from "../workspace/format.js";
1
2
  /**
2
3
  * Turning a result into the text a model reads.
3
4
  *
@@ -15,43 +16,9 @@
15
16
  * A grep match carries the whole line, and a minified bundle is one line of
16
17
  * megabytes — a single match in `dist/` would otherwise be the entire result. The
17
18
  * head is kept because a match's file and line number are what the model acts on;
18
- * it reads the region with `sb_read` next.
19
+ * it reads the region with `read` next.
19
20
  */
20
21
  const MAX_MATCH_LINE_CHARS = 200;
21
- /**
22
- * Middle-out truncation, so both the first error and the final summary survive.
23
- *
24
- * The guard is not defensive padding. Without it a `max` at or below the marker's
25
- * own length makes `half` zero or negative, and `slice(-0)` is `slice(0)` — the
26
- * *whole* string — so the function returns more than it was given: 500 characters
27
- * in, 543 out at `max: 80`. A silent inversion of the one thing it does,
28
- * reachable from a public config field.
29
- */
30
- export function truncateOutput(text, max) {
31
- if (text.length <= max)
32
- return text;
33
- const marker = (dropped) => `\n\n… [${dropped} characters omitted from the middle] …\n\n`;
34
- // No budget for two halves plus the marker: keep the head, which is where the
35
- // first error is, and say nothing clever.
36
- const half = Math.floor((max - marker(text.length).length) / 2);
37
- if (half < 1)
38
- return text.slice(0, Math.max(0, max));
39
- return (text.slice(0, half) + marker(text.length - half * 2) + text.slice(-half));
40
- }
41
- /**
42
- * A byte count in the form a model can act on.
43
- *
44
- * A listing that says a file is 4.2 MB tells the model the read it is about to do
45
- * comes back with a hole in the middle — the one thing it cannot infer from the
46
- * truncated result itself.
47
- */
48
- export function humanBytes(bytes) {
49
- if (bytes < 1024)
50
- return `${bytes} B`;
51
- if (bytes < 1024 * 1024)
52
- return `${(bytes / 1024).toFixed(1)} KB`;
53
- return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
54
- }
55
22
  /** One matching line, bounded — see {@link MAX_MATCH_LINE_CHARS}. */
56
23
  function capLine(text) {
57
24
  // A CRLF file leaves the carriage return on every line, since the reader splits
@@ -135,13 +102,6 @@ export function renderGrepMatches(matches, maxChars) {
135
102
  // Only a line that actually made it out is worth explaining.
136
103
  return { body, shown, capped: hasLongLine.slice(0, shown).includes(true) };
137
104
  }
138
- /** Elapsed time in the shape a sentence wants. */
139
- export function humanMs(ms) {
140
- const s = Math.round(ms / 1000);
141
- return s < 60
142
- ? `${s}s`
143
- : `${Math.floor(s / 60)}m${String(s % 60).padStart(2, "0")}s`;
144
- }
145
105
  /**
146
106
  * Render a completed command the way a terminal would, minus the noise.
147
107
  *
@@ -212,29 +172,3 @@ export function syncPendingNote(sync) {
212
172
  "in a moment, and if the workspace still disagrees with what the command " +
213
173
  "wrote, say so in your result rather than working around it.)");
214
174
  }
215
- /**
216
- * Why a command has nothing to show for itself.
217
- *
218
- * {@link renderResult} states a non-`completed` status on its verdict line;
219
- * {@link computerExec} has no verdict line, only the four fields its caller
220
- * branches on. A *killed* process writes nothing, so without this a `git clone`
221
- * that hit the ten-minute ceiling reaches the model through
222
- * `@dynamicagents/plugins/repo` as `clone failed:` with an empty reason — which reads
223
- * like a bug in the plugin rather than a limit it can work within.
224
- *
225
- * Only `cancelled`. A `failed` status is an ordinary non-zero exit, where the
226
- * command's own stderr is the better explanation and this would be noise on top
227
- * of it.
228
- *
229
- * Both plausible causes are named because the exit code cannot separate them:
230
- * 137 is SIGKILL, which is what the timeout sends *and* what the kernel sends a
231
- * container that ran out of memory.
232
- */
233
- export function cancelledNote(status, exitCode, timeoutMs) {
234
- if (status !== "cancelled")
235
- return undefined;
236
- return (`the command was killed rather than exiting on its own (exit ${exitCode}), so ` +
237
- `anything it had not yet written is gone. The two usual causes are the ` +
238
- `per-command time limit — ${humanMs(timeoutMs)} here — and the container ` +
239
- `running out of memory.`);
240
- }
@@ -55,7 +55,7 @@ import { z } from "zod";
55
55
  import { withAbort } from "@dynamicagents/core";
56
56
  import { renderResult } from "./render.js";
57
57
  import { execLostNote } from "./gate.js";
58
- import { withShellTranscript } from "./shell.js";
58
+ import { withShellTranscript } from "../workspace/shell.js";
59
59
  /** Running one command in the container. */
60
60
  /**
61
61
  * Wait for a disposable resource without outliving `signal`.
@@ -85,7 +85,7 @@ async function killLate(handle) {
85
85
  export function execTools(ctx) {
86
86
  const { config, cwd, timeoutMs, maxChars, workspace, awaitAdvisories, definedEnv } = ctx;
87
87
  return {
88
- sb_exec: tool({
88
+ bash: tool({
89
89
  /**
90
90
  * States what the tool guarantees, rather than what it might withhold.
91
91
  *
@@ -131,7 +131,7 @@ export function execTools(ctx) {
131
131
  * held, how long the command took, what it exited with — answer it
132
132
  * directly.
133
133
  *
134
- * Timed around the gate as well as the command, since a subagent blocked
134
+ * Timed around the gate as well as the command, since an agent blocked
135
135
  * waiting for `npm ci` and one running a slow test suite are
136
136
  * indistinguishable from the outside and want opposite fixes.
137
137
  */
@@ -143,15 +143,14 @@ export function execTools(ctx) {
143
143
  * where the call stopped. A command that outran the limit once will again;
144
144
  * one that never started because the workspace did not answer may not.
145
145
  *
146
- * `sb_exec` reads its call's signal rather than leaving the wait to core,
147
- * and this is why: core's abandonment can say only that the command may
148
- * still be running. Core's `TOOL_CALL_GRACE_MS` is the window this answer
149
- * has to arrive in, and it covers sending the kill below.
146
+ * `bash` reads its call's signal rather than leaving the wait to Think,
147
+ * and this is why: an abandoned wait can say only that the command may
148
+ * still be running, and the kill below is what stops it.
150
149
  */
151
150
  const stopped = (gateMs, started) => {
152
151
  const timedOut = abortSignal?.reason?.name ===
153
152
  "TimeoutError";
154
- console.info("[computer] sb_exec stopped", {
153
+ console.info("[computer] bash stopped", {
155
154
  command,
156
155
  gateMs,
157
156
  durationMs: Date.now() - startedAtMs - gateMs,
@@ -164,9 +163,9 @@ export function execTools(ctx) {
164
163
  ? "the command was stopped: it ran past this call's time limit. Anything it changed before then is still changed. Try something narrower."
165
164
  : "the command did not run: this call reached its time limit while the workspace was still getting ready, so nothing was changed. Try again; if it happens again, the workspace is not responding.";
166
165
  };
167
- // Only `sb_exec` waits on an install. The file tools read and write
166
+ // Only `bash` waits on an install. The file tools read and write
168
167
  // source, which is in the workspace and unaffected by an install in
169
- // flight — blocking them would stop the subagent doing the reading it
168
+ // flight — blocking them would stop the agent doing the reading it
170
169
  // could usefully do while it waits. They do consult `writeGate`, which
171
170
  // is a different question: not "is the tree ready" but "does a write
172
171
  // survive at all".
@@ -182,7 +181,7 @@ export function execTools(ctx) {
182
181
  }
183
182
  const gateMsWaited = Date.now() - startedAtMs;
184
183
  if (gate.block) {
185
- console.info("[computer] sb_exec blocked by a workspace advisory", {
184
+ console.info("[computer] bash blocked by a workspace advisory", {
186
185
  command,
187
186
  gateMs: gateMsWaited
188
187
  });
@@ -224,7 +223,7 @@ export function execTools(ctx) {
224
223
  // process are two acts. Disposing the handle is neither — it releases
225
224
  // this side's attachment and leaves the command running.
226
225
  const result = await withAbort(abortSignal, handle.result(), () => handle.kill("SIGTERM"));
227
- console.info("[computer] sb_exec", {
226
+ console.info("[computer] bash", {
228
227
  command,
229
228
  exitCode: result.exitCode,
230
229
  // Split so a slow command and a slow *wait* never look alike.
@@ -248,7 +247,7 @@ export function execTools(ctx) {
248
247
  if (abortSignal?.aborted)
249
248
  return note(stopped(gateMsWaited, started));
250
249
  const lost = execLostNote(err);
251
- console.warn("[computer] sb_exec failed", {
250
+ console.warn("[computer] bash failed", {
252
251
  command,
253
252
  gateMs: gateMsWaited,
254
253
  durationMs: Date.now() - startedAtMs - gateMsWaited,
@@ -1,4 +1,12 @@
1
1
  import type { ToolSet } from "ai";
2
2
  import type { ComputerContext } from "./context.js";
3
- /** One file at a time: reading it, replacing it, editing it in place. */
3
+ /**
4
+ * Editing a file in place — the one file tool this plugin keeps. Think's
5
+ * `read`, `write` and `delete` reach the workspace through the agent's own
6
+ * `this.workspace`, which is `computerWorkspace` (`./proxy.ts`).
7
+ *
8
+ * Not Think's `edit`, because that one is two calls through the workspace — a
9
+ * `readFile`, then a `writeFile` — and a lock inside the workspace cannot span
10
+ * the pair. See `./file-lock.ts`.
11
+ */
4
12
  export declare function fileTools(ctx: ComputerContext): ToolSet;