@madarco/agentbox 0.14.0 → 0.15.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/CHANGELOG.md +60 -0
- package/dist/{_cloud-attach-GUBB5RH2.js → _cloud-attach-R6TRWG5L.js} +3 -3
- package/dist/{chunk-BYCLD6D6.js → chunk-43Q5GWP6.js} +98 -54
- package/dist/chunk-43Q5GWP6.js.map +1 -0
- package/dist/{chunk-VATTS2MR.js → chunk-72CJTXN6.js} +2 -2
- package/dist/{chunk-TBSIJVSN.js → chunk-E7CHS7ZR.js} +21 -13
- package/dist/chunk-E7CHS7ZR.js.map +1 -0
- package/dist/{chunk-LDMYHWUS.js → chunk-MCOU6CZS.js} +2 -2
- package/dist/{chunk-TCS5HXJX.js → chunk-MLMFNN4T.js} +396 -324
- package/dist/chunk-MLMFNN4T.js.map +1 -0
- package/dist/{dist-J2IHD5T7.js → dist-AGTIA7AD.js} +3 -3
- package/dist/{dist-3IMQNTTV.js → dist-FIFEFKJ7.js} +3 -3
- package/dist/{dist-34RKQ74M.js → dist-JZ3XO6EB.js} +4 -4
- package/dist/{dist-4DPOL5A7.js → dist-OGJGZETZ.js} +2 -2
- package/dist/{dist-57M6ZA7H.js → dist-S4XR4ACV.js} +4 -4
- package/dist/index.js +868 -300
- package/dist/index.js.map +1 -1
- package/package.json +5 -5
- package/runtime/docker/apps/cli/share/agentbox-setup/SKILL.md +30 -0
- package/runtime/docker/packages/ctl/dist/bin.cjs +321 -27
- package/runtime/e2b/agentbox-setup-skill.md +30 -0
- package/runtime/e2b/ctl.cjs +321 -27
- package/runtime/hetzner/agentbox-setup-skill.md +30 -0
- package/runtime/hetzner/ctl.cjs +321 -27
- package/runtime/relay/bin.cjs +83 -5
- package/runtime/vercel/agentbox-setup-skill.md +30 -0
- package/runtime/vercel/ctl.cjs +321 -27
- package/share/agentbox-setup/SKILL.md +30 -0
- package/share/host-skills/agentbox-info/SKILL.md +21 -1
- package/dist/chunk-BYCLD6D6.js.map +0 -1
- package/dist/chunk-TBSIJVSN.js.map +0 -1
- package/dist/chunk-TCS5HXJX.js.map +0 -1
- /package/dist/{_cloud-attach-GUBB5RH2.js.map → _cloud-attach-R6TRWG5L.js.map} +0 -0
- /package/dist/{chunk-VATTS2MR.js.map → chunk-72CJTXN6.js.map} +0 -0
- /package/dist/{chunk-LDMYHWUS.js.map → chunk-MCOU6CZS.js.map} +0 -0
- /package/dist/{dist-J2IHD5T7.js.map → dist-AGTIA7AD.js.map} +0 -0
- /package/dist/{dist-3IMQNTTV.js.map → dist-FIFEFKJ7.js.map} +0 -0
- /package/dist/{dist-34RKQ74M.js.map → dist-JZ3XO6EB.js.map} +0 -0
- /package/dist/{dist-4DPOL5A7.js.map → dist-OGJGZETZ.js.map} +0 -0
- /package/dist/{dist-57M6ZA7H.js.map → dist-S4XR4ACV.js.map} +0 -0
package/runtime/vercel/ctl.cjs
CHANGED
|
@@ -3801,7 +3801,7 @@ var require_cross_spawn = __commonJS({
|
|
|
3801
3801
|
var cp = require("child_process");
|
|
3802
3802
|
var parse = require_parse();
|
|
3803
3803
|
var enoent = require_enoent();
|
|
3804
|
-
function
|
|
3804
|
+
function spawn11(command, args, options) {
|
|
3805
3805
|
const parsed = parse(command, args, options);
|
|
3806
3806
|
const spawned = cp.spawn(parsed.command, parsed.args, parsed.options);
|
|
3807
3807
|
enoent.hookChildProcess(spawned, parsed);
|
|
@@ -3813,8 +3813,8 @@ var require_cross_spawn = __commonJS({
|
|
|
3813
3813
|
result.error = result.error || enoent.verifyENOENTSync(result.status, parsed);
|
|
3814
3814
|
return result;
|
|
3815
3815
|
}
|
|
3816
|
-
module2.exports =
|
|
3817
|
-
module2.exports.spawn =
|
|
3816
|
+
module2.exports = spawn11;
|
|
3817
|
+
module2.exports.spawn = spawn11;
|
|
3818
3818
|
module2.exports.sync = spawnSync2;
|
|
3819
3819
|
module2.exports._parse = parse;
|
|
3820
3820
|
module2.exports._enoent = enoent;
|
|
@@ -11566,20 +11566,28 @@ async function postRpcAndExit(method, params, opts = {}) {
|
|
|
11566
11566
|
}
|
|
11567
11567
|
|
|
11568
11568
|
// src/commands/cp.ts
|
|
11569
|
+
function collectExclude(val, acc) {
|
|
11570
|
+
acc.push(val);
|
|
11571
|
+
return acc;
|
|
11572
|
+
}
|
|
11573
|
+
function buildCpParams(boxPath, hostPath, opts) {
|
|
11574
|
+
const params = { boxPath, hostPath };
|
|
11575
|
+
if (opts.recursive === false) params.recursive = false;
|
|
11576
|
+
if (opts.exclude.length > 0) params.exclude = opts.exclude;
|
|
11577
|
+
if (opts.defaultExcludes === false) params.defaultExcludes = false;
|
|
11578
|
+
if (opts.yes) params.yes = true;
|
|
11579
|
+
return params;
|
|
11580
|
+
}
|
|
11569
11581
|
var cpCommand = new Command("cp").description("Copy a file/dir between this box and the host (gated by user prompt on the host wrapper)").addCommand(
|
|
11570
|
-
new Command("toHost").description("Copy box:<boxPath> -> host:<hostPath>").argument("<boxPath>", "source path inside the container").argument("<hostPath>", "destination path on the host").option("--no-recursive", "reserved; current implementation is always recursive (docker cp -a)").action(async (boxPath, hostPath, opts) => {
|
|
11571
|
-
const
|
|
11572
|
-
if (opts.recursive === false) params.recursive = false;
|
|
11573
|
-
const code = await postRpcAndExit("cp.toHost", params, {
|
|
11582
|
+
new Command("toHost").description("Copy box:<boxPath> -> host:<hostPath>").argument("<boxPath>", "source path inside the container").argument("<hostPath>", "destination path on the host").option("--no-recursive", "reserved; current implementation is always recursive (docker cp -a)").option("--exclude <pattern>", "exclude paths matching <pattern> (repeatable)", collectExclude, []).option("--no-default-excludes", "keep heavy dirs the host drops by default (.git, node_modules, ...)").option("-y, --yes", "copy even if the source is over the host size limit").action(async (boxPath, hostPath, opts) => {
|
|
11583
|
+
const code = await postRpcAndExit("cp.toHost", buildCpParams(boxPath, hostPath, opts), {
|
|
11574
11584
|
errorPrefix: "agentbox-ctl cp"
|
|
11575
11585
|
});
|
|
11576
11586
|
process.exit(code);
|
|
11577
11587
|
})
|
|
11578
11588
|
).addCommand(
|
|
11579
|
-
new Command("fromHost").description("Copy host:<hostPath> -> box:<boxPath>").argument("<hostPath>", "source path on the host").argument("<boxPath>", "destination path inside the container").option("--no-recursive", "reserved; current implementation is always recursive (docker cp -a)").action(async (hostPath, boxPath, opts) => {
|
|
11580
|
-
const
|
|
11581
|
-
if (opts.recursive === false) params.recursive = false;
|
|
11582
|
-
const code = await postRpcAndExit("cp.fromHost", params, {
|
|
11589
|
+
new Command("fromHost").description("Copy host:<hostPath> -> box:<boxPath>").argument("<hostPath>", "source path on the host").argument("<boxPath>", "destination path inside the container").option("--no-recursive", "reserved; current implementation is always recursive (docker cp -a)").option("--exclude <pattern>", "exclude paths matching <pattern> (repeatable)", collectExclude, []).option("--no-default-excludes", "keep heavy dirs the host drops by default (.git, node_modules, ...)").option("-y, --yes", "copy even if the source is over the host size limit").action(async (hostPath, boxPath, opts) => {
|
|
11590
|
+
const code = await postRpcAndExit("cp.fromHost", buildCpParams(boxPath, hostPath, opts), {
|
|
11583
11591
|
errorPrefix: "agentbox-ctl cp"
|
|
11584
11592
|
});
|
|
11585
11593
|
process.exit(code);
|
|
@@ -18468,6 +18476,115 @@ var import_path2 = require("path");
|
|
|
18468
18476
|
var import_yaml2 = __toESM(require_dist(), 1);
|
|
18469
18477
|
var import_path3 = require("path");
|
|
18470
18478
|
var import_yaml3 = __toESM(require_dist(), 1);
|
|
18479
|
+
var BUILT_IN_DEFAULTS = {
|
|
18480
|
+
box: {
|
|
18481
|
+
provider: "docker",
|
|
18482
|
+
hostSnapshot: void 0,
|
|
18483
|
+
defaultCheckpoint: "",
|
|
18484
|
+
defaultCheckpointDocker: "",
|
|
18485
|
+
defaultCheckpointDaytona: "",
|
|
18486
|
+
defaultCheckpointHetzner: "",
|
|
18487
|
+
defaultCheckpointVercel: "",
|
|
18488
|
+
defaultCheckpointE2b: "",
|
|
18489
|
+
size: "",
|
|
18490
|
+
sizeDocker: "",
|
|
18491
|
+
sizeDaytona: "",
|
|
18492
|
+
sizeHetzner: "",
|
|
18493
|
+
sizeVercel: "",
|
|
18494
|
+
sizeE2b: "",
|
|
18495
|
+
withPlaywright: false,
|
|
18496
|
+
withEnv: false,
|
|
18497
|
+
resyncOnStart: true,
|
|
18498
|
+
vnc: true,
|
|
18499
|
+
autoApproveHostActions: false,
|
|
18500
|
+
isolateClaudeConfig: false,
|
|
18501
|
+
isolateCodexConfig: false,
|
|
18502
|
+
isolateOpencodeConfig: false,
|
|
18503
|
+
image: "agentbox/box:dev",
|
|
18504
|
+
imageDocker: "",
|
|
18505
|
+
imageDaytona: "",
|
|
18506
|
+
imageHetzner: "",
|
|
18507
|
+
imageVercel: "",
|
|
18508
|
+
imageE2b: "",
|
|
18509
|
+
// Mirrors BOX_IMAGE_REGISTRY in @agentbox/sandbox-docker. Empty disables the
|
|
18510
|
+
// registry pull (always build the docker base image locally).
|
|
18511
|
+
imageRegistry: "ghcr.io/madarco/agentbox/box",
|
|
18512
|
+
dockerCacheShared: false,
|
|
18513
|
+
memory: 0,
|
|
18514
|
+
cpus: 0,
|
|
18515
|
+
pidsLimit: 0,
|
|
18516
|
+
disk: "",
|
|
18517
|
+
bundleDepth: void 0,
|
|
18518
|
+
vercelVcpus: 2,
|
|
18519
|
+
vercelTimeoutMs: 27e5,
|
|
18520
|
+
vercelNetworkPolicy: "",
|
|
18521
|
+
cpMaxBytes: 100 * 1024 * 1024
|
|
18522
|
+
},
|
|
18523
|
+
checkpoint: {
|
|
18524
|
+
maxLayers: 3
|
|
18525
|
+
},
|
|
18526
|
+
claude: {
|
|
18527
|
+
sessionName: "claude",
|
|
18528
|
+
dangerouslySkipPermissions: true
|
|
18529
|
+
},
|
|
18530
|
+
codex: {
|
|
18531
|
+
sessionName: "codex",
|
|
18532
|
+
dangerouslySkipPermissions: true
|
|
18533
|
+
},
|
|
18534
|
+
opencode: {
|
|
18535
|
+
sessionName: "opencode"
|
|
18536
|
+
},
|
|
18537
|
+
attach: {
|
|
18538
|
+
openIn: "split",
|
|
18539
|
+
cmuxStatus: true
|
|
18540
|
+
},
|
|
18541
|
+
code: {
|
|
18542
|
+
ide: "auto",
|
|
18543
|
+
wait: true,
|
|
18544
|
+
timeoutMs: 12e4,
|
|
18545
|
+
autoTerminals: true
|
|
18546
|
+
},
|
|
18547
|
+
shell: {
|
|
18548
|
+
user: "vscode",
|
|
18549
|
+
login: true,
|
|
18550
|
+
tmux: true
|
|
18551
|
+
},
|
|
18552
|
+
engine: {
|
|
18553
|
+
kind: "auto"
|
|
18554
|
+
},
|
|
18555
|
+
browser: {
|
|
18556
|
+
default: "agent-browser"
|
|
18557
|
+
},
|
|
18558
|
+
relay: {
|
|
18559
|
+
port: 8787
|
|
18560
|
+
},
|
|
18561
|
+
vnc: {
|
|
18562
|
+
containerPort: 6080
|
|
18563
|
+
},
|
|
18564
|
+
portless: {
|
|
18565
|
+
enabled: void 0,
|
|
18566
|
+
stateDir: ""
|
|
18567
|
+
},
|
|
18568
|
+
autopause: {
|
|
18569
|
+
enabled: true,
|
|
18570
|
+
maxRunningBoxes: 5,
|
|
18571
|
+
idleMinutes: 5
|
|
18572
|
+
},
|
|
18573
|
+
queue: {
|
|
18574
|
+
enabled: true,
|
|
18575
|
+
maxConcurrent: 5,
|
|
18576
|
+
maxWorking: 0,
|
|
18577
|
+
idleGraceSeconds: 15,
|
|
18578
|
+
openIn: "none"
|
|
18579
|
+
},
|
|
18580
|
+
cloud: {
|
|
18581
|
+
useCurrentBranch: false
|
|
18582
|
+
},
|
|
18583
|
+
maintenance: {
|
|
18584
|
+
pruneProjectConfigs: true,
|
|
18585
|
+
pruneProjectConfigsEvery: 50
|
|
18586
|
+
}
|
|
18587
|
+
};
|
|
18471
18588
|
var KEY_REGISTRY = [
|
|
18472
18589
|
{
|
|
18473
18590
|
key: "box.provider",
|
|
@@ -18576,6 +18693,11 @@ var KEY_REGISTRY = [
|
|
|
18576
18693
|
type: "bool",
|
|
18577
18694
|
description: "Run the per-box Xvnc + noVNC stack."
|
|
18578
18695
|
},
|
|
18696
|
+
{
|
|
18697
|
+
key: "box.autoApproveHostActions",
|
|
18698
|
+
type: "bool",
|
|
18699
|
+
description: "Auto-approve host-action confirmations (git push, cp host<->box, gh PR writes, checkpoint) for this box without an interactive prompt. Off by default; intended for unattended orchestration of trusted boxes. Each auto-approval is recorded as a relay event (visible in `agentbox agent` / the dashboard)."
|
|
18700
|
+
},
|
|
18579
18701
|
{
|
|
18580
18702
|
key: "box.isolateClaudeConfig",
|
|
18581
18703
|
type: "bool",
|
|
@@ -18674,6 +18796,12 @@ var KEY_REGISTRY = [
|
|
|
18674
18796
|
type: "int",
|
|
18675
18797
|
description: "Max session length (ms) for new --provider vercel boxes before the VM auto-snapshots; persistent mode auto-resumes on the next call. Default 2700000 (45 min, the Hobby ceiling). Vercel-only."
|
|
18676
18798
|
},
|
|
18799
|
+
{
|
|
18800
|
+
key: "box.cpMaxBytes",
|
|
18801
|
+
type: "int",
|
|
18802
|
+
description: "Max bytes a single host\u2192box copy may transfer after excludes, shared by `agentbox cp` (blocked with a size breakdown unless --yes) and each `carry:` entry (rejected at resolve time). Default 104857600 (100 MiB).",
|
|
18803
|
+
advanced: true
|
|
18804
|
+
},
|
|
18677
18805
|
{
|
|
18678
18806
|
key: "box.vercelNetworkPolicy",
|
|
18679
18807
|
type: "string",
|
|
@@ -18821,6 +18949,12 @@ var KEY_REGISTRY = [
|
|
|
18821
18949
|
type: "int",
|
|
18822
18950
|
description: "Seconds an agent must stay non-working before it frees its working slot (debounce against brief idle flaps between turns). Only used when queue.maxWorking > 0."
|
|
18823
18951
|
},
|
|
18952
|
+
{
|
|
18953
|
+
key: "queue.openIn",
|
|
18954
|
+
type: "enum",
|
|
18955
|
+
enumValues: ["none", "split", "window", "tab"],
|
|
18956
|
+
description: "When a background `-i` job finishes creating its box, where the host relay opens an attached terminal onto it: `none` (default \u2014 open nothing, just queue), `split`, `window`, or `tab`. Honored only when the submitting shell runs inside tmux, cmux, or iTerm2 (the targeting is captured at submit time). Under cmux, `split` splits the pane you submitted from (falling back to the parent workspace, then a new workspace), `tab` adds a tab in the parent workspace, and `window` opens a separate workspace; iTerm2 opens relative to the frontmost window. Unlike `attach.openIn` there is no `same` mode \u2014 the box is created asynchronously, so it is always a fresh terminal."
|
|
18957
|
+
},
|
|
18824
18958
|
{
|
|
18825
18959
|
key: "cloud.useCurrentBranch",
|
|
18826
18960
|
type: "bool",
|
|
@@ -19008,6 +19142,21 @@ var EventBuffer = class {
|
|
|
19008
19142
|
};
|
|
19009
19143
|
var PendingPrompts = class {
|
|
19010
19144
|
entries = /* @__PURE__ */ new Map();
|
|
19145
|
+
autoApprove = null;
|
|
19146
|
+
/** Install the per-box auto-approve policy (relay server, once at startup). */
|
|
19147
|
+
setAutoApprovePolicy(policy) {
|
|
19148
|
+
this.autoApprove = policy;
|
|
19149
|
+
}
|
|
19150
|
+
/**
|
|
19151
|
+
* True when this box opted into `box.autoApproveHostActions`. Records the
|
|
19152
|
+
* bypass to the audit sink as a side effect so the caller short-circuits
|
|
19153
|
+
* with a trail. Returns false when no policy is installed.
|
|
19154
|
+
*/
|
|
19155
|
+
consumeAutoApprove(boxId, params) {
|
|
19156
|
+
if (!this.autoApprove || !this.autoApprove.shouldAutoApprove(boxId)) return false;
|
|
19157
|
+
this.autoApprove.audit(boxId, params);
|
|
19158
|
+
return true;
|
|
19159
|
+
}
|
|
19011
19160
|
add(boxId, ev) {
|
|
19012
19161
|
return new Promise((resolve2) => {
|
|
19013
19162
|
this.entries.set(ev.id, {
|
|
@@ -19094,6 +19243,9 @@ async function askPrompt(prompts, subscribers, boxId, params, opts) {
|
|
|
19094
19243
|
if (process.env.AGENTBOX_PROMPT === "off") {
|
|
19095
19244
|
return { answer: "y" };
|
|
19096
19245
|
}
|
|
19246
|
+
if (prompts.consumeAutoApprove(boxId, params)) {
|
|
19247
|
+
return { answer: "y" };
|
|
19248
|
+
}
|
|
19097
19249
|
const ev = { id: (0, import_crypto2.randomUUID)(), ...params };
|
|
19098
19250
|
const promise = prompts.add(boxId, ev);
|
|
19099
19251
|
subscribers.broadcast(boxId, "prompt-ask", ev);
|
|
@@ -20240,6 +20392,21 @@ function createRelayServer(opts) {
|
|
|
20240
20392
|
const prompts = new PendingPrompts();
|
|
20241
20393
|
const subscribers = new PromptSubscribers();
|
|
20242
20394
|
const notices = new BoxNotices(subscribers);
|
|
20395
|
+
prompts.setAutoApprovePolicy({
|
|
20396
|
+
shouldAutoApprove: (boxId) => registry.get(boxId)?.autoApproveHostActions === true,
|
|
20397
|
+
audit: (boxId, params) => {
|
|
20398
|
+
events.append({
|
|
20399
|
+
boxId,
|
|
20400
|
+
type: "host-action-auto-approved",
|
|
20401
|
+
payload: {
|
|
20402
|
+
command: params.context?.command,
|
|
20403
|
+
argv: params.context?.argv,
|
|
20404
|
+
message: params.message
|
|
20405
|
+
}
|
|
20406
|
+
});
|
|
20407
|
+
log(`auto-approved host action for ${boxId}: ${params.context?.command ?? params.message}`);
|
|
20408
|
+
}
|
|
20409
|
+
});
|
|
20243
20410
|
const hostInitiatedTokens = new HostInitiatedTokens();
|
|
20244
20411
|
let queuePoke = null;
|
|
20245
20412
|
const host = opts.host ?? "0.0.0.0";
|
|
@@ -20427,11 +20594,22 @@ function createRelayServer(opts) {
|
|
|
20427
20594
|
send(res, 400, { error: "cp.* requires {boxPath, hostPath} strings" });
|
|
20428
20595
|
return;
|
|
20429
20596
|
}
|
|
20597
|
+
if (params.exclude !== void 0 && (!Array.isArray(params.exclude) || params.exclude.some((p) => typeof p !== "string"))) {
|
|
20598
|
+
send(res, 400, { error: "cp.* exclude must be an array of strings" });
|
|
20599
|
+
return;
|
|
20600
|
+
}
|
|
20430
20601
|
const direction = body.method === "cp.toHost" ? "box -> host" : "host -> box";
|
|
20602
|
+
const pathDetail = body.method === "cp.toHost" ? `${params.boxPath} -> ${params.hostPath}` : `${params.hostPath} -> ${params.boxPath}`;
|
|
20603
|
+
const detailParts = [pathDetail];
|
|
20604
|
+
if (params.exclude && params.exclude.length > 0) {
|
|
20605
|
+
detailParts.push(`exclude: ${params.exclude.join(", ")}`);
|
|
20606
|
+
}
|
|
20607
|
+
if (params.defaultExcludes === false) detailParts.push("(default excludes off)");
|
|
20608
|
+
if (params.yes) detailParts.push("(over size limit \u2014 confirmed)");
|
|
20431
20609
|
const verdict = await askPrompt(prompts, subscribers, reg.boxId, {
|
|
20432
20610
|
kind: "confirm",
|
|
20433
20611
|
message: `Allow cp (${direction}) on ${reg.name}?`,
|
|
20434
|
-
detail:
|
|
20612
|
+
detail: detailParts.join("\n"),
|
|
20435
20613
|
defaultAnswer: "n",
|
|
20436
20614
|
context: {
|
|
20437
20615
|
command: body.method,
|
|
@@ -20597,7 +20775,8 @@ function createRelayServer(opts) {
|
|
|
20597
20775
|
worktrees,
|
|
20598
20776
|
previewUrl: typeof body.previewUrl === "string" && body.previewUrl.length > 0 ? body.previewUrl : void 0,
|
|
20599
20777
|
previewToken: typeof body.previewToken === "string" && body.previewToken.length > 0 ? body.previewToken : void 0,
|
|
20600
|
-
bridgeToken: typeof body.bridgeToken === "string" && body.bridgeToken.length > 0 ? body.bridgeToken : void 0
|
|
20778
|
+
bridgeToken: typeof body.bridgeToken === "string" && body.bridgeToken.length > 0 ? body.bridgeToken : void 0,
|
|
20779
|
+
autoApproveHostActions: body.autoApproveHostActions === true
|
|
20601
20780
|
};
|
|
20602
20781
|
registry.register(reg);
|
|
20603
20782
|
log(
|
|
@@ -20705,6 +20884,15 @@ function createRelayServer(opts) {
|
|
|
20705
20884
|
send(res, 200, { boxes: redacted });
|
|
20706
20885
|
return;
|
|
20707
20886
|
}
|
|
20887
|
+
if (route === "GET /admin/prompts") {
|
|
20888
|
+
const boxId = url.searchParams.get("boxId") ?? "";
|
|
20889
|
+
if (boxId.length === 0) {
|
|
20890
|
+
send(res, 400, { error: "missing boxId query param" });
|
|
20891
|
+
return;
|
|
20892
|
+
}
|
|
20893
|
+
send(res, 200, { prompts: prompts.forBox(boxId) });
|
|
20894
|
+
return;
|
|
20895
|
+
}
|
|
20708
20896
|
if (route === "GET /admin/prompts/stream") {
|
|
20709
20897
|
const boxId = url.searchParams.get("boxId") ?? "";
|
|
20710
20898
|
if (boxId.length === 0) {
|
|
@@ -21022,7 +21210,11 @@ async function handleCpRpc(reg, method, params) {
|
|
|
21022
21210
|
};
|
|
21023
21211
|
}
|
|
21024
21212
|
const boxRef = `${reg.name}:${params.boxPath}`;
|
|
21025
|
-
const
|
|
21213
|
+
const flags = [];
|
|
21214
|
+
for (const pat of params.exclude ?? []) flags.push("--exclude", pat);
|
|
21215
|
+
if (params.defaultExcludes === false) flags.push("--no-default-excludes");
|
|
21216
|
+
if (params.yes) flags.push("--yes");
|
|
21217
|
+
const argv = method === "cp.toHost" ? [process.execPath, entry, "cp", boxRef, params.hostPath, ...flags] : [process.execPath, entry, "cp", params.hostPath, boxRef, ...flags];
|
|
21026
21218
|
return runHostCommand(argv, CP_RPC_TIMEOUT_MS);
|
|
21027
21219
|
}
|
|
21028
21220
|
async function handleDownloadRpc(reg, kind) {
|
|
@@ -21639,8 +21831,68 @@ function defaultCapturePane(sessionName) {
|
|
|
21639
21831
|
});
|
|
21640
21832
|
}
|
|
21641
21833
|
|
|
21642
|
-
// src/
|
|
21834
|
+
// src/claude-scraper.ts
|
|
21643
21835
|
var import_node_child_process8 = require("child_process");
|
|
21836
|
+
var DEFAULT_INTERVAL_MS2 = 1500;
|
|
21837
|
+
var DEFAULT_SESSION2 = "claude";
|
|
21838
|
+
var BOTTOM_LINES = 25;
|
|
21839
|
+
var WORKING_GUARD = /\besc to interrupt\b|ctrl-?c to (stop|interrupt)/i;
|
|
21840
|
+
var WAITING_PATTERNS = [
|
|
21841
|
+
/❯\s*\d+\.\s/,
|
|
21842
|
+
// the selector arrow on a numbered option — the Claude select menu
|
|
21843
|
+
/Do you want to proceed\?/i,
|
|
21844
|
+
/Would you like to proceed\?/i,
|
|
21845
|
+
/\b\d+\.\s+Yes\b/,
|
|
21846
|
+
// a "N. Yes" option line
|
|
21847
|
+
/\b(wants to (use|run|access)|Allow .* to (use|run|access))/i
|
|
21848
|
+
// MCP / tool trust dialog
|
|
21849
|
+
];
|
|
21850
|
+
function startClaudeScraper(opts) {
|
|
21851
|
+
const sessionName = opts.sessionName ?? DEFAULT_SESSION2;
|
|
21852
|
+
const intervalMs = opts.intervalMs ?? DEFAULT_INTERVAL_MS2;
|
|
21853
|
+
const capture = opts.capturePane ?? defaultCapturePane2;
|
|
21854
|
+
let stopped = false;
|
|
21855
|
+
const tick = async () => {
|
|
21856
|
+
if (stopped) return;
|
|
21857
|
+
try {
|
|
21858
|
+
const pane = await capture(sessionName);
|
|
21859
|
+
if (pane === null) return;
|
|
21860
|
+
if (matchWaiting(pane)) opts.reporter.markScreenWaiting();
|
|
21861
|
+
} catch {
|
|
21862
|
+
}
|
|
21863
|
+
};
|
|
21864
|
+
const timer = setInterval(() => void tick(), intervalMs);
|
|
21865
|
+
timer.unref();
|
|
21866
|
+
void tick();
|
|
21867
|
+
return {
|
|
21868
|
+
stop() {
|
|
21869
|
+
stopped = true;
|
|
21870
|
+
clearInterval(timer);
|
|
21871
|
+
}
|
|
21872
|
+
};
|
|
21873
|
+
}
|
|
21874
|
+
function matchWaiting(pane) {
|
|
21875
|
+
const region = pane.split("\n").slice(-BOTTOM_LINES).join("\n");
|
|
21876
|
+
if (WORKING_GUARD.test(region)) return false;
|
|
21877
|
+
return WAITING_PATTERNS.some((re) => re.test(region));
|
|
21878
|
+
}
|
|
21879
|
+
function defaultCapturePane2(sessionName) {
|
|
21880
|
+
return new Promise((resolve2) => {
|
|
21881
|
+
const child = (0, import_node_child_process8.spawn)("tmux", ["capture-pane", "-p", "-t", sessionName], {
|
|
21882
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
21883
|
+
});
|
|
21884
|
+
let stdout = "";
|
|
21885
|
+
child.stdout.on("data", (b) => stdout += b.toString("utf8"));
|
|
21886
|
+
child.on("error", () => resolve2(null));
|
|
21887
|
+
child.on("close", (code) => {
|
|
21888
|
+
if (code === 0) resolve2(stdout);
|
|
21889
|
+
else resolve2(null);
|
|
21890
|
+
});
|
|
21891
|
+
});
|
|
21892
|
+
}
|
|
21893
|
+
|
|
21894
|
+
// src/supervisor.ts
|
|
21895
|
+
var import_node_child_process9 = require("child_process");
|
|
21644
21896
|
var import_node_events15 = require("events");
|
|
21645
21897
|
var import_node_fs7 = require("fs");
|
|
21646
21898
|
var import_promises18 = require("fs/promises");
|
|
@@ -21906,7 +22158,7 @@ var cachedLoginPath;
|
|
|
21906
22158
|
function loginShellPath() {
|
|
21907
22159
|
if (cachedLoginPath !== void 0) return cachedLoginPath;
|
|
21908
22160
|
try {
|
|
21909
|
-
const out = (0,
|
|
22161
|
+
const out = (0, import_node_child_process9.execFileSync)("bash", ["-lc", 'printf %s "$PATH"'], {
|
|
21910
22162
|
encoding: "utf8",
|
|
21911
22163
|
timeout: 5e3
|
|
21912
22164
|
}).trim();
|
|
@@ -21921,7 +22173,7 @@ var ServiceRunner = class extends import_node_events15.EventEmitter {
|
|
|
21921
22173
|
super();
|
|
21922
22174
|
this.spec = spec;
|
|
21923
22175
|
this.opts = opts;
|
|
21924
|
-
this.spawnFn = opts.spawn ??
|
|
22176
|
+
this.spawnFn = opts.spawn ?? import_node_child_process9.spawn;
|
|
21925
22177
|
this.setTimer = opts.setTimer ?? ((fn, ms) => setTimeout(fn, ms));
|
|
21926
22178
|
this.clearTimer = opts.clearTimer ?? ((h2) => {
|
|
21927
22179
|
clearTimeout(h2);
|
|
@@ -22152,7 +22404,7 @@ var TaskRunner = class extends import_node_events15.EventEmitter {
|
|
|
22152
22404
|
super();
|
|
22153
22405
|
this.spec = spec;
|
|
22154
22406
|
this.opts = opts;
|
|
22155
|
-
this.spawnFn = opts.spawn ??
|
|
22407
|
+
this.spawnFn = opts.spawn ?? import_node_child_process9.spawn;
|
|
22156
22408
|
}
|
|
22157
22409
|
spec;
|
|
22158
22410
|
opts;
|
|
@@ -22325,6 +22577,20 @@ var Supervisor = class extends import_node_events15.EventEmitter {
|
|
|
22325
22577
|
}
|
|
22326
22578
|
return out;
|
|
22327
22579
|
}
|
|
22580
|
+
/**
|
|
22581
|
+
* Set of service names that declare a `ready_when` probe of any kind (port or
|
|
22582
|
+
* log_match). A probed service is only "up" once it reaches `ready`; an
|
|
22583
|
+
* unprobed one is up at `running`. The status reporter surfaces this so the
|
|
22584
|
+
* host can tell a warming-up `running` service from a settled one.
|
|
22585
|
+
*/
|
|
22586
|
+
probedServices() {
|
|
22587
|
+
const out = /* @__PURE__ */ new Set();
|
|
22588
|
+
for (const u2 of this.units.values()) {
|
|
22589
|
+
if (u2.kind !== "service") continue;
|
|
22590
|
+
if (u2.spec.readyWhen) out.add(u2.name);
|
|
22591
|
+
}
|
|
22592
|
+
return out;
|
|
22593
|
+
}
|
|
22328
22594
|
/**
|
|
22329
22595
|
* Map of service name -> `expose:` mapping, for the (at most one) service
|
|
22330
22596
|
* that declares it. The status reporter surfaces this so the host knows the
|
|
@@ -22701,10 +22967,10 @@ var import_promises19 = require("fs/promises");
|
|
|
22701
22967
|
var import_node_path7 = require("path");
|
|
22702
22968
|
|
|
22703
22969
|
// src/status-reporter.ts
|
|
22704
|
-
var
|
|
22970
|
+
var import_node_child_process11 = require("child_process");
|
|
22705
22971
|
|
|
22706
22972
|
// src/tmux.ts
|
|
22707
|
-
var
|
|
22973
|
+
var import_node_child_process10 = require("child_process");
|
|
22708
22974
|
var import_node_os4 = require("os");
|
|
22709
22975
|
var MAX_TITLE_LEN = 120;
|
|
22710
22976
|
function sanitizePaneTitle(raw, ctx) {
|
|
@@ -22718,7 +22984,7 @@ function sanitizePaneTitle(raw, ctx) {
|
|
|
22718
22984
|
}
|
|
22719
22985
|
function runTool(cmd, args) {
|
|
22720
22986
|
return new Promise((resolve2) => {
|
|
22721
|
-
const child = (0,
|
|
22987
|
+
const child = (0, import_node_child_process10.spawn)(cmd, args, { stdio: ["ignore", "pipe", "pipe"] });
|
|
22722
22988
|
let stdout = "";
|
|
22723
22989
|
let stderr = "";
|
|
22724
22990
|
child.stdout.on("data", (b) => stdout += b.toString("utf8"));
|
|
@@ -22815,6 +23081,23 @@ var StatusReporter = class {
|
|
|
22815
23081
|
}
|
|
22816
23082
|
this.schedulePush();
|
|
22817
23083
|
}
|
|
23084
|
+
/**
|
|
23085
|
+
* Screen-scraper safety net: promote a *stuck* `working` to `waiting` when the
|
|
23086
|
+
* Claude tmux pane shows a prompt the hooks missed (MCP tool dialogs have no
|
|
23087
|
+
* hook; the `Notification:permission_prompt` hook can fire late or drop).
|
|
23088
|
+
* Deliberately promote-ONLY — it acts solely when the current state is
|
|
23089
|
+
* `working`, so it never clobbers the richer hook-driven `end-plan`/`question`
|
|
23090
|
+
* (sticky) or `idle`/`compacting`/`error`. The next real hook
|
|
23091
|
+
* (`UserPromptSubmit`/`PreToolUse`) overwrites `waiting`→`working` when the
|
|
23092
|
+
* agent resumes, so no demote path is needed. Returns true if it promoted.
|
|
23093
|
+
*/
|
|
23094
|
+
markScreenWaiting() {
|
|
23095
|
+
if (this.claudeState !== "working") return false;
|
|
23096
|
+
this.claudeState = "waiting";
|
|
23097
|
+
this.claudeUpdatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
23098
|
+
this.schedulePush();
|
|
23099
|
+
return true;
|
|
23100
|
+
}
|
|
22818
23101
|
setCodexState(state) {
|
|
22819
23102
|
this.codexState = state;
|
|
22820
23103
|
this.codexUpdatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
@@ -22851,11 +23134,13 @@ var StatusReporter = class {
|
|
|
22851
23134
|
}
|
|
22852
23135
|
async snapshot() {
|
|
22853
23136
|
const probePorts = this.supervisor.serviceProbePorts();
|
|
23137
|
+
const probed = this.supervisor.probedServices();
|
|
22854
23138
|
const exposes = this.supervisor.serviceExposes();
|
|
22855
23139
|
const services = this.supervisor.list().map((s) => ({
|
|
22856
23140
|
name: s.name,
|
|
22857
23141
|
state: s.state,
|
|
22858
23142
|
port: probePorts.get(s.name) ?? null,
|
|
23143
|
+
...probed.has(s.name) ? { probed: true } : {},
|
|
22859
23144
|
...exposes.has(s.name) ? { expose: exposes.get(s.name) } : {}
|
|
22860
23145
|
}));
|
|
22861
23146
|
const tasks = this.supervisor.listTasks().map((t) => ({ name: t.name, state: t.state }));
|
|
@@ -22911,7 +23196,7 @@ async function collectPorts(supervisor) {
|
|
|
22911
23196
|
}
|
|
22912
23197
|
function run(cmd, args) {
|
|
22913
23198
|
return new Promise((resolve2) => {
|
|
22914
|
-
const child = (0,
|
|
23199
|
+
const child = (0, import_node_child_process11.spawn)(cmd, args, { stdio: ["ignore", "pipe", "ignore"] });
|
|
22915
23200
|
let stdout = "";
|
|
22916
23201
|
child.stdout.on("data", (b) => stdout += b.toString("utf8"));
|
|
22917
23202
|
child.on("error", () => resolve2({ exitCode: 127, stdout }));
|
|
@@ -23248,6 +23533,14 @@ var daemonCommand = new Command("daemon").description("Run the agentbox-ctl supe
|
|
|
23248
23533
|
} catch (err) {
|
|
23249
23534
|
const msg = err instanceof Error ? err.message : String(err);
|
|
23250
23535
|
process.stderr.write(`agentbox-ctl: codex scraper failed to start: ${msg}
|
|
23536
|
+
`);
|
|
23537
|
+
}
|
|
23538
|
+
let claudeScraper = null;
|
|
23539
|
+
try {
|
|
23540
|
+
claudeScraper = startClaudeScraper({ reporter });
|
|
23541
|
+
} catch (err) {
|
|
23542
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
23543
|
+
process.stderr.write(`agentbox-ctl: claude scraper failed to start: ${msg}
|
|
23251
23544
|
`);
|
|
23252
23545
|
}
|
|
23253
23546
|
const server = await startServer({
|
|
@@ -23325,6 +23618,7 @@ var daemonCommand = new Command("daemon").description("Run the agentbox-ctl supe
|
|
|
23325
23618
|
process.stdout.write(`agentbox-ctl: ${signal} \u2014 shutting down
|
|
23326
23619
|
`);
|
|
23327
23620
|
if (codexScraper) codexScraper.stop();
|
|
23621
|
+
if (claudeScraper) claudeScraper.stop();
|
|
23328
23622
|
reporter.stop();
|
|
23329
23623
|
reporter.flush();
|
|
23330
23624
|
server.close();
|
|
@@ -23486,7 +23780,7 @@ var apiCommand = new Command("api").description(
|
|
|
23486
23780
|
var ghCommand = new Command("gh").description("GitHub CLI operations routed through the relay (host `gh` runs with host creds; box never sees a token)").addCommand(buildPrCommand("agentbox-ctl gh pr")).addCommand(buildRunCommand("agentbox-ctl gh run")).addCommand(apiCommand).addCommand(repoCommand);
|
|
23487
23781
|
|
|
23488
23782
|
// src/commands/git.ts
|
|
23489
|
-
var
|
|
23783
|
+
var import_node_child_process12 = require("child_process");
|
|
23490
23784
|
function hostInitiatedOption() {
|
|
23491
23785
|
return new Option(
|
|
23492
23786
|
"--host-initiated-token <token>",
|
|
@@ -23502,7 +23796,7 @@ function buildParams(opts, extra) {
|
|
|
23502
23796
|
}
|
|
23503
23797
|
function runLocalGit(args, cwd) {
|
|
23504
23798
|
return new Promise((resolve2) => {
|
|
23505
|
-
const child = (0,
|
|
23799
|
+
const child = (0, import_node_child_process12.spawn)("git", args, { cwd, stdio: "inherit" });
|
|
23506
23800
|
child.on("close", (code) => resolve2(code ?? 1));
|
|
23507
23801
|
child.on("error", (err) => {
|
|
23508
23802
|
process.stderr.write(`agentbox-ctl git: ${String(err.message ?? err)}
|
|
@@ -23513,7 +23807,7 @@ function runLocalGit(args, cwd) {
|
|
|
23513
23807
|
}
|
|
23514
23808
|
function hasGitIdentity(cwd) {
|
|
23515
23809
|
return new Promise((resolve2) => {
|
|
23516
|
-
const child = (0,
|
|
23810
|
+
const child = (0, import_node_child_process12.spawn)("git", ["config", "user.email"], {
|
|
23517
23811
|
cwd,
|
|
23518
23812
|
stdio: ["ignore", "pipe", "ignore"]
|
|
23519
23813
|
});
|
|
@@ -23615,7 +23909,7 @@ var notifyCommand = new Command("notify").description(
|
|
|
23615
23909
|
);
|
|
23616
23910
|
|
|
23617
23911
|
// src/commands/open.ts
|
|
23618
|
-
var
|
|
23912
|
+
var import_node_child_process13 = require("child_process");
|
|
23619
23913
|
var OPEN_TIMEOUT_MS = 3e4;
|
|
23620
23914
|
function isHttpUrl(value) {
|
|
23621
23915
|
try {
|
|
@@ -23627,7 +23921,7 @@ function isHttpUrl(value) {
|
|
|
23627
23921
|
}
|
|
23628
23922
|
function openInBoxBrowser(url) {
|
|
23629
23923
|
return new Promise((resolve2) => {
|
|
23630
|
-
const child = (0,
|
|
23924
|
+
const child = (0, import_node_child_process13.spawn)("agent-browser", ["open", "--headed", url], { stdio: "inherit" });
|
|
23631
23925
|
const timer = setTimeout(() => {
|
|
23632
23926
|
child.kill("SIGTERM");
|
|
23633
23927
|
process.stderr.write(
|
|
@@ -192,6 +192,36 @@ services:
|
|
|
192
192
|
factor: 2
|
|
193
193
|
```
|
|
194
194
|
|
|
195
|
+
## 6b. Bringing extra host files/folders into the box
|
|
196
|
+
|
|
197
|
+
Two ways to copy host files in (both COPY — never a live mount, so the box can't
|
|
198
|
+
write back to the host):
|
|
199
|
+
|
|
200
|
+
- **`carry:` block** (declarative, in `agentbox.yaml`) — for files/dirs every box
|
|
201
|
+
should get at create time. Each entry is `{ src, dest }` with optional `mode`,
|
|
202
|
+
`user`, `optional`, and `exclude:` (a list of tar globs / bare dir names to drop
|
|
203
|
+
when copying a directory). Heavy regenerable dirs (`.git`, `node_modules`, `bin`,
|
|
204
|
+
`obj`, `packages`, `dist`, `.next`, `target`) are dropped by default; `exclude:`
|
|
205
|
+
is additive. Each carry entry is capped at `box.cpMaxBytes` (default 100 MiB
|
|
206
|
+
after excludes) — the same limit `agentbox cp` enforces.
|
|
207
|
+
- **`agentbox-ctl cp fromHost <hostPath> <boxPath>`** (ad-hoc, from inside the box)
|
|
208
|
+
— for a one-off copy. Prompts the user on the host to approve.
|
|
209
|
+
|
|
210
|
+
**The per-copy size limit (important for large/legacy folders).** A single copy is
|
|
211
|
+
blocked above `box.cpMaxBytes` (default **100 MB**) *after* default excludes, so it
|
|
212
|
+
fails loud instead of silently hanging. When blocked you get a `du`-style tree of
|
|
213
|
+
the biggest remaining folders/subfolders. To get under the limit, EITHER:
|
|
214
|
+
|
|
215
|
+
- **drop what the box can regenerate** (the default excludes already remove
|
|
216
|
+
`node_modules`/`.git`/build output; add more with `--exclude=<glob-or-name>`), OR
|
|
217
|
+
- **copy the heavy folders one at a time** so each copy is under the limit, OR
|
|
218
|
+
- pass `--yes` to copy the whole thing anyway (only when you really need it all).
|
|
219
|
+
|
|
220
|
+
Example: a 2.4 GB legacy folder is mostly `packages/` (NuGet) + `.git`; those are
|
|
221
|
+
excluded by default, and what's left can be split:
|
|
222
|
+
`agentbox-ctl cp fromHost ../legacy/src /workspace/legacy/src` then
|
|
223
|
+
`... cp fromHost ../legacy/Database /workspace/legacy/Database`.
|
|
224
|
+
|
|
195
225
|
## 7. Validate before handing off
|
|
196
226
|
|
|
197
227
|
- check with `agentbox-ctl reload` and then `agentbox-ctl status` that everything is running as expected.
|
|
@@ -174,10 +174,29 @@ Wrap step 2 in a loop to babysit a box across many turns. Use the narrow `wait-f
|
|
|
174
174
|
Implications for you, the host-side agent:
|
|
175
175
|
|
|
176
176
|
- Inside the box you can `git commit … && git push` exactly as normal. No setup needed.
|
|
177
|
-
- Pushes are gated host-side: the relay can require a confirm prompt for destructive operations (the user sees it in the dashboard footer, ~25 s TTL). If a push appears to hang,
|
|
177
|
+
- Pushes are gated host-side: the relay can require a confirm prompt for destructive operations (the user sees it in the dashboard footer, ~25 s TTL). If a push appears to hang, it's waiting on this approval — see "Answering host-action approvals" below.
|
|
178
178
|
- The relay process is started lazily by the first `agentbox create` / `agentbox claude` and persists across runs (PID at `~/.agentbox/relay.pid`, log at `~/.agentbox/relay.log`). You normally don't need to manage it.
|
|
179
179
|
- For HTTPS origins (`https://github.com/...`), pushing usually needs a credential — recommend the user run `gh auth login` and `gh auth setup-git` once on the host. After that, host `git push` uses gh's OAuth token automatically. SSH origins (`git@github.com:...`) keep using the host's SSH agent as before.
|
|
180
180
|
|
|
181
|
+
## Answering approvals (orchestrator path)
|
|
182
|
+
|
|
183
|
+
When you are **orchestrating boxes unattended** (no human watching the dashboard footer), a box blocks on two kinds of approval and `agent approvals` / `agent approve` cover **both**:
|
|
184
|
+
|
|
185
|
+
- **Relay host-action approvals** — `git push` / `cp` / `gh pr` write / checkpoint. You answer them yourself; you're a host process that already holds the user's git/file credentials, so approving grants nothing you don't already have.
|
|
186
|
+
- **In-TUI agent prompts** — Claude plan-mode approval, `AskUserQuestion`, a tool-permission dialog. Previously you had to craft `drive keypress` sends by hand; now `approve` enacts the right keystrokes for you.
|
|
187
|
+
|
|
188
|
+
```bash
|
|
189
|
+
agentbox agent approvals 1 --json # list everything box 1 is blocked on: each row has an id + kind
|
|
190
|
+
agentbox agent approve <id> # answer that exact prompt (default = approve / first option)
|
|
191
|
+
agentbox agent approve <id> --option 2 # in-TUI question/plan: pick option 2 (or --option "Risk first")
|
|
192
|
+
agentbox agent approve <id> --deny # reject (relay: deny; in-TUI: Escape)
|
|
193
|
+
agentbox agent approvals 1 --wait 600000 # block until something is pending, then act
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
`kind` is `host-action` (relay), or `plan` / `question` / `permission` (in-TUI). Relay rows carry `command`/`argv`; `question` rows carry the option labels; `plan` rows the plan body.
|
|
197
|
+
|
|
198
|
+
**The id is a safety token — inspect, then approve that exact id.** `approve <id>` answers the specific prompt you listed; if a *different* prompt has since taken its place, the recomputed id won't match and the approve is **refused** (it never answers the wrong thing). So always `approvals` → read the `command`/`argv`/options → `approve <id>`, one at a time. Do not blanket-approve whatever a box asks (that defeats the gate against a prompt-injected box laundering a malicious push), and never hand-`curl` `/admin/prompts/answer` — these commands are the supported surface. In-TUI keystroke mapping is best-effort and TUI-version-sensitive; if an approve doesn't take, fall back to `drive snapshot` + `drive keypress`.
|
|
199
|
+
|
|
181
200
|
## PRs through the host relay (`agentbox-ctl git pr …`)
|
|
182
201
|
|
|
183
202
|
In-box agents can drive GitHub PRs from inside a box via the host's `gh` CLI. Same model as `git push`: the box has no GitHub token; the relay shells out to `gh` on the host with the user's authenticated gh identity. Requires `gh` installed on the host and `gh auth login` run once.
|
|
@@ -208,6 +227,7 @@ If a PR op appears to hang, tell the user to check the dashboard footer for the
|
|
|
208
227
|
| `agentbox code [n\|name]` | Open VS Code / Cursor pointed at the box. |
|
|
209
228
|
| `agentbox prepare --provider <name>` | One-time base image / snapshot build for `daytona` or `hetzner` or `vercel`. With no `--provider`, prints status across all providers. |
|
|
210
229
|
| `agentbox prune --provider <name>` | Clean up orphan boxes / images / snapshots for a provider (docker + daytona supported; hetzner pending). |
|
|
230
|
+
| `agentbox cp <src> <dst>` | Copy a file/dir host↔box (`box:/path` prefix picks direction). Heavy dirs (`.git`, `node_modules`, build output) are dropped by default; add `--exclude=<glob\|name>` or `--no-default-excludes`. Uploads over `box.cpMaxBytes` (100 MB, post-exclude) are **blocked** with a size breakdown — trim with `--exclude`, copy heavy folders one at a time, or pass `--yes`. |
|
|
211
231
|
|
|
212
232
|
Per-project numeric index (`1`, `2`, …) and friendly name (`review`, `smoke`) both work wherever `<box>` is accepted. Index `1` is the first box created in the current workspace.
|
|
213
233
|
|