@sanlabs/sanbox-cli 0.0.1 → 0.0.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +107 -7
- package/dist/activity.js +72 -0
- package/dist/api.js +109 -5
- package/dist/args.js +23 -7
- package/dist/artifacts.js +124 -0
- package/dist/cli.js +1219 -86
- package/dist/config.js +57 -9
- package/dist/errors.js +24 -0
- package/dist/inputs.js +150 -0
- package/dist/output.js +77 -1
- package/dist/runs.js +13 -25
- package/dist/version.js +1 -1
- package/dist/watch.js +170 -0
- package/package.json +9 -6
- package/dist/dossier.js +0 -108
- package/dist/mcp.js +0 -131
package/dist/dossier.js
DELETED
|
@@ -1,108 +0,0 @@
|
|
|
1
|
-
import crypto from "node:crypto";
|
|
2
|
-
import fs from "node:fs/promises";
|
|
3
|
-
import path from "node:path";
|
|
4
|
-
import fg from "fast-glob";
|
|
5
|
-
import ignore from "ignore";
|
|
6
|
-
import yazl from "yazl";
|
|
7
|
-
const defaultIgnorePatterns = [
|
|
8
|
-
".git/**",
|
|
9
|
-
"**/.git/**",
|
|
10
|
-
"node_modules/**",
|
|
11
|
-
"**/node_modules/**",
|
|
12
|
-
"dist/**",
|
|
13
|
-
"**/dist/**",
|
|
14
|
-
"build/**",
|
|
15
|
-
"**/build/**",
|
|
16
|
-
"coverage/**",
|
|
17
|
-
"**/coverage/**",
|
|
18
|
-
".next/**",
|
|
19
|
-
"**/.next/**",
|
|
20
|
-
".turbo/**",
|
|
21
|
-
"**/.turbo/**",
|
|
22
|
-
".env",
|
|
23
|
-
".env.*",
|
|
24
|
-
"**/.env",
|
|
25
|
-
"**/.env.*",
|
|
26
|
-
"**/*secret*",
|
|
27
|
-
"**/*Secret*",
|
|
28
|
-
"**/*token*",
|
|
29
|
-
"**/*Token*",
|
|
30
|
-
"**/*credential*",
|
|
31
|
-
"**/*Credential*",
|
|
32
|
-
"**/*.pem",
|
|
33
|
-
"**/*.key",
|
|
34
|
-
"**/id_rsa",
|
|
35
|
-
"**/id_ed25519"
|
|
36
|
-
];
|
|
37
|
-
const normalizeInclude = (patterns) => patterns.length === 0 ? ["**/*"] : patterns.map((pattern) => (pattern === "." ? "**/*" : pattern));
|
|
38
|
-
const readSanboxIgnore = async (cwd) => {
|
|
39
|
-
try {
|
|
40
|
-
const raw = await fs.readFile(path.join(cwd, ".sanboxignore"), "utf8");
|
|
41
|
-
return raw.split(/\r?\n/).map((line) => line.trim()).filter((line) => line && !line.startsWith("#"));
|
|
42
|
-
}
|
|
43
|
-
catch (error) {
|
|
44
|
-
if (error.code === "ENOENT")
|
|
45
|
-
return [];
|
|
46
|
-
throw error;
|
|
47
|
-
}
|
|
48
|
-
};
|
|
49
|
-
const zipToBuffer = async (zip) => new Promise((resolve, reject) => {
|
|
50
|
-
const chunks = [];
|
|
51
|
-
zip.outputStream.on("data", (chunk) => chunks.push(chunk));
|
|
52
|
-
zip.outputStream.on("error", reject);
|
|
53
|
-
zip.outputStream.on("end", () => resolve(Buffer.concat(chunks)));
|
|
54
|
-
zip.end();
|
|
55
|
-
});
|
|
56
|
-
export const sha256 = (buffer) => crypto.createHash("sha256").update(buffer).digest("hex");
|
|
57
|
-
export const buildTaskDossier = async (input) => {
|
|
58
|
-
const patterns = normalizeInclude(input.include);
|
|
59
|
-
const ig = ignore().add([...defaultIgnorePatterns, ...(await readSanboxIgnore(input.cwd))]);
|
|
60
|
-
const candidates = await fg(patterns, {
|
|
61
|
-
cwd: input.cwd,
|
|
62
|
-
dot: true,
|
|
63
|
-
onlyFiles: true,
|
|
64
|
-
followSymbolicLinks: false,
|
|
65
|
-
unique: true
|
|
66
|
-
});
|
|
67
|
-
const files = candidates
|
|
68
|
-
.map((item) => item.replaceAll(path.sep, "/"))
|
|
69
|
-
.filter((item) => !item.startsWith("../") && !path.isAbsolute(item))
|
|
70
|
-
.filter((item) => !ig.ignores(item))
|
|
71
|
-
.sort();
|
|
72
|
-
const zip = new yazl.ZipFile();
|
|
73
|
-
const fileMetadata = [];
|
|
74
|
-
const runbook = [
|
|
75
|
-
"# Sanbox Task",
|
|
76
|
-
"",
|
|
77
|
-
"Follow these instructions exactly.",
|
|
78
|
-
"",
|
|
79
|
-
input.task.trim(),
|
|
80
|
-
"",
|
|
81
|
-
"Write durable results under `/workspace/output`.",
|
|
82
|
-
"Use files under `/workspace/dossier/input/repo` as the provided source context."
|
|
83
|
-
].join("\n");
|
|
84
|
-
zip.addBuffer(Buffer.from(runbook, "utf8"), "RUNBOOK.md");
|
|
85
|
-
for (const rel of files) {
|
|
86
|
-
const abs = path.join(input.cwd, rel);
|
|
87
|
-
const stat = await fs.stat(abs);
|
|
88
|
-
if (!stat.isFile())
|
|
89
|
-
continue;
|
|
90
|
-
const content = await fs.readFile(abs);
|
|
91
|
-
fileMetadata.push({ path: rel, size: stat.size, sha256: sha256(content) });
|
|
92
|
-
zip.addBuffer(content, `input/repo/${rel}`);
|
|
93
|
-
}
|
|
94
|
-
const manifest = {
|
|
95
|
-
source: "sanbox-cli",
|
|
96
|
-
cli_version: input.cliVersion,
|
|
97
|
-
created_at: new Date().toISOString(),
|
|
98
|
-
task: input.task,
|
|
99
|
-
files: fileMetadata
|
|
100
|
-
};
|
|
101
|
-
zip.addBuffer(Buffer.from(`${JSON.stringify(manifest, null, 2)}\n`, "utf8"), "manifest.json");
|
|
102
|
-
const buffer = await zipToBuffer(zip);
|
|
103
|
-
return { buffer, sha256: sha256(buffer), files: fileMetadata };
|
|
104
|
-
};
|
|
105
|
-
export const readDossierFile = async (dossierPath) => {
|
|
106
|
-
const buffer = await fs.readFile(dossierPath);
|
|
107
|
-
return { buffer, sha256: sha256(buffer), files: [] };
|
|
108
|
-
};
|
package/dist/mcp.js
DELETED
|
@@ -1,131 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
3
|
-
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
4
|
-
import * as z from "zod/v4";
|
|
5
|
-
import { SanboxClient } from "./api.js";
|
|
6
|
-
import { defaultWorkloadId, readConfig } from "./config.js";
|
|
7
|
-
import { createRun, readTasks, runPool, stableBatchId, waitForRun } from "./runs.js";
|
|
8
|
-
import { version } from "./version.js";
|
|
9
|
-
const textAndJson = (summary, structuredContent) => ({
|
|
10
|
-
content: [{ type: "text", text: `${summary}\n\n${JSON.stringify(structuredContent, null, 2)}` }],
|
|
11
|
-
structuredContent
|
|
12
|
-
});
|
|
13
|
-
const client = () => new SanboxClient(readConfig());
|
|
14
|
-
const server = new McpServer({
|
|
15
|
-
name: "sanbox",
|
|
16
|
-
version
|
|
17
|
-
});
|
|
18
|
-
server.registerTool("create_sandbox_run", {
|
|
19
|
-
title: "Create Sanbox Run",
|
|
20
|
-
description: "Create one Sanbox run from a task or existing dossier ZIP.",
|
|
21
|
-
inputSchema: {
|
|
22
|
-
task: z.string().optional().describe("Task instructions. Required unless dossier_path is provided."),
|
|
23
|
-
dossier_path: z.string().optional().describe("Path to an existing dossier ZIP."),
|
|
24
|
-
include: z.array(z.string()).optional().describe("Repo file globs to include when task is provided."),
|
|
25
|
-
external_run_id: z.string().optional().describe("Optional idempotency key."),
|
|
26
|
-
workload_id: z.string().optional().default(defaultWorkloadId),
|
|
27
|
-
wait: z.boolean().optional().default(false)
|
|
28
|
-
}
|
|
29
|
-
}, async (args) => {
|
|
30
|
-
if (!args.task && !args.dossier_path)
|
|
31
|
-
throw new Error("task or dossier_path is required.");
|
|
32
|
-
const api = client();
|
|
33
|
-
let payload = await createRun(api, {
|
|
34
|
-
cwd: process.cwd(),
|
|
35
|
-
cliVersion: version,
|
|
36
|
-
task: args.task,
|
|
37
|
-
dossierPath: args.dossier_path,
|
|
38
|
-
include: args.include || [],
|
|
39
|
-
externalRunId: args.external_run_id,
|
|
40
|
-
workloadId: args.workload_id || defaultWorkloadId
|
|
41
|
-
});
|
|
42
|
-
if (args.wait)
|
|
43
|
-
payload = await waitForRun(api, payload.run.id);
|
|
44
|
-
return textAndJson(`Run ${payload.run.id} is ${payload.run.status}.`, payload);
|
|
45
|
-
});
|
|
46
|
-
server.registerTool("create_sandbox_runs", {
|
|
47
|
-
title: "Create Multiple Sanbox Runs",
|
|
48
|
-
description: "Create independent Sanbox runs for parallel task delegation.",
|
|
49
|
-
inputSchema: {
|
|
50
|
-
tasks: z.array(z.object({
|
|
51
|
-
task: z.string(),
|
|
52
|
-
external_run_id: z.string().optional(),
|
|
53
|
-
include: z.array(z.string()).optional()
|
|
54
|
-
})).optional().describe("Inline tasks to run."),
|
|
55
|
-
tasks_path: z.string().optional().describe("Path to a JSON task array."),
|
|
56
|
-
include: z.array(z.string()).optional().describe("Default repo file globs for generated dossiers."),
|
|
57
|
-
batch_id: z.string().optional().describe("Optional stable batch id for idempotency."),
|
|
58
|
-
max_parallel: z.number().int().min(1).max(20).optional().default(5),
|
|
59
|
-
wait: z.boolean().optional().default(false)
|
|
60
|
-
}
|
|
61
|
-
}, async (args) => {
|
|
62
|
-
const tasks = args.tasks_path ? await readTasks(args.tasks_path) : args.tasks;
|
|
63
|
-
if (!tasks || tasks.length === 0)
|
|
64
|
-
throw new Error("tasks or tasks_path is required.");
|
|
65
|
-
const batchId = args.batch_id || (args.tasks_path ? await stableBatchId(args.tasks_path) : `mcp-${Date.now().toString(36)}`);
|
|
66
|
-
const api = client();
|
|
67
|
-
const results = await runPool(tasks, args.max_parallel || 5, async (task, index) => {
|
|
68
|
-
let payload = await createRun(api, {
|
|
69
|
-
cwd: process.cwd(),
|
|
70
|
-
cliVersion: version,
|
|
71
|
-
task: task.task,
|
|
72
|
-
include: task.include || args.include || [],
|
|
73
|
-
externalRunId: task.external_run_id || `sanbox-batch-${batchId}-${index + 1}`,
|
|
74
|
-
workloadId: defaultWorkloadId
|
|
75
|
-
});
|
|
76
|
-
if (args.wait)
|
|
77
|
-
payload = await waitForRun(api, payload.run.id);
|
|
78
|
-
return payload;
|
|
79
|
-
});
|
|
80
|
-
const structured = { batch_id: batchId, runs: results.map((result) => result.run), results };
|
|
81
|
-
return textAndJson(`Created ${results.length} Sanbox runs.`, structured);
|
|
82
|
-
});
|
|
83
|
-
server.registerTool("get_sandbox_run", {
|
|
84
|
-
title: "Get Sanbox Run",
|
|
85
|
-
description: "Fetch a Sanbox run and recent events.",
|
|
86
|
-
inputSchema: {
|
|
87
|
-
run_id: z.string()
|
|
88
|
-
}
|
|
89
|
-
}, async ({ run_id }) => {
|
|
90
|
-
const payload = await client().getRun(run_id);
|
|
91
|
-
return textAndJson(`Run ${payload.run.id} is ${payload.run.status}.`, payload);
|
|
92
|
-
});
|
|
93
|
-
server.registerTool("list_sandbox_events", {
|
|
94
|
-
title: "List Sanbox Events",
|
|
95
|
-
description: "List events for a Sanbox run.",
|
|
96
|
-
inputSchema: {
|
|
97
|
-
run_id: z.string(),
|
|
98
|
-
after_event_id: z.number().int().min(0).optional().default(0)
|
|
99
|
-
}
|
|
100
|
-
}, async ({ run_id, after_event_id }) => {
|
|
101
|
-
const payload = await client().listEvents(run_id, after_event_id || 0);
|
|
102
|
-
return textAndJson(`Found ${payload.events.length} events.`, payload);
|
|
103
|
-
});
|
|
104
|
-
server.registerTool("cancel_sandbox_run", {
|
|
105
|
-
title: "Cancel Sanbox Run",
|
|
106
|
-
description: "Request cancellation for a queued or running Sanbox run.",
|
|
107
|
-
inputSchema: {
|
|
108
|
-
run_id: z.string()
|
|
109
|
-
}
|
|
110
|
-
}, async ({ run_id }) => {
|
|
111
|
-
const payload = await client().cancelRun(run_id);
|
|
112
|
-
return textAndJson(`Cancel requested for ${payload.run.id}.`, payload);
|
|
113
|
-
});
|
|
114
|
-
server.registerTool("send_sandbox_message", {
|
|
115
|
-
title: "Send Sanbox Message",
|
|
116
|
-
description: "Ask a follow-up question against a retained Sanbox run workspace.",
|
|
117
|
-
inputSchema: {
|
|
118
|
-
run_id: z.string(),
|
|
119
|
-
message: z.string()
|
|
120
|
-
}
|
|
121
|
-
}, async ({ run_id, message }) => {
|
|
122
|
-
const payload = await client().sendMessage(run_id, message);
|
|
123
|
-
return textAndJson(`Message sent to ${payload.run.id}.`, payload);
|
|
124
|
-
});
|
|
125
|
-
const main = async () => {
|
|
126
|
-
await server.connect(new StdioServerTransport());
|
|
127
|
-
};
|
|
128
|
-
main().catch((error) => {
|
|
129
|
-
process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`);
|
|
130
|
-
process.exit(1);
|
|
131
|
-
});
|