hammerkit 1.26.0 → 1.27.0-next.1

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 (117) hide show
  1. package/build.schema.json +53 -1
  2. package/dist/cache/backends/local-cache-backend.js +10 -2
  3. package/dist/cache/explain.js +13 -11
  4. package/dist/cli.js +3 -0
  5. package/dist/docker/item-network.js +471 -0
  6. package/dist/docker/pull.js +4 -2
  7. package/dist/docker/remove-orphaned-containers.js +13 -6
  8. package/dist/environment/replace-env-variables.js +8 -6
  9. package/dist/executer/await-completed-dependencies.js +2 -1
  10. package/dist/executer/check-readiness.js +2 -2
  11. package/dist/executer/docker-service.js +24 -13
  12. package/dist/executer/docker-task.js +18 -29
  13. package/dist/executer/event-cache.js +4 -0
  14. package/dist/executer/execute-command.js +18 -5
  15. package/dist/executer/execute-docker.js +4 -1
  16. package/dist/executer/execution-steps.js +33 -7
  17. package/dist/executer/get-docker-executor.js +14 -7
  18. package/dist/executer/scheduler/enqueue-next.js +17 -3
  19. package/dist/executer/watch-loop.js +2 -2
  20. package/dist/executer/watch-state-key.js +1 -1
  21. package/dist/file/within-root.js +42 -0
  22. package/dist/harness/connect.js +99 -0
  23. package/dist/harness/format.js +73 -0
  24. package/dist/harness/mcp-command.js +59 -0
  25. package/dist/harness/mcp.js +432 -0
  26. package/dist/harness/opencode.js +492 -0
  27. package/dist/harness/pi.js +217 -0
  28. package/dist/harness/sandbox-client.js +373 -0
  29. package/dist/index.js +33 -36
  30. package/dist/kubernetes/apply.js +31 -1
  31. package/dist/kubernetes/await-running-state.js +33 -1
  32. package/dist/kubernetes/ensure-kubernetes-deployment-exists.js +43 -21
  33. package/dist/kubernetes/ensure-kubernetes-persistent-volume-claim-exists.js +131 -4
  34. package/dist/kubernetes/ensure-kubernetes-service-exists.js +25 -4
  35. package/dist/kubernetes/ensure-persistent-data.js +48 -7
  36. package/dist/kubernetes/get-service-ip.js +15 -1
  37. package/dist/kubernetes/job-placement.js +99 -0
  38. package/dist/kubernetes/job-secrets.js +306 -0
  39. package/dist/kubernetes/kept-claims.js +191 -0
  40. package/dist/kubernetes/kubernetes-instance.js +10 -2
  41. package/dist/kubernetes/network-gate.js +71 -0
  42. package/dist/kubernetes/network-policy.js +526 -0
  43. package/dist/kubernetes/resources.js +13 -2
  44. package/dist/kubernetes/restore-kubernetes-data.js +19 -4
  45. package/dist/kubernetes/secrets.js +2 -0
  46. package/dist/kubernetes/store-kubernetes-data.js +1 -1
  47. package/dist/kubernetes/volumes.js +100 -11
  48. package/dist/main.js +41 -0
  49. package/dist/network/egress-proxy.js +368 -0
  50. package/dist/network/port-relay.js +35 -0
  51. package/dist/optimizer/get-work-cache-stats.js +16 -0
  52. package/dist/optimizer/work-task-cache-description.js +3 -2
  53. package/dist/planner/service-definition.js +5 -0
  54. package/dist/planner/utils/append-work-environment.js +2 -0
  55. package/dist/planner/utils/append-work-service.js +4 -0
  56. package/dist/planner/utils/append-work-task.js +17 -1
  57. package/dist/planner/utils/assert-item-paths.js +50 -0
  58. package/dist/planner/utils/hosted-volumes.js +35 -0
  59. package/dist/planner/work-network.js +193 -0
  60. package/dist/planner/work-runtime-docker.js +41 -0
  61. package/dist/planner/work-runtime-kubernetes.js +335 -102
  62. package/dist/program.js +21 -0
  63. package/dist/remote/annotations.js +103 -0
  64. package/dist/remote/bundle-text.js +24 -0
  65. package/dist/remote/bundle.js +9 -1
  66. package/dist/remote/remote-commands.js +4 -1
  67. package/dist/remote/remote-work-commands.js +182 -69
  68. package/dist/remote/server-client.js +18 -5
  69. package/dist/remote/source-manifest.js +15 -2
  70. package/dist/remote/token-source.js +67 -0
  71. package/dist/schema/build-file-base-task-schema.js +3 -0
  72. package/dist/schema/build-file-container-service-schema.js +6 -0
  73. package/dist/schema/build-file-container-task-schema.js +3 -0
  74. package/dist/schema/build-file-environment-schema.js +9 -0
  75. package/dist/schema/network-schema.js +78 -0
  76. package/dist/schema/reference-parser.js +1 -0
  77. package/dist/schema/schema-parser.js +16 -3
  78. package/dist/schema/work-scope-parser.js +5 -0
  79. package/dist/secrets/command-secret-provider.js +73 -10
  80. package/dist/secrets/github-app-token-command.js +23 -0
  81. package/dist/secrets/github-app-token.js +191 -0
  82. package/dist/server/accounts-api.js +9 -2
  83. package/dist/server/definitions-api.js +69 -47
  84. package/dist/server/definitions.js +76 -51
  85. package/dist/server/executor.js +156 -41
  86. package/dist/server/git-checkout.js +72 -8
  87. package/dist/server/kubernetes-backend.js +554 -0
  88. package/dist/server/kubernetes-sandbox-storage.js +996 -0
  89. package/dist/server/oauth.js +1 -0
  90. package/dist/server/plan.js +51 -1
  91. package/dist/server/policy.js +104 -15
  92. package/dist/server/proposals-api.js +24 -40
  93. package/dist/server/proposals.js +12 -25
  94. package/dist/server/repo-access.js +37 -6
  95. package/dist/server/run-logs.js +217 -0
  96. package/dist/server/run-plan.js +51 -2
  97. package/dist/server/run-request.js +23 -10
  98. package/dist/server/run-store.js +5 -0
  99. package/dist/server/run-worker-command.js +13 -1
  100. package/dist/server/run-worker.js +121 -24
  101. package/dist/server/runs.js +184 -10
  102. package/dist/server/sandbox-docker.js +62 -0
  103. package/dist/server/sandbox-files-api.js +269 -0
  104. package/dist/server/sandbox-files.js +1054 -0
  105. package/dist/server/sandbox-patch.js +481 -0
  106. package/dist/server/sandbox-pod-command.js +15 -0
  107. package/dist/server/sandbox-pod.js +146 -0
  108. package/dist/server/sandbox-storage.js +462 -0
  109. package/dist/server/sandboxes-api.js +210 -0
  110. package/dist/server/sandboxes.js +519 -0
  111. package/dist/server/server-config.js +412 -22
  112. package/dist/server/server.js +204 -39
  113. package/dist/utils/run-id.js +12 -0
  114. package/harness/opencode.ts +11 -0
  115. package/harness/pi.ts +7 -0
  116. package/package.json +8 -2
  117. package/dist/remote/text-diff.js +0 -89
