mirror of
https://github.com/harivansh-afk/sandbox-agent.git
synced 2026-04-15 06:04:43 +00:00
Configure lefthook formatter checks (#231)
* Add lefthook formatter checks * Fix SDK mode hydration * Stabilize SDK mode integration test
This commit is contained in:
parent
0471214d65
commit
d2346bafb3
282 changed files with 5840 additions and 8399 deletions
|
|
@ -11,17 +11,14 @@ setupImage();
|
|||
|
||||
console.log("Creating BoxLite sandbox...");
|
||||
const box = new SimpleBox({
|
||||
rootfsPath: OCI_DIR,
|
||||
env,
|
||||
ports: [{ hostPort: 3000, guestPort: 3000 }],
|
||||
diskSizeGb: 4,
|
||||
rootfsPath: OCI_DIR,
|
||||
env,
|
||||
ports: [{ hostPort: 3000, guestPort: 3000 }],
|
||||
diskSizeGb: 4,
|
||||
});
|
||||
|
||||
console.log("Starting server...");
|
||||
const result = await box.exec(
|
||||
"sh", "-c",
|
||||
"nohup sandbox-agent server --no-token --host 0.0.0.0 --port 3000 >/tmp/sandbox-agent.log 2>&1 &",
|
||||
);
|
||||
const result = await box.exec("sh", "-c", "nohup sandbox-agent server --no-token --host 0.0.0.0 --port 3000 >/tmp/sandbox-agent.log 2>&1 &");
|
||||
if (result.exitCode !== 0) throw new Error(`Failed to start server: ${result.stderr}`);
|
||||
|
||||
const baseUrl = "http://localhost:3000";
|
||||
|
|
@ -36,9 +33,9 @@ console.log(" Press Ctrl+C to stop.");
|
|||
|
||||
const keepAlive = setInterval(() => {}, 60_000);
|
||||
const cleanup = async () => {
|
||||
clearInterval(keepAlive);
|
||||
await box.stop();
|
||||
process.exit(0);
|
||||
clearInterval(keepAlive);
|
||||
await box.stop();
|
||||
process.exit(0);
|
||||
};
|
||||
process.once("SIGINT", cleanup);
|
||||
process.once("SIGTERM", cleanup);
|
||||
|
|
|
|||
|
|
@ -5,12 +5,12 @@ export const DOCKER_IMAGE = "sandbox-agent-boxlite";
|
|||
export const OCI_DIR = new URL("../oci-image", import.meta.url).pathname;
|
||||
|
||||
export function setupImage() {
|
||||
console.log(`Building image "${DOCKER_IMAGE}" (cached after first run)...`);
|
||||
execSync(`docker build -t ${DOCKER_IMAGE} ${new URL("..", import.meta.url).pathname}`, { stdio: "inherit" });
|
||||
console.log(`Building image "${DOCKER_IMAGE}" (cached after first run)...`);
|
||||
execSync(`docker build -t ${DOCKER_IMAGE} ${new URL("..", import.meta.url).pathname}`, { stdio: "inherit" });
|
||||
|
||||
if (!existsSync(`${OCI_DIR}/oci-layout`)) {
|
||||
console.log("Exporting to OCI layout...");
|
||||
mkdirSync(OCI_DIR, { recursive: true });
|
||||
execSync(`docker save ${DOCKER_IMAGE} | tar -xf - -C ${OCI_DIR}`, { stdio: "inherit" });
|
||||
}
|
||||
if (!existsSync(`${OCI_DIR}/oci-layout`)) {
|
||||
console.log("Exporting to OCI layout...");
|
||||
mkdirSync(OCI_DIR, { recursive: true });
|
||||
execSync(`docker save ${DOCKER_IMAGE} | tar -xf - -C ${OCI_DIR}`, { stdio: "inherit" });
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -128,7 +128,7 @@ export function App() {
|
|||
console.error("Event stream error:", err);
|
||||
}
|
||||
},
|
||||
[log]
|
||||
[log],
|
||||
);
|
||||
|
||||
const send = useCallback(async () => {
|
||||
|
|
@ -162,12 +162,7 @@ export function App() {
|
|||
<div style={styles.connectForm}>
|
||||
<label style={styles.label}>
|
||||
Sandbox name:
|
||||
<input
|
||||
style={styles.input}
|
||||
value={sandboxName}
|
||||
onChange={(e) => setSandboxName(e.target.value)}
|
||||
placeholder="demo"
|
||||
/>
|
||||
<input style={styles.input} value={sandboxName} onChange={(e) => setSandboxName(e.target.value)} placeholder="demo" />
|
||||
</label>
|
||||
<button style={styles.button} onClick={connect}>
|
||||
Connect
|
||||
|
|
|
|||
|
|
@ -5,5 +5,5 @@ import { App } from "./App";
|
|||
createRoot(document.getElementById("root")!).render(
|
||||
<StrictMode>
|
||||
<App />
|
||||
</StrictMode>
|
||||
</StrictMode>,
|
||||
);
|
||||
|
|
|
|||
|
|
@ -2,65 +2,61 @@ import type { Sandbox } from "@cloudflare/sandbox";
|
|||
import { SandboxAgent } from "sandbox-agent";
|
||||
|
||||
export type PromptRequest = {
|
||||
agent?: string;
|
||||
prompt?: string;
|
||||
agent?: string;
|
||||
prompt?: string;
|
||||
};
|
||||
|
||||
export async function runPromptEndpointStream(
|
||||
sandbox: Sandbox,
|
||||
request: PromptRequest,
|
||||
port: number,
|
||||
emit: (event: { type: string; [key: string]: unknown }) => Promise<void> | void,
|
||||
sandbox: Sandbox,
|
||||
request: PromptRequest,
|
||||
port: number,
|
||||
emit: (event: { type: string; [key: string]: unknown }) => Promise<void> | void,
|
||||
): Promise<void> {
|
||||
const client = await SandboxAgent.connect({
|
||||
fetch: (req, init) =>
|
||||
sandbox.containerFetch(
|
||||
req,
|
||||
{
|
||||
...(init ?? {}),
|
||||
// Cloudflare containerFetch may drop long-lived update streams when
|
||||
// a forwarded AbortSignal is cancelled; clear it for this path.
|
||||
signal: undefined,
|
||||
},
|
||||
port,
|
||||
),
|
||||
});
|
||||
const client = await SandboxAgent.connect({
|
||||
fetch: (req, init) =>
|
||||
sandbox.containerFetch(
|
||||
req,
|
||||
{
|
||||
...(init ?? {}),
|
||||
// Cloudflare containerFetch may drop long-lived update streams when
|
||||
// a forwarded AbortSignal is cancelled; clear it for this path.
|
||||
signal: undefined,
|
||||
},
|
||||
port,
|
||||
),
|
||||
});
|
||||
|
||||
let unsubscribe: (() => void) | undefined;
|
||||
try {
|
||||
const session = await client.createSession({
|
||||
agent: request.agent ?? "codex",
|
||||
});
|
||||
let unsubscribe: (() => void) | undefined;
|
||||
try {
|
||||
const session = await client.createSession({
|
||||
agent: request.agent ?? "codex",
|
||||
});
|
||||
|
||||
const promptText =
|
||||
request.prompt?.trim() || "Reply with a short confirmation.";
|
||||
await emit({
|
||||
type: "session.created",
|
||||
sessionId: session.id,
|
||||
agent: session.agent,
|
||||
prompt: promptText,
|
||||
});
|
||||
const promptText = request.prompt?.trim() || "Reply with a short confirmation.";
|
||||
await emit({
|
||||
type: "session.created",
|
||||
sessionId: session.id,
|
||||
agent: session.agent,
|
||||
prompt: promptText,
|
||||
});
|
||||
|
||||
let pendingWrites: Promise<void> = Promise.resolve();
|
||||
unsubscribe = session.onEvent((event) => {
|
||||
pendingWrites = pendingWrites
|
||||
.then(async () => {
|
||||
await emit({ type: "session.event", event });
|
||||
})
|
||||
.catch(() => {});
|
||||
});
|
||||
let pendingWrites: Promise<void> = Promise.resolve();
|
||||
unsubscribe = session.onEvent((event) => {
|
||||
pendingWrites = pendingWrites
|
||||
.then(async () => {
|
||||
await emit({ type: "session.event", event });
|
||||
})
|
||||
.catch(() => {});
|
||||
});
|
||||
|
||||
const response = await session.prompt([{ type: "text", text: promptText }]);
|
||||
await pendingWrites;
|
||||
await emit({ type: "prompt.response", response });
|
||||
await emit({ type: "prompt.completed" });
|
||||
} finally {
|
||||
if (unsubscribe) {
|
||||
unsubscribe();
|
||||
}
|
||||
await Promise.race([
|
||||
client.dispose(),
|
||||
new Promise((resolve) => setTimeout(resolve, 250)),
|
||||
]);
|
||||
}
|
||||
const response = await session.prompt([{ type: "text", text: promptText }]);
|
||||
await pendingWrites;
|
||||
await emit({ type: "prompt.response", response });
|
||||
await emit({ type: "prompt.completed" });
|
||||
} finally {
|
||||
if (unsubscribe) {
|
||||
unsubscribe();
|
||||
}
|
||||
await Promise.race([client.dispose(), new Promise((resolve) => setTimeout(resolve, 250))]);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,8 +15,7 @@ import { fileURLToPath } from "node:url";
|
|||
import { resolve } from "node:path";
|
||||
|
||||
const PORT = 3000;
|
||||
const REQUEST_TIMEOUT_MS =
|
||||
Number.parseInt(process.env.COMPUTESDK_TIMEOUT_MS || "", 10) || 120_000;
|
||||
const REQUEST_TIMEOUT_MS = Number.parseInt(process.env.COMPUTESDK_TIMEOUT_MS || "", 10) || 120_000;
|
||||
|
||||
/**
|
||||
* Detects and validates the provider to use.
|
||||
|
|
@ -24,28 +23,22 @@ const REQUEST_TIMEOUT_MS =
|
|||
*/
|
||||
function resolveProvider(): ProviderName {
|
||||
const providerOverride = process.env.COMPUTESDK_PROVIDER;
|
||||
|
||||
|
||||
if (providerOverride) {
|
||||
if (!isValidProvider(providerOverride)) {
|
||||
throw new Error(
|
||||
`Unsupported ComputeSDK provider "${providerOverride}". Supported providers: ${PROVIDER_NAMES.join(", ")}`
|
||||
);
|
||||
throw new Error(`Unsupported ComputeSDK provider "${providerOverride}". Supported providers: ${PROVIDER_NAMES.join(", ")}`);
|
||||
}
|
||||
if (!isProviderAuthComplete(providerOverride)) {
|
||||
const missing = getMissingEnvVars(providerOverride);
|
||||
throw new Error(
|
||||
`Missing credentials for provider "${providerOverride}". Set: ${missing.join(", ")}`
|
||||
);
|
||||
throw new Error(`Missing credentials for provider "${providerOverride}". Set: ${missing.join(", ")}`);
|
||||
}
|
||||
console.log(`Using ComputeSDK provider: ${providerOverride} (explicit)`);
|
||||
return providerOverride as ProviderName;
|
||||
}
|
||||
|
||||
|
||||
const detected = detectProvider();
|
||||
if (!detected) {
|
||||
throw new Error(
|
||||
`No provider credentials found. Set one of: ${PROVIDER_NAMES.map((p) => getMissingEnvVars(p).join(", ")).join(" | ")}`
|
||||
);
|
||||
throw new Error(`No provider credentials found. Set one of: ${PROVIDER_NAMES.map((p) => getMissingEnvVars(p).join(", ")).join(" | ")}`);
|
||||
}
|
||||
console.log(`Using ComputeSDK provider: ${detected} (auto-detected)`);
|
||||
return detected as ProviderName;
|
||||
|
|
@ -53,20 +46,19 @@ function resolveProvider(): ProviderName {
|
|||
|
||||
function configureComputeSDK(): void {
|
||||
const provider = resolveProvider();
|
||||
|
||||
|
||||
const config: ExplicitComputeConfig = {
|
||||
provider,
|
||||
computesdkApiKey: process.env.COMPUTESDK_API_KEY,
|
||||
requestTimeoutMs: REQUEST_TIMEOUT_MS,
|
||||
};
|
||||
|
||||
|
||||
const providerConfig = getProviderConfigFromEnv(provider);
|
||||
if (Object.keys(providerConfig).length > 0) {
|
||||
const configWithProvider =
|
||||
config as ExplicitComputeConfig & Record<ProviderName, Record<string, string>>;
|
||||
const configWithProvider = config as ExplicitComputeConfig & Record<ProviderName, Record<string, string>>;
|
||||
configWithProvider[provider] = providerConfig;
|
||||
}
|
||||
|
||||
|
||||
compute.setConfig(config);
|
||||
}
|
||||
|
||||
|
|
@ -149,9 +141,7 @@ export async function runComputeSdkExample(): Promise<void> {
|
|||
await new Promise(() => {});
|
||||
}
|
||||
|
||||
const isDirectRun = Boolean(
|
||||
process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url)
|
||||
);
|
||||
const isDirectRun = Boolean(process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url));
|
||||
|
||||
if (isDirectRun) {
|
||||
runComputeSdkExample().catch((error) => {
|
||||
|
|
|
|||
|
|
@ -5,12 +5,7 @@ import { setupComputeSdkSandboxAgent } from "../src/computesdk.ts";
|
|||
const hasModal = Boolean(process.env.MODAL_TOKEN_ID && process.env.MODAL_TOKEN_SECRET);
|
||||
const hasVercel = Boolean(process.env.VERCEL_TOKEN || process.env.VERCEL_OIDC_TOKEN);
|
||||
const hasProviderKey = Boolean(
|
||||
process.env.BLAXEL_API_KEY ||
|
||||
process.env.CSB_API_KEY ||
|
||||
process.env.DAYTONA_API_KEY ||
|
||||
process.env.E2B_API_KEY ||
|
||||
hasModal ||
|
||||
hasVercel
|
||||
process.env.BLAXEL_API_KEY || process.env.CSB_API_KEY || process.env.DAYTONA_API_KEY || process.env.E2B_API_KEY || hasModal || hasVercel,
|
||||
);
|
||||
|
||||
const shouldRun = Boolean(process.env.COMPUTESDK_API_KEY) && hasProviderKey;
|
||||
|
|
@ -34,6 +29,6 @@ describe("computesdk example", () => {
|
|||
await cleanup();
|
||||
}
|
||||
},
|
||||
timeoutMs
|
||||
timeoutMs,
|
||||
);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -5,23 +5,19 @@ import { detectAgent, buildInspectorUrl } from "@sandbox-agent/example-shared";
|
|||
const daytona = new Daytona();
|
||||
|
||||
const envVars: Record<string, string> = {};
|
||||
if (process.env.ANTHROPIC_API_KEY)
|
||||
envVars.ANTHROPIC_API_KEY = process.env.ANTHROPIC_API_KEY;
|
||||
if (process.env.OPENAI_API_KEY)
|
||||
envVars.OPENAI_API_KEY = process.env.OPENAI_API_KEY;
|
||||
if (process.env.ANTHROPIC_API_KEY) envVars.ANTHROPIC_API_KEY = process.env.ANTHROPIC_API_KEY;
|
||||
if (process.env.OPENAI_API_KEY) envVars.OPENAI_API_KEY = process.env.OPENAI_API_KEY;
|
||||
|
||||
// Build a custom image with sandbox-agent pre-installed (slower first run, faster subsequent runs)
|
||||
const image = Image.base("ubuntu:22.04").runCommands(
|
||||
"apt-get update && apt-get install -y curl ca-certificates",
|
||||
"curl -fsSL https://releases.rivet.dev/sandbox-agent/0.3.x/install.sh | sh",
|
||||
"apt-get update && apt-get install -y curl ca-certificates",
|
||||
"curl -fsSL https://releases.rivet.dev/sandbox-agent/0.3.x/install.sh | sh",
|
||||
);
|
||||
|
||||
console.log("Creating Daytona sandbox (first run builds the base image and may take a few minutes, subsequent runs are fast)...");
|
||||
const sandbox = await daytona.create({ envVars, image, autoStopInterval: 0 }, { timeout: 180 });
|
||||
|
||||
await sandbox.process.executeCommand(
|
||||
"nohup sandbox-agent server --no-token --host 0.0.0.0 --port 3000 >/tmp/sandbox-agent.log 2>&1 &",
|
||||
);
|
||||
await sandbox.process.executeCommand("nohup sandbox-agent server --no-token --host 0.0.0.0 --port 3000 >/tmp/sandbox-agent.log 2>&1 &");
|
||||
|
||||
const baseUrl = (await sandbox.getSignedPreviewUrl(3000, 4 * 60 * 60)).url;
|
||||
|
||||
|
|
@ -35,9 +31,9 @@ console.log(" Press Ctrl+C to stop.");
|
|||
|
||||
const keepAlive = setInterval(() => {}, 60_000);
|
||||
const cleanup = async () => {
|
||||
clearInterval(keepAlive);
|
||||
await sandbox.delete(60);
|
||||
process.exit(0);
|
||||
clearInterval(keepAlive);
|
||||
await sandbox.delete(60);
|
||||
process.exit(0);
|
||||
};
|
||||
process.once("SIGINT", cleanup);
|
||||
process.once("SIGTERM", cleanup);
|
||||
|
|
|
|||
|
|
@ -5,10 +5,8 @@ import { detectAgent, buildInspectorUrl } from "@sandbox-agent/example-shared";
|
|||
const daytona = new Daytona();
|
||||
|
||||
const envVars: Record<string, string> = {};
|
||||
if (process.env.ANTHROPIC_API_KEY)
|
||||
envVars.ANTHROPIC_API_KEY = process.env.ANTHROPIC_API_KEY;
|
||||
if (process.env.OPENAI_API_KEY)
|
||||
envVars.OPENAI_API_KEY = process.env.OPENAI_API_KEY;
|
||||
if (process.env.ANTHROPIC_API_KEY) envVars.ANTHROPIC_API_KEY = process.env.ANTHROPIC_API_KEY;
|
||||
if (process.env.OPENAI_API_KEY) envVars.OPENAI_API_KEY = process.env.OPENAI_API_KEY;
|
||||
|
||||
// Use default image and install sandbox-agent at runtime (faster startup, no snapshot build)
|
||||
console.log("Creating Daytona sandbox...");
|
||||
|
|
@ -16,17 +14,13 @@ const sandbox = await daytona.create({ envVars, autoStopInterval: 0 });
|
|||
|
||||
// Install sandbox-agent and start server
|
||||
console.log("Installing sandbox-agent...");
|
||||
await sandbox.process.executeCommand(
|
||||
"curl -fsSL https://releases.rivet.dev/sandbox-agent/0.3.x/install.sh | sh",
|
||||
);
|
||||
await sandbox.process.executeCommand("curl -fsSL https://releases.rivet.dev/sandbox-agent/0.3.x/install.sh | sh");
|
||||
|
||||
console.log("Installing agents...");
|
||||
await sandbox.process.executeCommand("sandbox-agent install-agent claude");
|
||||
await sandbox.process.executeCommand("sandbox-agent install-agent codex");
|
||||
|
||||
await sandbox.process.executeCommand(
|
||||
"nohup sandbox-agent server --no-token --host 0.0.0.0 --port 3000 >/tmp/sandbox-agent.log 2>&1 &",
|
||||
);
|
||||
await sandbox.process.executeCommand("nohup sandbox-agent server --no-token --host 0.0.0.0 --port 3000 >/tmp/sandbox-agent.log 2>&1 &");
|
||||
|
||||
const baseUrl = (await sandbox.getSignedPreviewUrl(3000, 4 * 60 * 60)).url;
|
||||
|
||||
|
|
@ -40,9 +34,9 @@ console.log(" Press Ctrl+C to stop.");
|
|||
|
||||
const keepAlive = setInterval(() => {}, 60_000);
|
||||
const cleanup = async () => {
|
||||
clearInterval(keepAlive);
|
||||
await sandbox.delete(60);
|
||||
process.exit(0);
|
||||
clearInterval(keepAlive);
|
||||
await sandbox.delete(60);
|
||||
process.exit(0);
|
||||
};
|
||||
process.once("SIGINT", cleanup);
|
||||
process.once("SIGTERM", cleanup);
|
||||
|
|
|
|||
|
|
@ -23,6 +23,6 @@ describe("daytona example", () => {
|
|||
await cleanup();
|
||||
}
|
||||
},
|
||||
timeoutMs
|
||||
timeoutMs,
|
||||
);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -8,9 +8,7 @@ const IMAGE = "node:22-bookworm-slim";
|
|||
const PORT = 3000;
|
||||
const agent = detectAgent();
|
||||
const codexAuthPath = process.env.HOME ? path.join(process.env.HOME, ".codex", "auth.json") : null;
|
||||
const bindMounts = codexAuthPath && fs.existsSync(codexAuthPath)
|
||||
? [`${codexAuthPath}:/root/.codex/auth.json:ro`]
|
||||
: [];
|
||||
const bindMounts = codexAuthPath && fs.existsSync(codexAuthPath) ? [`${codexAuthPath}:/root/.codex/auth.json:ro`] : [];
|
||||
|
||||
const docker = new Docker({ socketPath: "/var/run/docker.sock" });
|
||||
|
||||
|
|
@ -22,7 +20,7 @@ try {
|
|||
await new Promise<void>((resolve, reject) => {
|
||||
docker.pull(IMAGE, (err: Error | null, stream: NodeJS.ReadableStream) => {
|
||||
if (err) return reject(err);
|
||||
docker.modem.followProgress(stream, (err: Error | null) => err ? reject(err) : resolve());
|
||||
docker.modem.followProgress(stream, (err: Error | null) => (err ? reject(err) : resolve()));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
@ -30,13 +28,17 @@ try {
|
|||
console.log("Starting container...");
|
||||
const container = await docker.createContainer({
|
||||
Image: IMAGE,
|
||||
Cmd: ["sh", "-c", [
|
||||
"apt-get update",
|
||||
"DEBIAN_FRONTEND=noninteractive apt-get install -y curl ca-certificates bash libstdc++6",
|
||||
"rm -rf /var/lib/apt/lists/*",
|
||||
"curl -fsSL https://releases.rivet.dev/sandbox-agent/0.3.x/install.sh | sh",
|
||||
`sandbox-agent server --no-token --host 0.0.0.0 --port ${PORT}`,
|
||||
].join(" && ")],
|
||||
Cmd: [
|
||||
"sh",
|
||||
"-c",
|
||||
[
|
||||
"apt-get update",
|
||||
"DEBIAN_FRONTEND=noninteractive apt-get install -y curl ca-certificates bash libstdc++6",
|
||||
"rm -rf /var/lib/apt/lists/*",
|
||||
"curl -fsSL https://releases.rivet.dev/sandbox-agent/0.3.x/install.sh | sh",
|
||||
`sandbox-agent server --no-token --host 0.0.0.0 --port ${PORT}`,
|
||||
].join(" && "),
|
||||
],
|
||||
Env: [
|
||||
process.env.ANTHROPIC_API_KEY ? `ANTHROPIC_API_KEY=${process.env.ANTHROPIC_API_KEY}` : "",
|
||||
process.env.OPENAI_API_KEY ? `OPENAI_API_KEY=${process.env.OPENAI_API_KEY}` : "",
|
||||
|
|
@ -63,8 +65,12 @@ console.log(" Press Ctrl+C to stop.");
|
|||
const keepAlive = setInterval(() => {}, 60_000);
|
||||
const cleanup = async () => {
|
||||
clearInterval(keepAlive);
|
||||
try { await container.stop({ t: 5 }); } catch {}
|
||||
try { await container.remove({ force: true }); } catch {}
|
||||
try {
|
||||
await container.stop({ t: 5 });
|
||||
} catch {}
|
||||
try {
|
||||
await container.remove({ force: true });
|
||||
} catch {}
|
||||
process.exit(0);
|
||||
};
|
||||
process.once("SIGINT", cleanup);
|
||||
|
|
|
|||
|
|
@ -23,6 +23,6 @@ describe("docker example", () => {
|
|||
await cleanup();
|
||||
}
|
||||
},
|
||||
timeoutMs
|
||||
timeoutMs,
|
||||
);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -23,6 +23,6 @@ describe("e2b example", () => {
|
|||
await cleanup();
|
||||
}
|
||||
},
|
||||
timeoutMs
|
||||
timeoutMs,
|
||||
);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -24,10 +24,7 @@ console.log("Uploading files via batch tar...");
|
|||
const client = await SandboxAgent.connect({ baseUrl });
|
||||
|
||||
const tarPath = path.join(tmpDir, "upload.tar");
|
||||
await tar.create(
|
||||
{ file: tarPath, cwd: tmpDir },
|
||||
["my-project"],
|
||||
);
|
||||
await tar.create({ file: tarPath, cwd: tmpDir }, ["my-project"]);
|
||||
const tarBuffer = await fs.promises.readFile(tarPath);
|
||||
const uploadResult = await client.uploadFsBatch(tarBuffer, { path: "/opt" });
|
||||
console.log(` Uploaded ${uploadResult.paths.length} files: ${uploadResult.paths.join(", ")}`);
|
||||
|
|
@ -54,4 +51,7 @@ console.log(' Try: "read the README in /opt/my-project"');
|
|||
console.log(" Press Ctrl+C to stop.");
|
||||
|
||||
const keepAlive = setInterval(() => {}, 60_000);
|
||||
process.on("SIGINT", () => { clearInterval(keepAlive); cleanup().then(() => process.exit(0)); });
|
||||
process.on("SIGINT", () => {
|
||||
clearInterval(keepAlive);
|
||||
cleanup().then(() => process.exit(0));
|
||||
});
|
||||
|
|
|
|||
|
|
@ -23,10 +23,7 @@ console.log("Uploading MCP server bundle...");
|
|||
const client = await SandboxAgent.connect({ baseUrl });
|
||||
|
||||
const bundle = await fs.promises.readFile(serverFile);
|
||||
const written = await client.writeFsFile(
|
||||
{ path: "/opt/mcp/custom-tools/mcp-server.cjs" },
|
||||
bundle,
|
||||
);
|
||||
const written = await client.writeFsFile({ path: "/opt/mcp/custom-tools/mcp-server.cjs" }, bundle);
|
||||
console.log(` Written: ${written.path} (${written.bytesWritten} bytes)`);
|
||||
|
||||
// Create a session with the uploaded MCP server as a local command.
|
||||
|
|
@ -35,12 +32,14 @@ const session = await client.createSession({
|
|||
agent: detectAgent(),
|
||||
sessionInit: {
|
||||
cwd: "/root",
|
||||
mcpServers: [{
|
||||
name: "customTools",
|
||||
command: "node",
|
||||
args: ["/opt/mcp/custom-tools/mcp-server.cjs"],
|
||||
env: [],
|
||||
}],
|
||||
mcpServers: [
|
||||
{
|
||||
name: "customTools",
|
||||
command: "node",
|
||||
args: ["/opt/mcp/custom-tools/mcp-server.cjs"],
|
||||
env: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
const sessionId = session.id;
|
||||
|
|
@ -49,4 +48,7 @@ console.log(' Try: "generate a random number between 1 and 100"');
|
|||
console.log(" Press Ctrl+C to stop.");
|
||||
|
||||
const keepAlive = setInterval(() => {}, 60_000);
|
||||
process.on("SIGINT", () => { clearInterval(keepAlive); cleanup().then(() => process.exit(0)); });
|
||||
process.on("SIGINT", () => {
|
||||
clearInterval(keepAlive);
|
||||
cleanup().then(() => process.exit(0));
|
||||
});
|
||||
|
|
|
|||
|
|
@ -5,9 +5,7 @@ import { startDockerSandbox } from "@sandbox-agent/example-shared/docker";
|
|||
console.log("Starting sandbox...");
|
||||
const { baseUrl, cleanup } = await startDockerSandbox({
|
||||
port: 3002,
|
||||
setupCommands: [
|
||||
"npm install -g --silent @modelcontextprotocol/server-everything@2026.1.26",
|
||||
],
|
||||
setupCommands: ["npm install -g --silent @modelcontextprotocol/server-everything@2026.1.26"],
|
||||
});
|
||||
|
||||
console.log("Creating session with everything MCP server...");
|
||||
|
|
@ -16,12 +14,14 @@ const session = await client.createSession({
|
|||
agent: detectAgent(),
|
||||
sessionInit: {
|
||||
cwd: "/root",
|
||||
mcpServers: [{
|
||||
name: "everything",
|
||||
command: "mcp-server-everything",
|
||||
args: [],
|
||||
env: [],
|
||||
}],
|
||||
mcpServers: [
|
||||
{
|
||||
name: "everything",
|
||||
command: "mcp-server-everything",
|
||||
args: [],
|
||||
env: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
const sessionId = session.id;
|
||||
|
|
@ -30,4 +30,7 @@ console.log(' Try: "generate a random number between 1 and 100"');
|
|||
console.log(" Press Ctrl+C to stop.");
|
||||
|
||||
const keepAlive = setInterval(() => {}, 60_000);
|
||||
process.on("SIGINT", () => { clearInterval(keepAlive); cleanup().then(() => process.exit(0)); });
|
||||
process.on("SIGINT", () => {
|
||||
clearInterval(keepAlive);
|
||||
cleanup().then(() => process.exit(0));
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,18 +1,12 @@
|
|||
import { createInterface } from "node:readline/promises";
|
||||
import { stdin as input, stdout as output } from "node:process";
|
||||
import { Command } from "commander";
|
||||
import {
|
||||
SandboxAgent,
|
||||
type PermissionReply,
|
||||
type SessionPermissionRequest,
|
||||
} from "sandbox-agent";
|
||||
import { SandboxAgent, type PermissionReply, type SessionPermissionRequest } from "sandbox-agent";
|
||||
|
||||
const options = parseOptions();
|
||||
const agent = options.agent.trim().toLowerCase();
|
||||
const autoReply = parsePermissionReply(options.reply);
|
||||
const promptText =
|
||||
options.prompt?.trim() ||
|
||||
`Create ./permission-example.txt with the text 'hello from the ${agent} permissions example'.`;
|
||||
const promptText = options.prompt?.trim() || `Create ./permission-example.txt with the text 'hello from the ${agent} permissions example'.`;
|
||||
|
||||
const sdk = await SandboxAgent.start({
|
||||
spawn: {
|
||||
|
|
@ -31,11 +25,7 @@ try {
|
|||
: [];
|
||||
const modeOption = configOptions.find((option) => option.category === "mode");
|
||||
const availableModes = extractOptionValues(modeOption);
|
||||
const mode =
|
||||
options.mode?.trim() ||
|
||||
(typeof modeOption?.currentValue === "string" ? modeOption.currentValue : "") ||
|
||||
availableModes[0] ||
|
||||
"";
|
||||
const mode = options.mode?.trim() || (typeof modeOption?.currentValue === "string" ? modeOption.currentValue : "") || availableModes[0] || "";
|
||||
|
||||
console.log(`Agent: ${agent}`);
|
||||
console.log(`Mode: ${mode || "(default)"}`);
|
||||
|
|
@ -91,10 +81,7 @@ async function handlePermissionRequest(
|
|||
await session.respondPermission(request.id, reply);
|
||||
}
|
||||
|
||||
async function promptForReply(
|
||||
request: SessionPermissionRequest,
|
||||
rl: ReturnType<typeof createInterface> | null,
|
||||
): Promise<PermissionReply> {
|
||||
async function promptForReply(request: SessionPermissionRequest, rl: ReturnType<typeof createInterface> | null): Promise<PermissionReply> {
|
||||
if (!rl) {
|
||||
return "reject";
|
||||
}
|
||||
|
|
@ -136,8 +123,7 @@ function extractOptionValues(option: { options?: unknown[] } | undefined): strin
|
|||
if (!nested || typeof nested !== "object") {
|
||||
continue;
|
||||
}
|
||||
const nestedValue =
|
||||
"value" in nested && typeof nested.value === "string" ? nested.value : null;
|
||||
const nestedValue = "value" in nested && typeof nested.value === "string" ? nested.value : null;
|
||||
if (nestedValue) {
|
||||
values.push(nestedValue);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,10 +7,7 @@ const persist = new InMemorySessionPersistDriver();
|
|||
console.log("Starting sandbox...");
|
||||
const sandbox = await startDockerSandbox({
|
||||
port: 3000,
|
||||
setupCommands: [
|
||||
"sandbox-agent install-agent claude",
|
||||
"sandbox-agent install-agent codex",
|
||||
],
|
||||
setupCommands: ["sandbox-agent install-agent claude", "sandbox-agent install-agent codex"],
|
||||
});
|
||||
|
||||
const sdk = await SandboxAgent.connect({ baseUrl: sandbox.baseUrl, persist });
|
||||
|
|
|
|||
|
|
@ -16,21 +16,47 @@ if (process.env.DATABASE_URL) {
|
|||
connectionString = process.env.DATABASE_URL;
|
||||
} else {
|
||||
const name = `persist-example-${randomUUID().slice(0, 8)}`;
|
||||
containerId = execFileSync("docker", [
|
||||
"run", "-d", "--rm", "--name", name,
|
||||
"-e", "POSTGRES_USER=postgres", "-e", "POSTGRES_PASSWORD=postgres", "-e", "POSTGRES_DB=sandbox",
|
||||
"-p", "127.0.0.1::5432", "postgres:16-alpine",
|
||||
], { encoding: "utf8" }).trim();
|
||||
containerId = execFileSync(
|
||||
"docker",
|
||||
[
|
||||
"run",
|
||||
"-d",
|
||||
"--rm",
|
||||
"--name",
|
||||
name,
|
||||
"-e",
|
||||
"POSTGRES_USER=postgres",
|
||||
"-e",
|
||||
"POSTGRES_PASSWORD=postgres",
|
||||
"-e",
|
||||
"POSTGRES_DB=sandbox",
|
||||
"-p",
|
||||
"127.0.0.1::5432",
|
||||
"postgres:16-alpine",
|
||||
],
|
||||
{ encoding: "utf8" },
|
||||
).trim();
|
||||
const port = execFileSync("docker", ["port", containerId, "5432/tcp"], { encoding: "utf8" })
|
||||
.trim().split("\n")[0]?.match(/:(\d+)$/)?.[1];
|
||||
.trim()
|
||||
.split("\n")[0]
|
||||
?.match(/:(\d+)$/)?.[1];
|
||||
connectionString = `postgres://postgres:postgres@127.0.0.1:${port}/sandbox`;
|
||||
console.log(`Postgres on port ${port}`);
|
||||
|
||||
const deadline = Date.now() + 30_000;
|
||||
while (Date.now() < deadline) {
|
||||
const c = new Client({ connectionString });
|
||||
try { await c.connect(); await c.query("SELECT 1"); await c.end(); break; }
|
||||
catch { try { await c.end(); } catch {} await delay(250); }
|
||||
try {
|
||||
await c.connect();
|
||||
await c.query("SELECT 1");
|
||||
await c.end();
|
||||
break;
|
||||
} catch {
|
||||
try {
|
||||
await c.end();
|
||||
} catch {}
|
||||
await delay(250);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -40,10 +66,7 @@ try {
|
|||
console.log("Starting sandbox...");
|
||||
const sandbox = await startDockerSandbox({
|
||||
port: 3000,
|
||||
setupCommands: [
|
||||
"sandbox-agent install-agent claude",
|
||||
"sandbox-agent install-agent codex",
|
||||
],
|
||||
setupCommands: ["sandbox-agent install-agent claude", "sandbox-agent install-agent codex"],
|
||||
});
|
||||
|
||||
const sdk = await SandboxAgent.connect({ baseUrl: sandbox.baseUrl, persist });
|
||||
|
|
@ -71,6 +94,8 @@ try {
|
|||
await sandbox.cleanup();
|
||||
} finally {
|
||||
if (containerId) {
|
||||
try { execFileSync("docker", ["rm", "-f", containerId], { stdio: "ignore" }); } catch {}
|
||||
try {
|
||||
execFileSync("docker", ["rm", "-f", containerId], { stdio: "ignore" });
|
||||
} catch {}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,10 +8,7 @@ const persist = new SQLiteSessionPersistDriver({ filename: "./sessions.db" });
|
|||
console.log("Starting sandbox...");
|
||||
const sandbox = await startDockerSandbox({
|
||||
port: 3000,
|
||||
setupCommands: [
|
||||
"sandbox-agent install-agent claude",
|
||||
"sandbox-agent install-agent codex",
|
||||
],
|
||||
setupCommands: ["sandbox-agent install-agent claude", "sandbox-agent install-agent codex"],
|
||||
});
|
||||
|
||||
const sdk = await SandboxAgent.connect({ baseUrl: sandbox.baseUrl, persist });
|
||||
|
|
|
|||
|
|
@ -40,7 +40,7 @@ const DIRECT_CREDENTIAL_KEYS = [
|
|||
|
||||
function stripShellQuotes(value: string): string {
|
||||
const trimmed = value.trim();
|
||||
if (trimmed.length >= 2 && trimmed.startsWith("\"") && trimmed.endsWith("\"")) {
|
||||
if (trimmed.length >= 2 && trimmed.startsWith('"') && trimmed.endsWith('"')) {
|
||||
return trimmed.slice(1, -1);
|
||||
}
|
||||
if (trimmed.length >= 2 && trimmed.startsWith("'") && trimmed.endsWith("'")) {
|
||||
|
|
@ -107,11 +107,7 @@ function collectCredentialEnv(): Record<string, string> {
|
|||
const merged: Record<string, string> = {};
|
||||
let extracted: Record<string, string> = {};
|
||||
try {
|
||||
const output = execFileSync(
|
||||
"sandbox-agent",
|
||||
["credentials", "extract-env"],
|
||||
{ encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] },
|
||||
);
|
||||
const output = execFileSync("sandbox-agent", ["credentials", "extract-env"], { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] });
|
||||
extracted = parseExtractedCredentials(output);
|
||||
} catch {
|
||||
// Fall back to direct env vars if extraction is unavailable.
|
||||
|
|
@ -132,10 +128,7 @@ function shellSingleQuotedLiteral(value: string): string {
|
|||
}
|
||||
|
||||
function stripAnsi(value: string): string {
|
||||
return value.replace(
|
||||
/[\u001B\u009B][[\]()#;?]*(?:(?:[a-zA-Z\d]*(?:;[a-zA-Z\d]*)*)?\u0007|(?:\d{1,4}(?:;\d{0,4})*)?[0-9A-ORZcf-nqry=><])/g,
|
||||
"",
|
||||
);
|
||||
return value.replace(/[\u001B\u009B][[\]()#;?]*(?:(?:[a-zA-Z\d]*(?:;[a-zA-Z\d]*)*)?\u0007|(?:\d{1,4}(?:;\d{0,4})*)?[0-9A-ORZcf-nqry=><])/g, "");
|
||||
}
|
||||
|
||||
async function ensureExampleImage(_docker: Docker): Promise<string> {
|
||||
|
|
@ -145,11 +138,7 @@ async function ensureExampleImage(_docker: Docker): Promise<string> {
|
|||
if (dev) {
|
||||
console.log(" Building sandbox image from source (may take a while, only runs once)...");
|
||||
try {
|
||||
execFileSync("docker", [
|
||||
"build", "-t", imageName,
|
||||
"-f", path.join(DOCKERFILE_DIR, "Dockerfile.dev"),
|
||||
REPO_ROOT,
|
||||
], {
|
||||
execFileSync("docker", ["build", "-t", imageName, "-f", path.join(DOCKERFILE_DIR, "Dockerfile.dev"), REPO_ROOT], {
|
||||
stdio: ["ignore", "ignore", "pipe"],
|
||||
});
|
||||
} catch (err: unknown) {
|
||||
|
|
@ -224,19 +213,13 @@ export async function startDockerSandbox(opts: DockerSandboxOptions): Promise<Do
|
|||
image = await ensureExampleImage(docker);
|
||||
}
|
||||
|
||||
const bootCommands = [
|
||||
...setupCommands,
|
||||
`sandbox-agent server --no-token --host 0.0.0.0 --port ${port}`,
|
||||
];
|
||||
const bootCommands = [...setupCommands, `sandbox-agent server --no-token --host 0.0.0.0 --port ${port}`];
|
||||
|
||||
const container = await docker.createContainer({
|
||||
Image: image,
|
||||
WorkingDir: "/root",
|
||||
Cmd: ["sh", "-c", bootCommands.join(" && ")],
|
||||
Env: [
|
||||
...Object.entries(credentialEnv).map(([key, value]) => `${key}=${value}`),
|
||||
...Object.entries(bootstrapEnv).map(([key, value]) => `${key}=${value}`),
|
||||
],
|
||||
Env: [...Object.entries(credentialEnv).map(([key, value]) => `${key}=${value}`), ...Object.entries(bootstrapEnv).map(([key, value]) => `${key}=${value}`)],
|
||||
ExposedPorts: { [`${port}/tcp`]: {} },
|
||||
HostConfig: {
|
||||
AutoRemove: true,
|
||||
|
|
@ -246,12 +229,12 @@ export async function startDockerSandbox(opts: DockerSandboxOptions): Promise<Do
|
|||
await container.start();
|
||||
|
||||
const logChunks: string[] = [];
|
||||
const startupLogs = await container.logs({
|
||||
const startupLogs = (await container.logs({
|
||||
follow: true,
|
||||
stdout: true,
|
||||
stderr: true,
|
||||
since: 0,
|
||||
}) as NodeJS.ReadableStream;
|
||||
})) as NodeJS.ReadableStream;
|
||||
const stdoutStream = new PassThrough();
|
||||
const stderrStream = new PassThrough();
|
||||
stdoutStream.on("data", (chunk) => {
|
||||
|
|
@ -263,7 +246,9 @@ export async function startDockerSandbox(opts: DockerSandboxOptions): Promise<Do
|
|||
docker.modem.demuxStream(startupLogs, stdoutStream, stderrStream);
|
||||
const stopStartupLogs = () => {
|
||||
const stream = startupLogs as NodeJS.ReadableStream & { destroy?: () => void };
|
||||
try { stream.destroy?.(); } catch {}
|
||||
try {
|
||||
stream.destroy?.();
|
||||
} catch {}
|
||||
};
|
||||
|
||||
const inspect = await container.inspect();
|
||||
|
|
@ -279,8 +264,12 @@ export async function startDockerSandbox(opts: DockerSandboxOptions): Promise<Do
|
|||
|
||||
const cleanup = async () => {
|
||||
stopStartupLogs();
|
||||
try { await container.stop({ t: 5 }); } catch {}
|
||||
try { await container.remove({ force: true }); } catch {}
|
||||
try {
|
||||
await container.stop({ t: 5 });
|
||||
} catch {}
|
||||
try {
|
||||
await container.remove({ force: true });
|
||||
} catch {}
|
||||
process.exit(0);
|
||||
};
|
||||
process.once("SIGINT", cleanup);
|
||||
|
|
|
|||
|
|
@ -41,15 +41,7 @@ export function buildInspectorUrl({
|
|||
return `${normalized}/ui/${sessionPath}${queryString ? `?${queryString}` : ""}`;
|
||||
}
|
||||
|
||||
export function logInspectorUrl({
|
||||
baseUrl,
|
||||
token,
|
||||
headers,
|
||||
}: {
|
||||
baseUrl: string;
|
||||
token?: string;
|
||||
headers?: Record<string, string>;
|
||||
}): void {
|
||||
export function logInspectorUrl({ baseUrl, token, headers }: { baseUrl: string; token?: string; headers?: Record<string, string> }): void {
|
||||
console.log(`Inspector: ${buildInspectorUrl({ baseUrl, token, headers })}`);
|
||||
}
|
||||
|
||||
|
|
@ -84,10 +76,7 @@ export function generateSessionId(): string {
|
|||
export function detectAgent(): string {
|
||||
if (process.env.SANDBOX_AGENT) return process.env.SANDBOX_AGENT;
|
||||
const hasClaude = Boolean(
|
||||
process.env.ANTHROPIC_API_KEY ||
|
||||
process.env.CLAUDE_API_KEY ||
|
||||
process.env.CLAUDE_CODE_OAUTH_TOKEN ||
|
||||
process.env.ANTHROPIC_AUTH_TOKEN,
|
||||
process.env.ANTHROPIC_API_KEY || process.env.CLAUDE_API_KEY || process.env.CLAUDE_CODE_OAUTH_TOKEN || process.env.ANTHROPIC_AUTH_TOKEN,
|
||||
);
|
||||
const openAiLikeKey = process.env.OPENAI_API_KEY || process.env.CODEX_API_KEY || "";
|
||||
const hasCodexApiKey = openAiLikeKey.startsWith("sk-");
|
||||
|
|
|
|||
|
|
@ -23,25 +23,16 @@ console.log("Uploading script and skill file...");
|
|||
const client = await SandboxAgent.connect({ baseUrl });
|
||||
|
||||
const script = await fs.promises.readFile(scriptFile);
|
||||
const scriptResult = await client.writeFsFile(
|
||||
{ path: "/opt/skills/random-number/random-number.cjs" },
|
||||
script,
|
||||
);
|
||||
const scriptResult = await client.writeFsFile({ path: "/opt/skills/random-number/random-number.cjs" }, script);
|
||||
console.log(` Script: ${scriptResult.path} (${scriptResult.bytesWritten} bytes)`);
|
||||
|
||||
const skillMd = await fs.promises.readFile(path.resolve(__dirname, "../SKILL.md"));
|
||||
const skillResult = await client.writeFsFile(
|
||||
{ path: "/opt/skills/random-number/SKILL.md" },
|
||||
skillMd,
|
||||
);
|
||||
const skillResult = await client.writeFsFile({ path: "/opt/skills/random-number/SKILL.md" }, skillMd);
|
||||
console.log(` Skill: ${skillResult.path} (${skillResult.bytesWritten} bytes)`);
|
||||
|
||||
// Configure the uploaded skill.
|
||||
console.log("Configuring custom skill...");
|
||||
await client.setSkillsConfig(
|
||||
{ directory: "/", skillName: "random-number" },
|
||||
{ sources: [{ type: "local", source: "/opt/skills/random-number" }] },
|
||||
);
|
||||
await client.setSkillsConfig({ directory: "/", skillName: "random-number" }, { sources: [{ type: "local", source: "/opt/skills/random-number" }] });
|
||||
|
||||
// Create a session.
|
||||
console.log("Creating session with custom skill...");
|
||||
|
|
@ -52,4 +43,7 @@ console.log(' Try: "generate a random number between 1 and 100"');
|
|||
console.log(" Press Ctrl+C to stop.");
|
||||
|
||||
const keepAlive = setInterval(() => {}, 60_000);
|
||||
process.on("SIGINT", () => { clearInterval(keepAlive); cleanup().then(() => process.exit(0)); });
|
||||
process.on("SIGINT", () => {
|
||||
clearInterval(keepAlive);
|
||||
cleanup().then(() => process.exit(0));
|
||||
});
|
||||
|
|
|
|||
|
|
@ -22,4 +22,7 @@ console.log(' Try: "How do I start sandbox-agent?"');
|
|||
console.log(" Press Ctrl+C to stop.");
|
||||
|
||||
const keepAlive = setInterval(() => {}, 60_000);
|
||||
process.on("SIGINT", () => { clearInterval(keepAlive); cleanup().then(() => process.exit(0)); });
|
||||
process.on("SIGINT", () => {
|
||||
clearInterval(keepAlive);
|
||||
cleanup().then(() => process.exit(0));
|
||||
});
|
||||
|
|
|
|||
|
|
@ -23,6 +23,6 @@ describe("vercel example", () => {
|
|||
await cleanup();
|
||||
}
|
||||
},
|
||||
timeoutMs
|
||||
timeoutMs,
|
||||
);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,10 +1,7 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"lib": [
|
||||
"ES2022",
|
||||
"DOM"
|
||||
],
|
||||
"lib": ["ES2022", "DOM"],
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "Bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
|
|
@ -14,11 +11,6 @@
|
|||
"skipLibCheck": true,
|
||||
"resolveJsonModule": true
|
||||
},
|
||||
"include": [
|
||||
"src/**/*"
|
||||
],
|
||||
"exclude": [
|
||||
"node_modules",
|
||||
"**/*.test.ts"
|
||||
]
|
||||
"include": ["src/**/*"],
|
||||
"exclude": ["node_modules", "**/*.test.ts"]
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue