@giselles-ai/agent-kit 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +413 -0
- package/dist/index.d.ts +26 -0
- package/dist/index.js +320 -0
- package/package.json +47 -0
package/dist/cli.js
ADDED
|
@@ -0,0 +1,413 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
#!/usr/bin/env node
|
|
3
|
+
|
|
4
|
+
// src/cli.ts
|
|
5
|
+
import process from "process";
|
|
6
|
+
|
|
7
|
+
// src/build-snapshot.ts
|
|
8
|
+
import { readdir, readFile, stat } from "fs/promises";
|
|
9
|
+
import path from "path";
|
|
10
|
+
import { Sandbox } from "@vercel/sandbox";
|
|
11
|
+
|
|
12
|
+
// src/sandbox-utils.ts
|
|
13
|
+
async function runCommandOrThrow(sandbox, input) {
|
|
14
|
+
const result = await sandbox.runCommand({
|
|
15
|
+
cmd: input.cmd,
|
|
16
|
+
args: input.args,
|
|
17
|
+
cwd: input.cwd,
|
|
18
|
+
env: input.env
|
|
19
|
+
});
|
|
20
|
+
const stdout = await result.stdout().catch(() => "");
|
|
21
|
+
const stderr = await result.stderr().catch(() => "");
|
|
22
|
+
if (result.exitCode !== 0) {
|
|
23
|
+
const errorLines = [
|
|
24
|
+
`Command failed: ${input.cmd} ${(input.args ?? []).join(" ")}`,
|
|
25
|
+
`Exit code: ${result.exitCode}`
|
|
26
|
+
];
|
|
27
|
+
if (stdout.trim().length > 0) {
|
|
28
|
+
errorLines.push(`stdout:
|
|
29
|
+
${stdout}`);
|
|
30
|
+
}
|
|
31
|
+
if (stderr.trim().length > 0) {
|
|
32
|
+
errorLines.push(`stderr:
|
|
33
|
+
${stderr}`);
|
|
34
|
+
}
|
|
35
|
+
throw new Error(errorLines.join("\n\n"));
|
|
36
|
+
}
|
|
37
|
+
return { stdout, stderr };
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// src/build-snapshot.ts
|
|
41
|
+
var SKIP_DIR_NAMES = /* @__PURE__ */ new Set([
|
|
42
|
+
"node_modules",
|
|
43
|
+
"dist",
|
|
44
|
+
".next",
|
|
45
|
+
".git",
|
|
46
|
+
".jj"
|
|
47
|
+
]);
|
|
48
|
+
function toPosixPath(inputPath) {
|
|
49
|
+
return inputPath.split(path.sep).join("/");
|
|
50
|
+
}
|
|
51
|
+
async function collectFiles(targetPath, acc) {
|
|
52
|
+
const targetStat = await stat(targetPath);
|
|
53
|
+
if (targetStat.isFile()) {
|
|
54
|
+
acc.push(targetPath);
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
if (!targetStat.isDirectory()) {
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
const entries = await readdir(targetPath, { withFileTypes: true });
|
|
61
|
+
for (const entry of entries) {
|
|
62
|
+
const absolutePath = path.join(targetPath, entry.name);
|
|
63
|
+
if (entry.isDirectory()) {
|
|
64
|
+
if (SKIP_DIR_NAMES.has(entry.name)) {
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
await collectFiles(absolutePath, acc);
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
if (entry.isFile()) {
|
|
71
|
+
if (entry.name.endsWith(".tsbuildinfo")) {
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
acc.push(absolutePath);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
async function buildUploadFileList(repoRoot, includePaths) {
|
|
79
|
+
const files = [];
|
|
80
|
+
for (const relativePath of includePaths) {
|
|
81
|
+
const absolutePath = path.join(repoRoot, relativePath);
|
|
82
|
+
await collectFiles(absolutePath, files);
|
|
83
|
+
}
|
|
84
|
+
files.sort();
|
|
85
|
+
return files;
|
|
86
|
+
}
|
|
87
|
+
async function installAllAgentCLIs(sandbox) {
|
|
88
|
+
console.log("[snapshot] installing gemini cli...");
|
|
89
|
+
await runCommandOrThrow(sandbox, {
|
|
90
|
+
cmd: "npm",
|
|
91
|
+
args: ["install", "-g", "@google/gemini-cli"]
|
|
92
|
+
});
|
|
93
|
+
console.log("[snapshot] installing codex cli...");
|
|
94
|
+
await runCommandOrThrow(sandbox, {
|
|
95
|
+
cmd: "npm",
|
|
96
|
+
args: ["install", "-g", "@openai/codex"]
|
|
97
|
+
});
|
|
98
|
+
console.log("[snapshot] validating agent CLIs...");
|
|
99
|
+
await runCommandOrThrow(sandbox, {
|
|
100
|
+
cmd: "bash",
|
|
101
|
+
args: [
|
|
102
|
+
"-lc",
|
|
103
|
+
["set -e", "which gemini", "which codex", "codex --version"].join("\n")
|
|
104
|
+
]
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
async function setupBrowserToolFromNpm(sandbox, version) {
|
|
108
|
+
const packageSpec = version ? `@giselles-ai/browser-tool@${version}` : "@giselles-ai/browser-tool";
|
|
109
|
+
console.log(`[snapshot] installing ${packageSpec} globally...`);
|
|
110
|
+
await runCommandOrThrow(sandbox, {
|
|
111
|
+
cmd: "npm",
|
|
112
|
+
args: ["install", "-g", packageSpec]
|
|
113
|
+
});
|
|
114
|
+
console.log("[snapshot] locating mcp-server entry point...");
|
|
115
|
+
const { stdout: globalRoot } = await runCommandOrThrow(sandbox, {
|
|
116
|
+
cmd: "npm",
|
|
117
|
+
args: ["root", "-g"]
|
|
118
|
+
});
|
|
119
|
+
const mcpServerPath = `${globalRoot.trim()}/@giselles-ai/browser-tool/dist/mcp-server/index.js`;
|
|
120
|
+
console.log(`[snapshot] mcp-server resolved at: ${mcpServerPath}`);
|
|
121
|
+
return mcpServerPath;
|
|
122
|
+
}
|
|
123
|
+
async function setupBrowserToolFromLocal(sandbox, sandboxRoot, repoRoot, includePaths) {
|
|
124
|
+
console.log("[snapshot] collecting local files...");
|
|
125
|
+
const uploadFiles = await buildUploadFileList(repoRoot, includePaths);
|
|
126
|
+
if (uploadFiles.length === 0) {
|
|
127
|
+
throw new Error("No files collected for snapshot upload.");
|
|
128
|
+
}
|
|
129
|
+
console.log(`[snapshot] files to upload: ${uploadFiles.length}`);
|
|
130
|
+
const filesForSandbox = await Promise.all(
|
|
131
|
+
uploadFiles.map(async (absolutePath) => {
|
|
132
|
+
const relativePath = path.relative(repoRoot, absolutePath);
|
|
133
|
+
const content = await readFile(absolutePath);
|
|
134
|
+
return {
|
|
135
|
+
path: toPosixPath(path.join(sandboxRoot, relativePath)),
|
|
136
|
+
content
|
|
137
|
+
};
|
|
138
|
+
})
|
|
139
|
+
);
|
|
140
|
+
console.log("[snapshot] uploading repository files...");
|
|
141
|
+
await sandbox.writeFiles(filesForSandbox);
|
|
142
|
+
console.log("[snapshot] preparing pnpm...");
|
|
143
|
+
await runCommandOrThrow(sandbox, {
|
|
144
|
+
cmd: "bash",
|
|
145
|
+
args: ["-lc", ["set -e", "corepack pnpm --version"].join("\n")]
|
|
146
|
+
});
|
|
147
|
+
console.log("[snapshot] installing dependencies...");
|
|
148
|
+
await runCommandOrThrow(sandbox, {
|
|
149
|
+
cmd: "bash",
|
|
150
|
+
args: [
|
|
151
|
+
"-lc",
|
|
152
|
+
[
|
|
153
|
+
"set -e",
|
|
154
|
+
`cd ${sandboxRoot}`,
|
|
155
|
+
[
|
|
156
|
+
"corepack pnpm install --no-frozen-lockfile",
|
|
157
|
+
"--filter @giselles-ai/browser-tool..."
|
|
158
|
+
].join(" ")
|
|
159
|
+
].join("\n")
|
|
160
|
+
]
|
|
161
|
+
});
|
|
162
|
+
console.log("[snapshot] building browser-tool (mcp-server)...");
|
|
163
|
+
await runCommandOrThrow(sandbox, {
|
|
164
|
+
cmd: "bash",
|
|
165
|
+
args: [
|
|
166
|
+
"-lc",
|
|
167
|
+
[
|
|
168
|
+
"set -e",
|
|
169
|
+
`cd ${sandboxRoot}`,
|
|
170
|
+
"corepack pnpm --filter @giselles-ai/browser-tool run build"
|
|
171
|
+
].join("\n")
|
|
172
|
+
]
|
|
173
|
+
});
|
|
174
|
+
const mcpServerPath = `${sandboxRoot}/packages/browser-tool/dist/mcp-server/index.js`;
|
|
175
|
+
console.log("[snapshot] validating artifacts...");
|
|
176
|
+
await runCommandOrThrow(sandbox, {
|
|
177
|
+
cmd: "bash",
|
|
178
|
+
args: ["-lc", `set -e
|
|
179
|
+
test -f ${mcpServerPath}`]
|
|
180
|
+
});
|
|
181
|
+
return mcpServerPath;
|
|
182
|
+
}
|
|
183
|
+
function writeAgentConfigs(sandbox, mcpServerPath, sandboxRoot) {
|
|
184
|
+
const geminiSettings = {
|
|
185
|
+
security: {
|
|
186
|
+
auth: {
|
|
187
|
+
selectedType: "gemini-api-key"
|
|
188
|
+
}
|
|
189
|
+
},
|
|
190
|
+
mcpServers: {
|
|
191
|
+
browser_tool_relay: {
|
|
192
|
+
command: "node",
|
|
193
|
+
args: [mcpServerPath],
|
|
194
|
+
cwd: sandboxRoot,
|
|
195
|
+
env: {}
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
};
|
|
199
|
+
const codexConfigToml = `[mcp_servers.browser_tool_relay]
|
|
200
|
+
command = "node"
|
|
201
|
+
args = ["${mcpServerPath}"]
|
|
202
|
+
cwd = "${sandboxRoot}"
|
|
203
|
+
|
|
204
|
+
[mcp_servers.browser_tool_relay.env]
|
|
205
|
+
`;
|
|
206
|
+
return Promise.all([
|
|
207
|
+
sandbox.writeFiles([
|
|
208
|
+
{
|
|
209
|
+
path: "/home/vercel-sandbox/.gemini/settings.json",
|
|
210
|
+
content: Buffer.from(JSON.stringify(geminiSettings, null, 2))
|
|
211
|
+
}
|
|
212
|
+
]),
|
|
213
|
+
sandbox.writeFiles([
|
|
214
|
+
{
|
|
215
|
+
path: "/home/vercel-sandbox/.codex/config.toml",
|
|
216
|
+
content: Buffer.from(codexConfigToml)
|
|
217
|
+
}
|
|
218
|
+
])
|
|
219
|
+
]);
|
|
220
|
+
}
|
|
221
|
+
var DEFAULT_LOCAL_INCLUDE_PATHS = [
|
|
222
|
+
"package.json",
|
|
223
|
+
"pnpm-lock.yaml",
|
|
224
|
+
"pnpm-workspace.yaml",
|
|
225
|
+
"tsconfig.base.json",
|
|
226
|
+
"packages/browser-tool"
|
|
227
|
+
];
|
|
228
|
+
async function buildSnapshot(options = {}) {
|
|
229
|
+
const sandboxRoot = options.sandboxRoot ?? "/vercel/sandbox";
|
|
230
|
+
const runtime = options.runtime ?? "node24";
|
|
231
|
+
const timeoutMs = options.timeoutMs ?? 27e5;
|
|
232
|
+
const isLocal = options.local ?? false;
|
|
233
|
+
if (isLocal && !options.repoRoot) {
|
|
234
|
+
throw new Error("repoRoot is required when local mode is enabled.");
|
|
235
|
+
}
|
|
236
|
+
let baseSnapshotId = options.baseSnapshotId ?? "";
|
|
237
|
+
if (baseSnapshotId) {
|
|
238
|
+
console.log(`[snapshot] using existing base snapshot: ${baseSnapshotId}`);
|
|
239
|
+
} else {
|
|
240
|
+
console.log(
|
|
241
|
+
"[snapshot] BASE_SNAPSHOT_ID not set, creating base snapshot with all agent CLIs..."
|
|
242
|
+
);
|
|
243
|
+
const baseSandbox = await Sandbox.create({
|
|
244
|
+
runtime,
|
|
245
|
+
timeout: timeoutMs
|
|
246
|
+
});
|
|
247
|
+
console.log(`[snapshot] base sandbox created: ${baseSandbox.sandboxId}`);
|
|
248
|
+
try {
|
|
249
|
+
await installAllAgentCLIs(baseSandbox);
|
|
250
|
+
console.log("[snapshot] creating base snapshot...");
|
|
251
|
+
const baseSnapshot = await baseSandbox.snapshot();
|
|
252
|
+
baseSnapshotId = baseSnapshot.snapshotId;
|
|
253
|
+
console.log(`[snapshot] base snapshot created: ${baseSnapshotId}`);
|
|
254
|
+
console.log(
|
|
255
|
+
`[snapshot] reuse with: BASE_SNAPSHOT_ID="${baseSnapshotId}"`
|
|
256
|
+
);
|
|
257
|
+
} catch (error) {
|
|
258
|
+
console.error("[snapshot] base snapshot creation failed:");
|
|
259
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
260
|
+
try {
|
|
261
|
+
await baseSandbox.stop();
|
|
262
|
+
} catch {
|
|
263
|
+
}
|
|
264
|
+
throw error;
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
console.log(
|
|
268
|
+
`[snapshot] creating sandbox from base snapshot runtime=${runtime} timeoutMs=${timeoutMs}...`
|
|
269
|
+
);
|
|
270
|
+
const sandbox = await Sandbox.create({
|
|
271
|
+
source: { type: "snapshot", snapshotId: baseSnapshotId },
|
|
272
|
+
runtime,
|
|
273
|
+
timeout: timeoutMs
|
|
274
|
+
});
|
|
275
|
+
console.log(`[snapshot] sandbox created: ${sandbox.sandboxId}`);
|
|
276
|
+
try {
|
|
277
|
+
let mcpServerPath;
|
|
278
|
+
if (isLocal && options.repoRoot) {
|
|
279
|
+
mcpServerPath = await setupBrowserToolFromLocal(
|
|
280
|
+
sandbox,
|
|
281
|
+
sandboxRoot,
|
|
282
|
+
options.repoRoot,
|
|
283
|
+
options.localIncludePaths ?? DEFAULT_LOCAL_INCLUDE_PATHS
|
|
284
|
+
);
|
|
285
|
+
} else {
|
|
286
|
+
mcpServerPath = await setupBrowserToolFromNpm(
|
|
287
|
+
sandbox,
|
|
288
|
+
options.browserToolVersion
|
|
289
|
+
);
|
|
290
|
+
}
|
|
291
|
+
console.log("[snapshot] validating agent CLIs...");
|
|
292
|
+
await runCommandOrThrow(sandbox, {
|
|
293
|
+
cmd: "bash",
|
|
294
|
+
args: [
|
|
295
|
+
"-lc",
|
|
296
|
+
["set -e", "which gemini", "which codex", "codex --version"].join("\n")
|
|
297
|
+
]
|
|
298
|
+
});
|
|
299
|
+
console.log("[snapshot] writing agent configs...");
|
|
300
|
+
await writeAgentConfigs(sandbox, mcpServerPath, sandboxRoot);
|
|
301
|
+
console.log("[snapshot] creating snapshot (sandbox will be stopped)...");
|
|
302
|
+
const snapshot = await sandbox.snapshot();
|
|
303
|
+
console.log("\n=== Snapshot Created ===");
|
|
304
|
+
console.log(`snapshotId: ${snapshot.snapshotId}`);
|
|
305
|
+
console.log(`sourceSandboxId: ${snapshot.sourceSandboxId}`);
|
|
306
|
+
console.log(`status: ${snapshot.status}`);
|
|
307
|
+
console.log(`createdAt: ${snapshot.createdAt.toISOString()}`);
|
|
308
|
+
console.log(`expiresAt: ${snapshot.expiresAt?.toISOString()}`);
|
|
309
|
+
console.log("\nSet this in your .env.local:");
|
|
310
|
+
console.log(`SANDBOX_SNAPSHOT_ID=${snapshot.snapshotId}`);
|
|
311
|
+
return snapshot;
|
|
312
|
+
} catch (error) {
|
|
313
|
+
console.error("[snapshot] failed:");
|
|
314
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
315
|
+
console.error(`sandboxId: ${sandbox.sandboxId}`);
|
|
316
|
+
try {
|
|
317
|
+
await sandbox.stop();
|
|
318
|
+
console.error("[snapshot] sandbox stopped.");
|
|
319
|
+
} catch {
|
|
320
|
+
}
|
|
321
|
+
throw error;
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
// src/cli.ts
|
|
326
|
+
function printUsage() {
|
|
327
|
+
const usage = `Usage:
|
|
328
|
+
agent-kit build-snapshot [options]
|
|
329
|
+
|
|
330
|
+
Options:
|
|
331
|
+
--local Copy local files instead of npm install (monorepo only)
|
|
332
|
+
--repo-root <path> Monorepo root directory (required with --local)
|
|
333
|
+
--base-snapshot-id <id> Reuse an existing base snapshot
|
|
334
|
+
--sandbox-root <path> Sandbox working directory (default: /vercel/sandbox)
|
|
335
|
+
--runtime <runtime> Sandbox runtime (default: node24)
|
|
336
|
+
--timeout-ms <ms> Sandbox timeout in ms (default: 2700000)
|
|
337
|
+
--browser-tool-version <version> Version of @giselles-ai/browser-tool to install
|
|
338
|
+
--help, -h Show this help message
|
|
339
|
+
`;
|
|
340
|
+
process.stderr.write(usage);
|
|
341
|
+
}
|
|
342
|
+
function parseArgs(argv) {
|
|
343
|
+
const args = argv.slice(2);
|
|
344
|
+
const options = {};
|
|
345
|
+
let command;
|
|
346
|
+
for (let i = 0; i < args.length; i++) {
|
|
347
|
+
const arg = args[i];
|
|
348
|
+
if (arg === void 0) continue;
|
|
349
|
+
if (arg === "--help" || arg === "-h") {
|
|
350
|
+
options.help = true;
|
|
351
|
+
continue;
|
|
352
|
+
}
|
|
353
|
+
if (arg === "--local") {
|
|
354
|
+
options.local = true;
|
|
355
|
+
continue;
|
|
356
|
+
}
|
|
357
|
+
if (arg.startsWith("--") && i + 1 < args.length) {
|
|
358
|
+
const key = arg.slice(2);
|
|
359
|
+
i++;
|
|
360
|
+
const value = args[i];
|
|
361
|
+
if (value !== void 0) {
|
|
362
|
+
options[key] = value;
|
|
363
|
+
}
|
|
364
|
+
continue;
|
|
365
|
+
}
|
|
366
|
+
if (!arg.startsWith("-") && !command) {
|
|
367
|
+
command = arg;
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
return { command, options };
|
|
371
|
+
}
|
|
372
|
+
async function main() {
|
|
373
|
+
const { command, options } = parseArgs(process.argv);
|
|
374
|
+
if (options.help) {
|
|
375
|
+
printUsage();
|
|
376
|
+
process.exit(0);
|
|
377
|
+
}
|
|
378
|
+
if (command !== "build-snapshot") {
|
|
379
|
+
printUsage();
|
|
380
|
+
process.exit(command ? 1 : 1);
|
|
381
|
+
}
|
|
382
|
+
const isLocal = options.local === true;
|
|
383
|
+
const repoRoot = options["repo-root"] ?? process.env.BROWSER_TOOL_REPO_ROOT?.trim();
|
|
384
|
+
if (isLocal && !repoRoot) {
|
|
385
|
+
console.error("Error: --repo-root is required when --local is specified.");
|
|
386
|
+
process.exit(1);
|
|
387
|
+
}
|
|
388
|
+
const timeoutMsRaw = options["timeout-ms"] ?? process.env.BROWSER_TOOL_SNAPSHOT_TIMEOUT_MS?.trim();
|
|
389
|
+
const timeoutMs = timeoutMsRaw ? Number.parseInt(timeoutMsRaw, 10) : void 0;
|
|
390
|
+
if (timeoutMs !== void 0 && (!Number.isFinite(timeoutMs) || timeoutMs <= 0)) {
|
|
391
|
+
console.error(`Error: Invalid timeout-ms: ${timeoutMsRaw}`);
|
|
392
|
+
process.exit(1);
|
|
393
|
+
}
|
|
394
|
+
try {
|
|
395
|
+
await buildSnapshot({
|
|
396
|
+
local: isLocal,
|
|
397
|
+
repoRoot,
|
|
398
|
+
baseSnapshotId: options["base-snapshot-id"] ?? process.env.BASE_SNAPSHOT_ID?.trim(),
|
|
399
|
+
sandboxRoot: options["sandbox-root"] ?? process.env.BROWSER_TOOL_SANDBOX_ROOT?.trim(),
|
|
400
|
+
runtime: options.runtime ?? process.env.BROWSER_TOOL_SNAPSHOT_RUNTIME?.trim(),
|
|
401
|
+
timeoutMs,
|
|
402
|
+
browserToolVersion: options["browser-tool-version"]
|
|
403
|
+
});
|
|
404
|
+
} catch {
|
|
405
|
+
process.exitCode = 1;
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
main().catch((err) => {
|
|
409
|
+
console.error(
|
|
410
|
+
`Unexpected error: ${err instanceof Error ? err.message : String(err)}`
|
|
411
|
+
);
|
|
412
|
+
process.exit(1);
|
|
413
|
+
});
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import * as _vercel_sandbox from '@vercel/sandbox';
|
|
2
|
+
|
|
3
|
+
interface BuildSnapshotOptions {
|
|
4
|
+
sandboxRoot?: string;
|
|
5
|
+
runtime?: string;
|
|
6
|
+
timeoutMs?: number;
|
|
7
|
+
baseSnapshotId?: string;
|
|
8
|
+
browserToolVersion?: string;
|
|
9
|
+
/**
|
|
10
|
+
* When true, copy local files into the sandbox and build from source.
|
|
11
|
+
* When false (default), install @giselles-ai/browser-tool from npm.
|
|
12
|
+
*/
|
|
13
|
+
local?: boolean;
|
|
14
|
+
/**
|
|
15
|
+
* Required when `local` is true. The root directory of the monorepo.
|
|
16
|
+
*/
|
|
17
|
+
repoRoot?: string;
|
|
18
|
+
/**
|
|
19
|
+
* Paths to include when `local` is true.
|
|
20
|
+
* Defaults to the standard set for this monorepo.
|
|
21
|
+
*/
|
|
22
|
+
localIncludePaths?: string[];
|
|
23
|
+
}
|
|
24
|
+
declare function buildSnapshot(options?: BuildSnapshotOptions): Promise<_vercel_sandbox.Snapshot>;
|
|
25
|
+
|
|
26
|
+
export { type BuildSnapshotOptions, buildSnapshot };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,320 @@
|
|
|
1
|
+
// src/build-snapshot.ts
|
|
2
|
+
import { readdir, readFile, stat } from "fs/promises";
|
|
3
|
+
import path from "path";
|
|
4
|
+
import { Sandbox } from "@vercel/sandbox";
|
|
5
|
+
|
|
6
|
+
// src/sandbox-utils.ts
|
|
7
|
+
async function runCommandOrThrow(sandbox, input) {
|
|
8
|
+
const result = await sandbox.runCommand({
|
|
9
|
+
cmd: input.cmd,
|
|
10
|
+
args: input.args,
|
|
11
|
+
cwd: input.cwd,
|
|
12
|
+
env: input.env
|
|
13
|
+
});
|
|
14
|
+
const stdout = await result.stdout().catch(() => "");
|
|
15
|
+
const stderr = await result.stderr().catch(() => "");
|
|
16
|
+
if (result.exitCode !== 0) {
|
|
17
|
+
const errorLines = [
|
|
18
|
+
`Command failed: ${input.cmd} ${(input.args ?? []).join(" ")}`,
|
|
19
|
+
`Exit code: ${result.exitCode}`
|
|
20
|
+
];
|
|
21
|
+
if (stdout.trim().length > 0) {
|
|
22
|
+
errorLines.push(`stdout:
|
|
23
|
+
${stdout}`);
|
|
24
|
+
}
|
|
25
|
+
if (stderr.trim().length > 0) {
|
|
26
|
+
errorLines.push(`stderr:
|
|
27
|
+
${stderr}`);
|
|
28
|
+
}
|
|
29
|
+
throw new Error(errorLines.join("\n\n"));
|
|
30
|
+
}
|
|
31
|
+
return { stdout, stderr };
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// src/build-snapshot.ts
|
|
35
|
+
var SKIP_DIR_NAMES = /* @__PURE__ */ new Set([
|
|
36
|
+
"node_modules",
|
|
37
|
+
"dist",
|
|
38
|
+
".next",
|
|
39
|
+
".git",
|
|
40
|
+
".jj"
|
|
41
|
+
]);
|
|
42
|
+
function toPosixPath(inputPath) {
|
|
43
|
+
return inputPath.split(path.sep).join("/");
|
|
44
|
+
}
|
|
45
|
+
async function collectFiles(targetPath, acc) {
|
|
46
|
+
const targetStat = await stat(targetPath);
|
|
47
|
+
if (targetStat.isFile()) {
|
|
48
|
+
acc.push(targetPath);
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
if (!targetStat.isDirectory()) {
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
const entries = await readdir(targetPath, { withFileTypes: true });
|
|
55
|
+
for (const entry of entries) {
|
|
56
|
+
const absolutePath = path.join(targetPath, entry.name);
|
|
57
|
+
if (entry.isDirectory()) {
|
|
58
|
+
if (SKIP_DIR_NAMES.has(entry.name)) {
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
await collectFiles(absolutePath, acc);
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
if (entry.isFile()) {
|
|
65
|
+
if (entry.name.endsWith(".tsbuildinfo")) {
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
acc.push(absolutePath);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
async function buildUploadFileList(repoRoot, includePaths) {
|
|
73
|
+
const files = [];
|
|
74
|
+
for (const relativePath of includePaths) {
|
|
75
|
+
const absolutePath = path.join(repoRoot, relativePath);
|
|
76
|
+
await collectFiles(absolutePath, files);
|
|
77
|
+
}
|
|
78
|
+
files.sort();
|
|
79
|
+
return files;
|
|
80
|
+
}
|
|
81
|
+
async function installAllAgentCLIs(sandbox) {
|
|
82
|
+
console.log("[snapshot] installing gemini cli...");
|
|
83
|
+
await runCommandOrThrow(sandbox, {
|
|
84
|
+
cmd: "npm",
|
|
85
|
+
args: ["install", "-g", "@google/gemini-cli"]
|
|
86
|
+
});
|
|
87
|
+
console.log("[snapshot] installing codex cli...");
|
|
88
|
+
await runCommandOrThrow(sandbox, {
|
|
89
|
+
cmd: "npm",
|
|
90
|
+
args: ["install", "-g", "@openai/codex"]
|
|
91
|
+
});
|
|
92
|
+
console.log("[snapshot] validating agent CLIs...");
|
|
93
|
+
await runCommandOrThrow(sandbox, {
|
|
94
|
+
cmd: "bash",
|
|
95
|
+
args: [
|
|
96
|
+
"-lc",
|
|
97
|
+
["set -e", "which gemini", "which codex", "codex --version"].join("\n")
|
|
98
|
+
]
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
async function setupBrowserToolFromNpm(sandbox, version) {
|
|
102
|
+
const packageSpec = version ? `@giselles-ai/browser-tool@${version}` : "@giselles-ai/browser-tool";
|
|
103
|
+
console.log(`[snapshot] installing ${packageSpec} globally...`);
|
|
104
|
+
await runCommandOrThrow(sandbox, {
|
|
105
|
+
cmd: "npm",
|
|
106
|
+
args: ["install", "-g", packageSpec]
|
|
107
|
+
});
|
|
108
|
+
console.log("[snapshot] locating mcp-server entry point...");
|
|
109
|
+
const { stdout: globalRoot } = await runCommandOrThrow(sandbox, {
|
|
110
|
+
cmd: "npm",
|
|
111
|
+
args: ["root", "-g"]
|
|
112
|
+
});
|
|
113
|
+
const mcpServerPath = `${globalRoot.trim()}/@giselles-ai/browser-tool/dist/mcp-server/index.js`;
|
|
114
|
+
console.log(`[snapshot] mcp-server resolved at: ${mcpServerPath}`);
|
|
115
|
+
return mcpServerPath;
|
|
116
|
+
}
|
|
117
|
+
async function setupBrowserToolFromLocal(sandbox, sandboxRoot, repoRoot, includePaths) {
|
|
118
|
+
console.log("[snapshot] collecting local files...");
|
|
119
|
+
const uploadFiles = await buildUploadFileList(repoRoot, includePaths);
|
|
120
|
+
if (uploadFiles.length === 0) {
|
|
121
|
+
throw new Error("No files collected for snapshot upload.");
|
|
122
|
+
}
|
|
123
|
+
console.log(`[snapshot] files to upload: ${uploadFiles.length}`);
|
|
124
|
+
const filesForSandbox = await Promise.all(
|
|
125
|
+
uploadFiles.map(async (absolutePath) => {
|
|
126
|
+
const relativePath = path.relative(repoRoot, absolutePath);
|
|
127
|
+
const content = await readFile(absolutePath);
|
|
128
|
+
return {
|
|
129
|
+
path: toPosixPath(path.join(sandboxRoot, relativePath)),
|
|
130
|
+
content
|
|
131
|
+
};
|
|
132
|
+
})
|
|
133
|
+
);
|
|
134
|
+
console.log("[snapshot] uploading repository files...");
|
|
135
|
+
await sandbox.writeFiles(filesForSandbox);
|
|
136
|
+
console.log("[snapshot] preparing pnpm...");
|
|
137
|
+
await runCommandOrThrow(sandbox, {
|
|
138
|
+
cmd: "bash",
|
|
139
|
+
args: ["-lc", ["set -e", "corepack pnpm --version"].join("\n")]
|
|
140
|
+
});
|
|
141
|
+
console.log("[snapshot] installing dependencies...");
|
|
142
|
+
await runCommandOrThrow(sandbox, {
|
|
143
|
+
cmd: "bash",
|
|
144
|
+
args: [
|
|
145
|
+
"-lc",
|
|
146
|
+
[
|
|
147
|
+
"set -e",
|
|
148
|
+
`cd ${sandboxRoot}`,
|
|
149
|
+
[
|
|
150
|
+
"corepack pnpm install --no-frozen-lockfile",
|
|
151
|
+
"--filter @giselles-ai/browser-tool..."
|
|
152
|
+
].join(" ")
|
|
153
|
+
].join("\n")
|
|
154
|
+
]
|
|
155
|
+
});
|
|
156
|
+
console.log("[snapshot] building browser-tool (mcp-server)...");
|
|
157
|
+
await runCommandOrThrow(sandbox, {
|
|
158
|
+
cmd: "bash",
|
|
159
|
+
args: [
|
|
160
|
+
"-lc",
|
|
161
|
+
[
|
|
162
|
+
"set -e",
|
|
163
|
+
`cd ${sandboxRoot}`,
|
|
164
|
+
"corepack pnpm --filter @giselles-ai/browser-tool run build"
|
|
165
|
+
].join("\n")
|
|
166
|
+
]
|
|
167
|
+
});
|
|
168
|
+
const mcpServerPath = `${sandboxRoot}/packages/browser-tool/dist/mcp-server/index.js`;
|
|
169
|
+
console.log("[snapshot] validating artifacts...");
|
|
170
|
+
await runCommandOrThrow(sandbox, {
|
|
171
|
+
cmd: "bash",
|
|
172
|
+
args: ["-lc", `set -e
|
|
173
|
+
test -f ${mcpServerPath}`]
|
|
174
|
+
});
|
|
175
|
+
return mcpServerPath;
|
|
176
|
+
}
|
|
177
|
+
function writeAgentConfigs(sandbox, mcpServerPath, sandboxRoot) {
|
|
178
|
+
const geminiSettings = {
|
|
179
|
+
security: {
|
|
180
|
+
auth: {
|
|
181
|
+
selectedType: "gemini-api-key"
|
|
182
|
+
}
|
|
183
|
+
},
|
|
184
|
+
mcpServers: {
|
|
185
|
+
browser_tool_relay: {
|
|
186
|
+
command: "node",
|
|
187
|
+
args: [mcpServerPath],
|
|
188
|
+
cwd: sandboxRoot,
|
|
189
|
+
env: {}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
};
|
|
193
|
+
const codexConfigToml = `[mcp_servers.browser_tool_relay]
|
|
194
|
+
command = "node"
|
|
195
|
+
args = ["${mcpServerPath}"]
|
|
196
|
+
cwd = "${sandboxRoot}"
|
|
197
|
+
|
|
198
|
+
[mcp_servers.browser_tool_relay.env]
|
|
199
|
+
`;
|
|
200
|
+
return Promise.all([
|
|
201
|
+
sandbox.writeFiles([
|
|
202
|
+
{
|
|
203
|
+
path: "/home/vercel-sandbox/.gemini/settings.json",
|
|
204
|
+
content: Buffer.from(JSON.stringify(geminiSettings, null, 2))
|
|
205
|
+
}
|
|
206
|
+
]),
|
|
207
|
+
sandbox.writeFiles([
|
|
208
|
+
{
|
|
209
|
+
path: "/home/vercel-sandbox/.codex/config.toml",
|
|
210
|
+
content: Buffer.from(codexConfigToml)
|
|
211
|
+
}
|
|
212
|
+
])
|
|
213
|
+
]);
|
|
214
|
+
}
|
|
215
|
+
var DEFAULT_LOCAL_INCLUDE_PATHS = [
|
|
216
|
+
"package.json",
|
|
217
|
+
"pnpm-lock.yaml",
|
|
218
|
+
"pnpm-workspace.yaml",
|
|
219
|
+
"tsconfig.base.json",
|
|
220
|
+
"packages/browser-tool"
|
|
221
|
+
];
|
|
222
|
+
async function buildSnapshot(options = {}) {
|
|
223
|
+
const sandboxRoot = options.sandboxRoot ?? "/vercel/sandbox";
|
|
224
|
+
const runtime = options.runtime ?? "node24";
|
|
225
|
+
const timeoutMs = options.timeoutMs ?? 27e5;
|
|
226
|
+
const isLocal = options.local ?? false;
|
|
227
|
+
if (isLocal && !options.repoRoot) {
|
|
228
|
+
throw new Error("repoRoot is required when local mode is enabled.");
|
|
229
|
+
}
|
|
230
|
+
let baseSnapshotId = options.baseSnapshotId ?? "";
|
|
231
|
+
if (baseSnapshotId) {
|
|
232
|
+
console.log(`[snapshot] using existing base snapshot: ${baseSnapshotId}`);
|
|
233
|
+
} else {
|
|
234
|
+
console.log(
|
|
235
|
+
"[snapshot] BASE_SNAPSHOT_ID not set, creating base snapshot with all agent CLIs..."
|
|
236
|
+
);
|
|
237
|
+
const baseSandbox = await Sandbox.create({
|
|
238
|
+
runtime,
|
|
239
|
+
timeout: timeoutMs
|
|
240
|
+
});
|
|
241
|
+
console.log(`[snapshot] base sandbox created: ${baseSandbox.sandboxId}`);
|
|
242
|
+
try {
|
|
243
|
+
await installAllAgentCLIs(baseSandbox);
|
|
244
|
+
console.log("[snapshot] creating base snapshot...");
|
|
245
|
+
const baseSnapshot = await baseSandbox.snapshot();
|
|
246
|
+
baseSnapshotId = baseSnapshot.snapshotId;
|
|
247
|
+
console.log(`[snapshot] base snapshot created: ${baseSnapshotId}`);
|
|
248
|
+
console.log(
|
|
249
|
+
`[snapshot] reuse with: BASE_SNAPSHOT_ID="${baseSnapshotId}"`
|
|
250
|
+
);
|
|
251
|
+
} catch (error) {
|
|
252
|
+
console.error("[snapshot] base snapshot creation failed:");
|
|
253
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
254
|
+
try {
|
|
255
|
+
await baseSandbox.stop();
|
|
256
|
+
} catch {
|
|
257
|
+
}
|
|
258
|
+
throw error;
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
console.log(
|
|
262
|
+
`[snapshot] creating sandbox from base snapshot runtime=${runtime} timeoutMs=${timeoutMs}...`
|
|
263
|
+
);
|
|
264
|
+
const sandbox = await Sandbox.create({
|
|
265
|
+
source: { type: "snapshot", snapshotId: baseSnapshotId },
|
|
266
|
+
runtime,
|
|
267
|
+
timeout: timeoutMs
|
|
268
|
+
});
|
|
269
|
+
console.log(`[snapshot] sandbox created: ${sandbox.sandboxId}`);
|
|
270
|
+
try {
|
|
271
|
+
let mcpServerPath;
|
|
272
|
+
if (isLocal && options.repoRoot) {
|
|
273
|
+
mcpServerPath = await setupBrowserToolFromLocal(
|
|
274
|
+
sandbox,
|
|
275
|
+
sandboxRoot,
|
|
276
|
+
options.repoRoot,
|
|
277
|
+
options.localIncludePaths ?? DEFAULT_LOCAL_INCLUDE_PATHS
|
|
278
|
+
);
|
|
279
|
+
} else {
|
|
280
|
+
mcpServerPath = await setupBrowserToolFromNpm(
|
|
281
|
+
sandbox,
|
|
282
|
+
options.browserToolVersion
|
|
283
|
+
);
|
|
284
|
+
}
|
|
285
|
+
console.log("[snapshot] validating agent CLIs...");
|
|
286
|
+
await runCommandOrThrow(sandbox, {
|
|
287
|
+
cmd: "bash",
|
|
288
|
+
args: [
|
|
289
|
+
"-lc",
|
|
290
|
+
["set -e", "which gemini", "which codex", "codex --version"].join("\n")
|
|
291
|
+
]
|
|
292
|
+
});
|
|
293
|
+
console.log("[snapshot] writing agent configs...");
|
|
294
|
+
await writeAgentConfigs(sandbox, mcpServerPath, sandboxRoot);
|
|
295
|
+
console.log("[snapshot] creating snapshot (sandbox will be stopped)...");
|
|
296
|
+
const snapshot = await sandbox.snapshot();
|
|
297
|
+
console.log("\n=== Snapshot Created ===");
|
|
298
|
+
console.log(`snapshotId: ${snapshot.snapshotId}`);
|
|
299
|
+
console.log(`sourceSandboxId: ${snapshot.sourceSandboxId}`);
|
|
300
|
+
console.log(`status: ${snapshot.status}`);
|
|
301
|
+
console.log(`createdAt: ${snapshot.createdAt.toISOString()}`);
|
|
302
|
+
console.log(`expiresAt: ${snapshot.expiresAt?.toISOString()}`);
|
|
303
|
+
console.log("\nSet this in your .env.local:");
|
|
304
|
+
console.log(`SANDBOX_SNAPSHOT_ID=${snapshot.snapshotId}`);
|
|
305
|
+
return snapshot;
|
|
306
|
+
} catch (error) {
|
|
307
|
+
console.error("[snapshot] failed:");
|
|
308
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
309
|
+
console.error(`sandboxId: ${sandbox.sandboxId}`);
|
|
310
|
+
try {
|
|
311
|
+
await sandbox.stop();
|
|
312
|
+
console.error("[snapshot] sandbox stopped.");
|
|
313
|
+
} catch {
|
|
314
|
+
}
|
|
315
|
+
throw error;
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
export {
|
|
319
|
+
buildSnapshot
|
|
320
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@giselles-ai/agent-kit",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"private": false,
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"agent-kit": "./dist/cli.js"
|
|
8
|
+
},
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"types": "./dist/index.d.ts",
|
|
12
|
+
"import": "./dist/index.js",
|
|
13
|
+
"default": "./dist/index.js"
|
|
14
|
+
}
|
|
15
|
+
},
|
|
16
|
+
"scripts": {
|
|
17
|
+
"build": "pnpm clean && tsup --config tsup.ts",
|
|
18
|
+
"clean": "rm -rf dist *.tsbuildinfo",
|
|
19
|
+
"dev": "tsx src/cli.ts",
|
|
20
|
+
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
21
|
+
"format": "pnpm exec biome check --write ."
|
|
22
|
+
},
|
|
23
|
+
"files": [
|
|
24
|
+
"dist"
|
|
25
|
+
],
|
|
26
|
+
"license": "Apache-2.0",
|
|
27
|
+
"repository": {
|
|
28
|
+
"type": "git",
|
|
29
|
+
"url": "https://github.com/giselles-ai/agent-container.git",
|
|
30
|
+
"directory": "packages/agent-kit"
|
|
31
|
+
},
|
|
32
|
+
"engines": {
|
|
33
|
+
"node": ">=20"
|
|
34
|
+
},
|
|
35
|
+
"publishConfig": {
|
|
36
|
+
"access": "public"
|
|
37
|
+
},
|
|
38
|
+
"dependencies": {
|
|
39
|
+
"@vercel/sandbox": "1.8.0"
|
|
40
|
+
},
|
|
41
|
+
"devDependencies": {
|
|
42
|
+
"@types/node": "25.3.0",
|
|
43
|
+
"tsup": "8.5.1",
|
|
44
|
+
"tsx": "4.21.0",
|
|
45
|
+
"typescript": "5.9.3"
|
|
46
|
+
}
|
|
47
|
+
}
|