@@ -0,0 +1,492 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.patchToSandbox = exports.createOpencodeTools = exports.createOpencodeV2Tools = exports.hammerkitOpencodeEntry = exports.hammerkitOpencodeV2Plugin = exports.OPENCODE_V2_LOCAL_TOOLS = exports.OPENCODE_V2_TOOLS = exports.loadHammerkitOpencodePlugin = exports.OPENCODE_TOOLS = exports.hammerkitOpencodePlugin = void 0;
4
+ const fs_1 = require("fs");
5
+ const minimatch_1 = require("minimatch");
6
+ const os_1 = require("os");
7
+ const path_1 = require("path");
8
+ const url_1 = require("url");
9
+ const connect_1 = require("./connect");
10
+ const format_1 = require("./format");
11
+ const sandbox_client_1 = require("./sandbox-client");
12
+ const DEFAULT_READ_LINES = 2000;
13
+ const LIST_LIMIT = 100;
14
+ const SEARCH_LIMIT = 100;
15
+ /**
16
+ * The tools of opencode on a hammerkit sandbox: `read`, `write`, `edit`, `apply_patch`,
17
+ * `bash`, `grep`, `glob` and `list` with the parameters of opencode's own, which a plugin replaces
18
+ * by registering tools of the same name, and `push`, which takes the work out as a branch. opencode resolves paths against its directory,
19
+ * which stands for the root of the sandbox. `schema` is the zod of opencode
20
+ * (`tool.schema` of `@opencode-ai/plugin`), so it reads the parameters as its own.
21
+ */
22
+ function hammerkitOpencodePlugin(schema, settings = {}) {
23
+ return async (input) => ({ tool: createOpencodeTools(schema, lazySandbox(input.directory, settings)) });
24
+ }
25
+ exports.hammerkitOpencodePlugin = hammerkitOpencodePlugin;
26
+ // the sandbox, connected on the first call of a tool and again after a failed connect
27
+ function lazySandbox(directory, settings) {
28
+ const environment = settings.environment ?? (0, connect_1.getHarnessEnvironment)(directory);
29
+ const options = settings.options ?? (0, connect_1.harnessOptionsFromEnv)(environment.processEnvs);
30
+ let connecting;
31
+ return () => {
32
+ if (!connecting) {
33
+ connecting = settings.client ? settings.client() : (0, connect_1.connectSandbox)(environment, options);
34
+ connecting.catch(() => (connecting = undefined));
35
+ }
36
+ return connecting;
37
+ };
38
+ }
39
+ // the names of the tools of the plugin, which stand in for opencode's own
40
+ exports.OPENCODE_TOOLS = ['read', 'write', 'edit', 'apply_patch', 'bash', 'grep', 'glob', 'push', 'list'];
41
+ /**
42
+ * The plugin as the file `harness/opencode.ts` exports it: it takes the zod of opencode from
43
+ * `@opencode-ai/plugin`, found next to hammerkit or else in the opencode directories of the
44
+ * project and of the user, as a linked hammerkit does not see the packages of the project.
45
+ * Without it the tools still stand in for opencode's and refuse: opencode carries on without a
46
+ * plugin that fails, and its own tools would work on this machine. `load` is `import` of the file.
47
+ */
48
+ function loadHammerkitOpencodePlugin(load, settings = {}) {
49
+ return async (input) => {
50
+ let schema;
51
+ let failure;
52
+ for (const specifier of ['@opencode-ai/plugin', ...opencodePluginFiles(input)]) {
53
+ try {
54
+ schema = (await load(specifier)).tool.schema;
55
+ break;
56
+ }
57
+ catch (e) {
58
+ failure = failure ?? e;
59
+ }
60
+ }
61
+ if (schema) {
62
+ return hammerkitOpencodePlugin(schema, settings)(input);
63
+ }
64
+ const message = `hammerkit's opencode plugin could not load @opencode-ai/plugin ` +
65
+ `(${failure instanceof Error ? failure.message : String(failure)}), so its tools refuse rather than ` +
66
+ 'work on this machine: add "@opencode-ai/plugin" to the dependencies of .opencode/package.json';
67
+ const refusing = {
68
+ description: message,
69
+ args: {},
70
+ async execute() {
71
+ throw new Error(message);
72
+ },
73
+ };
74
+ return { tool: Object.fromEntries(exports.OPENCODE_TOOLS.map((name) => [name, refusing])) };
75
+ };
76
+ }
77
+ exports.loadHammerkitOpencodePlugin = loadHammerkitOpencodePlugin;
78
+ // the entry of @opencode-ai/plugin in the opencode directories of the project and of the user
79
+ function opencodePluginFiles(input) {
80
+ const config = process.env.XDG_CONFIG_HOME || (0, path_1.join)((0, os_1.homedir)(), '.config');
81
+ const directories = [input.directory, input.worktree, (0, path_1.join)(config, 'opencode')]
82
+ .filter((directory) => !!directory)
83
+ .flatMap((directory) => [(0, path_1.join)(directory, '.opencode'), directory]);
84
+ const files = [];
85
+ for (const directory of directories) {
86
+ for (let current = directory;; current = (0, path_1.dirname)(current)) {
87
+ const manifest = (0, path_1.join)(current, 'node_modules', '@opencode-ai', 'plugin', 'package.json');
88
+ if ((0, fs_1.existsSync)(manifest)) {
89
+ const entry = packageEntry(manifest);
90
+ if (entry && !files.includes(entry)) {
91
+ files.push(entry);
92
+ }
93
+ break;
94
+ }
95
+ if ((0, path_1.dirname)(current) === current) {
96
+ break;
97
+ }
98
+ }
99
+ }
100
+ return files.map((file) => (0, url_1.pathToFileURL)(file).href);
101
+ }
102
+ function packageEntry(manifest) {
103
+ try {
104
+ const pkg = JSON.parse((0, fs_1.readFileSync)(manifest, 'utf8'));
105
+ const root = typeof pkg.exports === 'object' ? pkg.exports['.'] ?? pkg.exports : pkg.exports;
106
+ const entry = typeof root === 'string' ? root : root?.import ?? root?.default ?? pkg.module ?? pkg.main;
107
+ return typeof entry === 'string' ? (0, path_1.join)((0, path_1.dirname)(manifest), entry) : undefined;
108
+ }
109
+ catch {
110
+ return undefined;
111
+ }
112
+ }
113
+ // the names of the tools of the plugin on opencode 2, which stand in for its own
114
+ exports.OPENCODE_V2_TOOLS = ['read', 'write', 'edit', 'patch', 'shell', 'glob', 'grep', 'push'];
115
+ // the tools of opencode 2 that work on this machine and have no stand-in on a sandbox
116
+ exports.OPENCODE_V2_LOCAL_TOOLS = [
117
+ 'opencode_worktree_create',
118
+ 'opencode_worktree_list',
119
+ 'opencode_worktree_remove',
120
+ ];
121
+ /**
122
+ * The plugin for opencode 2: the tools of {@link createOpencodeV2Tools} in place of opencode's own,
123
+ * and none of the worktree tools, which would make worktrees of this machine. opencode 2 takes a
124
+ * plugin as an object with an id and a setup, it needs no package of opencode.
125
+ */
126
+ function hammerkitOpencodeV2Plugin(settings = {}) {
127
+ return {
128
+ id: 'hammerkit',
129
+ async setup(context) {
130
+ const directory = context.location.directory;
131
+ const tools = createOpencodeV2Tools(lazySandbox(directory, settings), directory);
132
+ await context.tool.transform((editor) => {
133
+ for (const tool of tools) {
134
+ editor.add(tool);
135
+ }
136
+ for (const name of exports.OPENCODE_V2_LOCAL_TOOLS) {
137
+ editor.remove(name);
138
+ }
139
+ });
140
+ // opencode describes the tool named shell anew for each request, as the shell of this machine
141
+ const shell = tools.find((tool) => tool.name === 'shell');
142
+ for (const name of ['context', 'compaction', 'generate']) {
143
+ await context.session.hook(name, (request) => {
144
+ if (request.tools.shell) {
145
+ request.tools.shell.description = shell.description;
146
+ }
147
+ });
148
+ }
149
+ },
150
+ };
151
+ }
152
+ exports.hammerkitOpencodeV2Plugin = hammerkitOpencodeV2Plugin;
153
+ /**
154
+ * The plugin as the file `harness/opencode.ts` exports it, for opencode 2 and for opencode 1, which
155
+ * takes the `server` of a plugin object: see {@link loadHammerkitOpencodePlugin}.
156
+ */
157
+ function hammerkitOpencodeEntry(load, settings = {}) {
158
+ return { ...hammerkitOpencodeV2Plugin(settings), server: loadHammerkitOpencodePlugin(load, settings) };
159
+ }
160
+ exports.hammerkitOpencodeEntry = hammerkitOpencodeEntry;
161
+ /**
162
+ * The tools of opencode 2 on a hammerkit sandbox, with the inputs of opencode's own: the tools of
163
+ * opencode 1 under their new names (`patch`, `shell`), paths as `path`, relative to `directory`
164
+ * or absolute below it. opencode 2 has no `list`, `read` lists a directory.
165
+ */
166
+ function createOpencodeV2Tools(client, directory) {
167
+ const tools = opencodeTools(client);
168
+ const tool = (name, of, input, args, permission) => ({
169
+ name,
170
+ description: tools[of].description,
171
+ input,
172
+ options: permission ? { codemode: false, permission } : { codemode: false },
173
+ async execute(input, context) {
174
+ const done = await tools[of].execute(args(input), { directory, abort: context.signal });
175
+ return { content: typeof done === 'string' ? done : done.output };
176
+ },
177
+ });
178
+ const same = (input) => input;
179
+ return [
180
+ tool('read', 'read', object({
181
+ path: text('The file or directory to read, relative to the project or absolute'),
182
+ offset: count('The line number to start reading from (1-based)'),
183
+ limit: count(`The maximum number of lines to read (defaults to ${DEFAULT_READ_LINES})`),
184
+ }, ['path']), ({ path, ...rest }) => ({ filePath: path, ...rest })),
185
+ tool('write', 'write', object({ path: text('The file to write'), content: text('The content to write to the file') }, [
186
+ 'path',
187
+ 'content',
188
+ ]), ({ path, ...rest }) => ({ filePath: path, ...rest }), 'edit'),
189
+ tool('edit', 'edit', object({
190
+ path: text('The file to modify'),
191
+ oldString: text('The text to replace'),
192
+ newString: text('The text to replace it with (must be different from oldString)'),
193
+ replaceAll: flag('Replace all occurrences of oldString (default false)'),
194
+ }, ['path', 'oldString', 'newString']), ({ path, ...rest }) => ({ filePath: path, ...rest }), 'edit'),
195
+ tool('patch', 'apply_patch', object({ patchText: text('The full patch text that describes all changes to be made') }, ['patchText']), same, 'edit'),
196
+ tool('shell', 'bash', object({
197
+ command: text('The command to execute'),
198
+ workdir: text('The directory to run the command in, relative to the project. Use this instead of cd.'),
199
+ timeout: count('Timeout in milliseconds, 0 for the default of the shell task'),
200
+ }, ['command']), ({ timeout, ...rest }) => ({ ...rest, timeout: timeout || undefined })),
201
+ tool('glob', 'glob', object({
202
+ pattern: text('The glob pattern to match files against, like "**/*.ts"'),
203
+ path: text('The directory to search in, the project by default'),
204
+ limit: count(`The maximum number of files (default ${SEARCH_LIMIT})`),
205
+ }, ['pattern']), same),
206
+ tool('grep', 'grep', object({
207
+ pattern: text('The regular expression to search for in file contents'),
208
+ path: text('The directory to search in, the project by default'),
209
+ include: text('File pattern to include in the search (e.g. "*.js", "*.{ts,tsx}")'),
210
+ literal: flag('Search for the pattern as plain text'),
211
+ caseSensitive: flag('Match case (default true)'),
212
+ limit: count(`The maximum number of matching lines (default ${SEARCH_LIMIT})`),
213
+ }, ['pattern']), same),
214
+ tool('push', 'push', object({
215
+ branch: text('The branch to push to, created when it does not exist'),
216
+ message: text('The commit message'),
217
+ }, ['branch', 'message']), same),
218
+ ];
219
+ }
220
+ exports.createOpencodeV2Tools = createOpencodeV2Tools;
221
+ const text = (description) => ({ type: 'string', description });
222
+ const count = (description) => ({ type: 'integer', minimum: 0, description });
223
+ const flag = (description) => ({ type: 'boolean', description });
224
+ const object = (properties, required) => ({
225
+ type: 'object',
226
+ properties,
227
+ required,
228
+ additionalProperties: false,
229
+ });
230
+ function createOpencodeTools(z, client) {
231
+ return Object.fromEntries(Object.entries(opencodeTools(client)).map(([name, { args, ...tool }]) => [name, { ...tool, args: args(z) }]));
232
+ }
233
+ exports.createOpencodeTools = createOpencodeTools;
234
+ function opencodeTools(client) {
235
+ const where = (context, path) => (0, sandbox_client_1.toSandboxPath)(context.directory, path ?? context.directory);
236
+ return inOrder({
237
+ read: {
238
+ description: 'Read a file or directory of the hammerkit sandbox. Lines come back numbered as `N: content`; ' +
239
+ `offset is the first line (1-indexed), limit the number of lines (default ${DEFAULT_READ_LINES}).`,
240
+ args: (z) => ({
241
+ filePath: z.string().describe('The absolute path to the file or directory to read'),
242
+ offset: z.number().int().min(0).optional().describe('The line number to start reading from (1-indexed)'),
243
+ limit: z.number().int().min(0).optional().describe('The maximum number of lines to read (defaults to 2000)'),
244
+ }),
245
+ async execute(args, context) {
246
+ const sandbox = await client();
247
+ const path = where(context, args.filePath);
248
+ const found = await sandbox.stat(path);
249
+ if (!found) {
250
+ throw new Error(`File not found: ${args.filePath}`);
251
+ }
252
+ if (found.type === 'directory') {
253
+ const tree = await sandbox.tree({ path: path || undefined, depth: 1, limit: 1000 });
254
+ const prefix = path ? `${path}/` : '';
255
+ return [
256
+ `<path>${args.filePath}</path>`,
257
+ '<type>directory</type>',
258
+ '<entries>',
259
+ (0, format_1.formatTree)(tree, prefix),
260
+ '</entries>',
261
+ ].join('\n');
262
+ }
263
+ const read = await sandbox.read(path, args.offset || 1, args.limit || DEFAULT_READ_LINES);
264
+ const last = read.nextOffset ? read.nextOffset - 1 : read.lines;
265
+ const footer = read.nextOffset
266
+ ? `(Showing lines ${read.offset}-${last} of ${read.lines}. Use offset=${read.nextOffset} to continue.)`
267
+ : `(End of file - total ${read.lines} lines)`;
268
+ return [
269
+ `<path>${args.filePath}</path>`,
270
+ '<type>file</type>',
271
+ '<content>',
272
+ (0, format_1.toColonNumbers)(read.content.replace(/\n$/, '')),
273
+ '',
274
+ footer,
275
+ '</content>',
276
+ ].join('\n');
277
+ },
278
+ },
279
+ write: {
280
+ description: 'Write a file of the hammerkit sandbox, replacing it if it exists. Read a file before you overwrite it.',
281
+ args: (z) => ({
282
+ content: z.string().describe('The content to write to the file'),
283
+ filePath: z.string().describe('The absolute path to the file to write (must be absolute, not relative)'),
284
+ }),
285
+ async execute(args, context) {
286
+ const sandbox = await client();
287
+ await sandbox.writeRaw(where(context, args.filePath), args.content);
288
+ return 'Wrote file successfully.';
289
+ },
290
+ },
291
+ edit: {
292
+ description: 'Replace an exact string in a file of the hammerkit sandbox. oldString has to match exactly once, ' +
293
+ 'unless replaceAll is set.',
294
+ args: (z) => ({
295
+ filePath: z.string().describe('The absolute path to the file to modify'),
296
+ oldString: z.string().describe('The text to replace'),
297
+ newString: z.string().describe('The text to replace it with (must be different from oldString)'),
298
+ replaceAll: z.boolean().optional().describe('Replace all occurrences of oldString (default false)'),
299
+ }),
300
+ async execute(args, context) {
301
+ if (args.oldString === args.newString) {
302
+ throw new Error('No changes to apply: oldString and newString are identical.');
303
+ }
304
+ const sandbox = await client();
305
+ const path = where(context, args.filePath);
306
+ if (args.oldString === '') {
307
+ // an empty oldString writes a new file, as opencode's edit does
308
+ await sandbox.writeRaw(path, args.newString);
309
+ }
310
+ else {
311
+ await sandbox.edit([
312
+ {
313
+ op: 'replace',
314
+ path,
315
+ old_string: args.oldString,
316
+ new_string: args.newString,
317
+ replace_all: args.replaceAll,
318
+ },
319
+ ]);
320
+ }
321
+ return 'Edit applied successfully.';
322
+ },
323
+ },
324
+ apply_patch: {
325
+ description: 'Apply a patch to the hammerkit sandbox: the `*** Begin Patch` format (Add File, Update File with ' +
326
+ '@@ context, Delete File, Move to) or a unified git diff. All files change or none.',
327
+ args: (z) => ({
328
+ patchText: z.string().describe('The full patch text that describes all changes to be made'),
329
+ }),
330
+ async execute(args, context) {
331
+ const sandbox = await client();
332
+ const files = await sandbox.edit([{ op: 'patch', patch: patchToSandbox(context.directory, args.patchText) }]);
333
+ const marks = { added: 'A', modified: 'M', deleted: 'D' };
334
+ return [
335
+ 'Success. Updated the following files:',
336
+ ...files.map((file) => `${marks[file.change]} ${file.path}`),
337
+ ].join('\n');
338
+ },
339
+ },
340
+ bash: {
341
+ description: 'Run a shell command on the hammerkit sandbox, as the shell task of the published build file, in its ' +
342
+ 'container. Commands run one at a time and nothing is kept between them but the files. They start in ' +
343
+ 'the root of the sandbox, where the paths of this machine do not exist: give them relative paths.',
344
+ args: (z) => ({
345
+ command: z.string().describe('The command to execute'),
346
+ timeout: z.number().int().min(1).optional().describe('Optional timeout in milliseconds'),
347
+ workdir: z
348
+ .string()
349
+ .optional()
350
+ .describe("The working directory to run the command in. Defaults to the current directory. Use this instead of 'cd' commands."),
351
+ }),
352
+ async execute(args, context) {
353
+ const sandbox = await client();
354
+ const ran = await sandbox.exec(args.command, where(context, args.workdir), {
355
+ signal: context.abort,
356
+ timeoutSeconds: args.timeout ? args.timeout / 1000 : undefined,
357
+ });
358
+ const lines = [(0, format_1.capOutput)(ran.output, 'narrow the command down, for example with grep, head or tail')];
359
+ if (ran.exitCode === null) {
360
+ lines.push(`hammerkit: ${ran.result?.error ?? ran.run.error ?? `the run ended ${ran.run.state}`}`);
361
+ }
362
+ else if (ran.exitCode !== 0) {
363
+ lines.push(`Exit code: ${ran.exitCode}`);
364
+ }
365
+ context.metadata?.({ metadata: { exit: ran.exitCode, run: ran.run.id } });
366
+ return lines.filter((line) => line.length > 0).join('\n\n') || '(no output)';
367
+ },
368
+ },
369
+ grep: {
370
+ description: 'Search the files of the hammerkit sandbox with a regular expression (ripgrep). Returns the matching ' +
371
+ 'lines with their file and line number.',
372
+ args: (z) => ({
373
+ pattern: z.string().describe('The regex pattern to search for in file contents'),
374
+ path: z.string().optional().describe('The directory to search in. Defaults to the current working directory.'),
375
+ include: z.string().optional().describe('File pattern to include in the search (e.g. "*.js", "*.{ts,tsx}")'),
376
+ }),
377
+ async execute(args, context) {
378
+ const sandbox = await client();
379
+ const result = await sandbox.search({
380
+ pattern: args.pattern,
381
+ path: where(context, args.path) || undefined,
382
+ glob: args.include ? [args.include] : undefined,
383
+ fixed_strings: args.literal || undefined,
384
+ i: args.caseSensitive === false || undefined,
385
+ output_mode: 'content',
386
+ head_limit: args.limit || SEARCH_LIMIT,
387
+ });
388
+ const matches = (result.matches ?? []).filter((match) => !match.context);
389
+ if (matches.length === 0) {
390
+ return 'No files found';
391
+ }
392
+ const lines = [`Found ${result.exact ? '' : 'at least '}${result.total} matches`];
393
+ let file = '';
394
+ for (const match of matches) {
395
+ if (match.path !== file) {
396
+ file = match.path;
397
+ lines.push('', `${(0, path_1.join)(context.directory, file)}:`);
398
+ }
399
+ lines.push(` Line ${match.line}: ${match.text}`);
400
+ }
401
+ if (result.truncated) {
402
+ lines.push('', '(Results are truncated. Consider using a more specific path or pattern.)');
403
+ }
404
+ return lines.join('\n');
405
+ },
406
+ },
407
+ glob: {
408
+ description: 'Find files of the hammerkit sandbox by a glob pattern like "**/*.js" or "src/**/*.ts".',
409
+ args: (z) => ({
410
+ pattern: z.string().describe('The glob pattern to match files against'),
411
+ path: z
412
+ .string()
413
+ .optional()
414
+ .describe('The directory to search in. If not specified, the current working directory will be used.'),
415
+ }),
416
+ async execute(args, context) {
417
+ const sandbox = await client();
418
+ const tree = await sandbox.tree({
419
+ path: where(context, args.path) || undefined,
420
+ glob: args.pattern,
421
+ limit: args.limit || SEARCH_LIMIT,
422
+ });
423
+ if (tree.entries.length === 0) {
424
+ return 'No files found';
425
+ }
426
+ const lines = tree.entries.map((entry) => (0, path_1.join)(context.directory, ...entry.path.split('/')));
427
+ if (tree.truncated) {
428
+ lines.push('', '(Results are truncated. Consider using a more specific path or pattern.)');
429
+ }
430
+ return lines.join('\n');
431
+ },
432
+ },
433
+ push: {
434
+ description: 'Commit the changes of the hammerkit sandbox and push them to a branch of its repository, for review. ' +
435
+ 'The server pushes only to the branches its operator allows; the sandbox is at the pushed commit afterwards.',
436
+ args: (z) => ({
437
+ branch: z.string().describe('The branch to push to, created when it does not exist'),
438
+ message: z.string().describe('The commit message'),
439
+ }),
440
+ async execute(args) {
441
+ const pushed = await (await client()).push(args.branch, args.message);
442
+ return { title: `pushed to ${pushed.branch}`, output: `pushed ${pushed.commit} to ${pushed.branch}` };
443
+ },
444
+ },
445
+ // opencode's list: a tree of the first files below a directory
446
+ list: {
447
+ description: 'List the files and directories below a directory of the hammerkit sandbox, as a tree. Prefer glob and ' +
448
+ 'grep when you know what you look for.',
449
+ args: (z) => ({
450
+ path: z
451
+ .string()
452
+ .optional()
453
+ .describe('The absolute path to the directory to list (must be absolute, not relative)'),
454
+ ignore: z.array(z.string()).optional().describe('List of glob patterns to ignore'),
455
+ }),
456
+ async execute(args, context) {
457
+ const sandbox = await client();
458
+ const path = where(context, args.path);
459
+ const tree = await sandbox.tree({ path: path || undefined, depth: 64, limit: LIST_LIMIT });
460
+ const prefix = path ? `${path}/` : '';
461
+ const ignore = (args.ignore ?? []).map((pattern) => new minimatch_1.Minimatch(pattern, { dot: true, matchBase: true }));
462
+ const lines = [`${args.path ?? context.directory}/`];
463
+ for (const entry of tree.entries) {
464
+ const relative = entry.path.startsWith(prefix) ? entry.path.slice(prefix.length) : entry.path;
465
+ if (ignore.some((pattern) => pattern.match(relative))) {
466
+ continue;
467
+ }
468
+ const parts = relative.split('/');
469
+ lines.push(`${' '.repeat(parts.length)}${parts[parts.length - 1]}${entry.type === 'directory' ? '/' : ''}`);
470
+ }
471
+ if (tree.truncated) {
472
+ lines.push('', `(${tree.entries.length} of ${tree.total} shown, list a directory below it or use glob)`);
473
+ }
474
+ return lines.join('\n');
475
+ },
476
+ },
477
+ });
478
+ }
479
+ // opencode runs the tool calls of a step in parallel, they reach the sandbox one at a time in their order
480
+ function inOrder(tools) {
481
+ const inTurn = (0, sandbox_client_1.oneAtATime)();
482
+ return Object.fromEntries(Object.entries(tools).map(([name, tool]) => [
483
+ name,
484
+ { ...tool, execute: (args, context) => inTurn(() => tool.execute(args, context)) },
485
+ ]));
486
+ }
487
+ // GPT models name the files of a patch by the absolute paths opencode shows them
488
+ function patchToSandbox(root, patch) {
489
+ return patch.replace(/^(\*\*\* (?:Add File|Update File|Delete File|Move to): )(.*?)([ \t\r]*)$/gm, (line, head, path, end) => (0, path_1.isAbsolute)(path) ? `${head}${(0, sandbox_client_1.toSandboxPath)(root, path)}${end}` : line);
490
+ }
491
+ exports.patchToSandbox = patchToSandbox;
492
+ //# sourceMappingURL=opencode.js.map