@clearlysh/beehaven 0.8.16
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/LICENSE +21 -0
- package/README.md +201 -0
- package/dist/__tests__/_cli-switch.js +105 -0
- package/dist/__tests__/agent-wrap-probe.js +136 -0
- package/dist/agent-attribution.js +192 -0
- package/dist/agent-identity-daemon.js +135 -0
- package/dist/agent-onboard.js +156 -0
- package/dist/agent-scope.js +79 -0
- package/dist/agent-sessions.js +370 -0
- package/dist/agent-state.js +482 -0
- package/dist/app-manifest.js +221 -0
- package/dist/app-signer.js +54 -0
- package/dist/app-signing.js +127 -0
- package/dist/audio-play.js +98 -0
- package/dist/avatar-protocol.js +25 -0
- package/dist/avatar-route.js +79 -0
- package/dist/bee/cloud-proxy.js +130 -0
- package/dist/blocked-actions.js +120 -0
- package/dist/build-channel.js +101 -0
- package/dist/buildings/daemon-workspace.js +475 -0
- package/dist/claude-bridge.js +310 -0
- package/dist/claude-credential-store.js +633 -0
- package/dist/clearly-sync.js +518 -0
- package/dist/clearly.js +616 -0
- package/dist/cli-state.js +334 -0
- package/dist/cli.js +6973 -0
- package/dist/commands/agent-skills.js +302 -0
- package/dist/commands/agent.js +1310 -0
- package/dist/commands/app.js +1123 -0
- package/dist/commands/auth.js +171 -0
- package/dist/commands/building-shell.js +392 -0
- package/dist/commands/canvas.js +932 -0
- package/dist/commands/claude.js +218 -0
- package/dist/commands/compliance.js +387 -0
- package/dist/commands/compose.js +507 -0
- package/dist/commands/composition.js +95 -0
- package/dist/commands/connect-session.js +409 -0
- package/dist/commands/conversations.js +160 -0
- package/dist/commands/costs.js +443 -0
- package/dist/commands/d1.js +253 -0
- package/dist/commands/daemon-cmd.js +162 -0
- package/dist/commands/deploy.js +164 -0
- package/dist/commands/design.js +617 -0
- package/dist/commands/dev-generate.js +125 -0
- package/dist/commands/dev.js +109 -0
- package/dist/commands/devices.js +135 -0
- package/dist/commands/explain-router.js +24 -0
- package/dist/commands/explain.js +882 -0
- package/dist/commands/explore.js +264 -0
- package/dist/commands/help.js +2418 -0
- package/dist/commands/init.js +408 -0
- package/dist/commands/install.js +129 -0
- package/dist/commands/library.js +229 -0
- package/dist/commands/marketplace.js +138 -0
- package/dist/commands/mcp-install.js +476 -0
- package/dist/commands/mention.js +257 -0
- package/dist/commands/omni.js +529 -0
- package/dist/commands/secrets.js +323 -0
- package/dist/commands/self-update.js +206 -0
- package/dist/commands/sheet.js +264 -0
- package/dist/commands/shell-output.js +309 -0
- package/dist/commands/shell.js +1048 -0
- package/dist/commands/shop.js +368 -0
- package/dist/commands/skills.js +311 -0
- package/dist/commands/slides.js +418 -0
- package/dist/commands/swarm.js +341 -0
- package/dist/commands/ticket.js +1301 -0
- package/dist/commands/tool.js +312 -0
- package/dist/commands/trash.js +106 -0
- package/dist/commands/treehouse.js +120 -0
- package/dist/commands/user.js +240 -0
- package/dist/commands/voice-tts.js +319 -0
- package/dist/commands/voice.js +166 -0
- package/dist/commands/watch-git.js +401 -0
- package/dist/commands/webhook.js +159 -0
- package/dist/commands/workspace.js +3354 -0
- package/dist/composition-repos.js +91 -0
- package/dist/daemon-avatar.js +130 -0
- package/dist/daemon-claude-code-auto-install.js +52 -0
- package/dist/daemon-client.js +363 -0
- package/dist/daemon-codex.js +349 -0
- package/dist/daemon-entry.js +101 -0
- package/dist/daemon-fetch-allowlist.js +194 -0
- package/dist/daemon-gemini.js +224 -0
- package/dist/daemon-mcp-clearly.js +353 -0
- package/dist/daemon-mcp-sandbox.js +127 -0
- package/dist/daemon-omni-company.js +338 -0
- package/dist/daemon-omni-hooks.js +375 -0
- package/dist/daemon-omni-identity.js +229 -0
- package/dist/daemon-omni-pipeline.js +251 -0
- package/dist/daemon-omni-policies.js +346 -0
- package/dist/daemon-omni-principal.js +580 -0
- package/dist/daemon-omni-tool-gate.js +116 -0
- package/dist/daemon-omni.js +1442 -0
- package/dist/daemon-path-transform.js +120 -0
- package/dist/daemon-write-sandbox.js +191 -0
- package/dist/daemon.js +4471 -0
- package/dist/dashboard.js +846 -0
- package/dist/dep-installer.js +286 -0
- package/dist/dev-mode.js +67 -0
- package/dist/doc-indexer.js +169 -0
- package/dist/e2e-crypto.js +94 -0
- package/dist/feature-flags.js +154 -0
- package/dist/file-browser.js +88 -0
- package/dist/first-party-hives.js +211 -0
- package/dist/folder-sync.js +226 -0
- package/dist/health.js +56 -0
- package/dist/ipc-auth.js +299 -0
- package/dist/ipc-client.js +495 -0
- package/dist/ipc-server.js +684 -0
- package/dist/keychain.js +224 -0
- package/dist/lib/admin-client.js +73 -0
- package/dist/lib/mobile-build-parser.js +156 -0
- package/dist/local-app-manager.js +660 -0
- package/dist/log-redaction.js +89 -0
- package/dist/logger.js +108 -0
- package/dist/manifest.js +143 -0
- package/dist/marketplace.js +793 -0
- package/dist/min-version-copy.js +51 -0
- package/dist/min-version.js +214 -0
- package/dist/public-surface.js +23 -0
- package/dist/relay-presets.js +89 -0
- package/dist/relay.js +2723 -0
- package/dist/repl.js +619 -0
- package/dist/setup-relay.js +363 -0
- package/dist/setup-service.js +536 -0
- package/dist/shared/bee-memory/index.js +6 -0
- package/dist/shared/bee-memory/types.js +682 -0
- package/dist/shared/claude-code/daemon-types.js +112 -0
- package/dist/shared/claude-code/index.js +2 -0
- package/dist/shared/claude-code/types.js +1 -0
- package/dist/shared/status/schema.js +133 -0
- package/dist/shell-session.js +198 -0
- package/dist/speak-lock.js +97 -0
- package/dist/spinner.js +57 -0
- package/dist/telemetry.js +166 -0
- package/dist/terminal/block-parser.js +207 -0
- package/dist/terminal/shell-integration.js +108 -0
- package/dist/transcript-tailer.js +215 -0
- package/dist/tts-qwen3.js +547 -0
- package/dist/types.js +4 -0
- package/dist/utm.js +38 -0
- package/dist/verb-surface.js +331 -0
- package/dist/version-utils.js +85 -0
- package/dist/watcher.js +207 -0
- package/package.json +88 -0
- package/python/qwen3_tts.py +230 -0
- package/scripts/stamp-build-channel.mjs +115 -0
- package/scripts/sync-installed.mjs +159 -0
- package/scripts/sync-skills.mjs +112 -0
- package/skills/README.md +24 -0
- package/skills/agent-doodle-space/SKILL.md +118 -0
- package/skills/agent-home/SKILL.md +98 -0
- package/skills/beehaven-cli/SKILL.md +144 -0
- package/skills/claude-shopper/README.md +90 -0
- package/skills/claude-shopper/SKILL.md +151 -0
- package/skills/clearly-agent/SKILL.md +183 -0
- package/skills/clearly-canvas/SKILL.md +222 -0
- package/skills/clearly-canvas/authoring.md +160 -0
- package/skills/clearly-canvas/communications.md +32 -0
- package/skills/clearly-canvas/composition-patterns.md +84 -0
- package/skills/clearly-canvas/example.mjs +78 -0
- package/skills/clearly-canvas/project-plans.md +156 -0
- package/skills/clearly-canvas/review.mjs +68 -0
- package/skills/clearly-workspace/SKILL.md +79 -0
- package/skills/clearly-workspace/survey.mjs +101 -0
- package/skills/codebase-map/SKILL.md +142 -0
- package/skills/design-craft/SKILL.md +300 -0
- package/skills/index.json +79 -0
- package/skills/layout-systems/SKILL.md +184 -0
- package/skills/pair-on-canvas/SKILL.md +92 -0
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Path transformation (#63) — rewrite relative agent-supplied paths
|
|
3
|
+
* to composition-scoped or project-scoped absolute paths.
|
|
4
|
+
*
|
|
5
|
+
* When Claude/Codex/Gemini-CLI run with `cwd: composition-scoped`
|
|
6
|
+
* (per #47), they emit Read/Write/Edit calls with paths relative to
|
|
7
|
+
* that cwd. Those paths are fine for the agent's filesystem layer,
|
|
8
|
+
* but the canvas wants absolute paths so the FAB shows the right
|
|
9
|
+
* file in the diff view + the user's "Open in Finder" link works.
|
|
10
|
+
*
|
|
11
|
+
* This helper is the daemon-side rewriter — every tool call's path
|
|
12
|
+
* argument runs through `transformPath()` before dispatch.
|
|
13
|
+
*
|
|
14
|
+
* Rules:
|
|
15
|
+
* - Absolute path that's inside the composition cwd → keep as-is
|
|
16
|
+
* - Absolute path outside the composition cwd → flag as "out-of-scope"
|
|
17
|
+
* (caller decides: hard fail in strict mode, warn in dev mode)
|
|
18
|
+
* - Relative path → resolve against the composition cwd
|
|
19
|
+
* - `~/...` paths → expand via os.homedir()
|
|
20
|
+
* - `./` or `../` traversal → normalize; if it escapes the cwd, flag
|
|
21
|
+
*/
|
|
22
|
+
import { resolve as resolvePath, relative as relativePath, isAbsolute, normalize } from 'node:path';
|
|
23
|
+
import { homedir } from 'node:os';
|
|
24
|
+
/**
|
|
25
|
+
* Transform an agent-supplied path. Pure function — no I/O, safe to
|
|
26
|
+
* call on every tool invocation.
|
|
27
|
+
*/
|
|
28
|
+
export function transformPath(input, ctx) {
|
|
29
|
+
if (!input || typeof input !== 'string') {
|
|
30
|
+
return {
|
|
31
|
+
absolutePath: null,
|
|
32
|
+
relativeToComposition: null,
|
|
33
|
+
outOfScope: true,
|
|
34
|
+
reason: 'empty-path',
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
// Expand ~ to homedir. Tilde-prefixed paths are agent-friendly but
|
|
38
|
+
// need expansion before resolve().
|
|
39
|
+
let path = input.startsWith('~/')
|
|
40
|
+
? input.replace(/^~/, homedir())
|
|
41
|
+
: input === '~'
|
|
42
|
+
? homedir()
|
|
43
|
+
: input;
|
|
44
|
+
// Resolve relative to composition cwd. Absolute paths stay absolute.
|
|
45
|
+
const absolute = isAbsolute(path) ? normalize(path) : resolvePath(ctx.compositionCwd, path);
|
|
46
|
+
const rel = relativePath(ctx.compositionCwd, absolute);
|
|
47
|
+
const escapesComposition = rel.startsWith('..') || isAbsolute(rel);
|
|
48
|
+
if (!escapesComposition) {
|
|
49
|
+
return {
|
|
50
|
+
absolutePath: absolute,
|
|
51
|
+
relativeToComposition: rel || '.',
|
|
52
|
+
outOfScope: false,
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
// Out-of-composition. Check project root before deciding.
|
|
56
|
+
if (ctx.projectRoot) {
|
|
57
|
+
const relProject = relativePath(ctx.projectRoot, absolute);
|
|
58
|
+
const escapesProject = relProject.startsWith('..') || isAbsolute(relProject);
|
|
59
|
+
if (!escapesProject) {
|
|
60
|
+
// In-project, out-of-composition — warn but allow.
|
|
61
|
+
return {
|
|
62
|
+
absolutePath: absolute,
|
|
63
|
+
relativeToComposition: rel,
|
|
64
|
+
outOfScope: true,
|
|
65
|
+
reason: `outside composition cwd but inside project root (${ctx.projectRoot})`,
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
// Outside everything — strict policy rejects.
|
|
70
|
+
if (ctx.outOfScopePolicy === 'strict') {
|
|
71
|
+
return {
|
|
72
|
+
absolutePath: null,
|
|
73
|
+
relativeToComposition: null,
|
|
74
|
+
outOfScope: true,
|
|
75
|
+
reason: `strict mode — path ${absolute} is outside composition cwd ${ctx.compositionCwd}`,
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
return {
|
|
79
|
+
absolutePath: absolute,
|
|
80
|
+
relativeToComposition: rel,
|
|
81
|
+
outOfScope: true,
|
|
82
|
+
reason: `outside composition cwd ${ctx.compositionCwd}`,
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Transform every path-shaped argument in a tool input object. Walks
|
|
87
|
+
* known path-bearing field names (file_path, path, source_path,
|
|
88
|
+
* dest_path) — schema-driven rather than blanket recursion to keep
|
|
89
|
+
* false positives down.
|
|
90
|
+
*/
|
|
91
|
+
export function transformToolPaths(toolName, args, ctx) {
|
|
92
|
+
const PATH_FIELDS_BY_TOOL = {
|
|
93
|
+
Read: ['file_path', 'path'],
|
|
94
|
+
Write: ['file_path', 'path'],
|
|
95
|
+
Edit: ['file_path', 'path'],
|
|
96
|
+
NotebookEdit: ['notebook_path'],
|
|
97
|
+
Glob: ['path'],
|
|
98
|
+
Grep: ['path'],
|
|
99
|
+
Bash: [], // no path field; cwd handled separately
|
|
100
|
+
};
|
|
101
|
+
const fields = PATH_FIELDS_BY_TOOL[toolName] ?? [];
|
|
102
|
+
const transformedArgs = { ...args };
|
|
103
|
+
const warnings = [];
|
|
104
|
+
for (const field of fields) {
|
|
105
|
+
const value = args[field];
|
|
106
|
+
if (typeof value !== 'string')
|
|
107
|
+
continue;
|
|
108
|
+
const result = transformPath(value, ctx);
|
|
109
|
+
if (result.absolutePath === null) {
|
|
110
|
+
warnings.push(`${toolName}.${field}: ${result.reason}`);
|
|
111
|
+
// Strict-mode rejection — caller should abort the tool call.
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
transformedArgs[field] = result.absolutePath;
|
|
115
|
+
if (result.outOfScope && result.reason) {
|
|
116
|
+
warnings.push(`${toolName}.${field}: ${result.reason}`);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
return { transformedArgs, warnings };
|
|
120
|
+
}
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sandbox-then-commit Write policy (#62) — gate destructive Write tool
|
|
3
|
+
* calls behind a temp-directory sandbox so a buggy agent can't
|
|
4
|
+
* irrecoverably trash the user's filesystem.
|
|
5
|
+
*
|
|
6
|
+
* The pattern:
|
|
7
|
+
* 1. Agent calls `Write` with file_path X + new content.
|
|
8
|
+
* 2. canUseTool wrapper intercepts:
|
|
9
|
+
* - mkdtemp under `~/.beehaven/sandbox/<sessionId>/`
|
|
10
|
+
* - mirror the file (or its containing dir) into the sandbox
|
|
11
|
+
* - write to the sandboxed copy
|
|
12
|
+
* - return ok to the agent (it sees the write as "successful")
|
|
13
|
+
* 3. On agent:done OR explicit `commit-sandbox` IPC verb, atomically
|
|
14
|
+
* promote the sandbox into the real path.
|
|
15
|
+
* 4. On `discard-sandbox` (timeout / agent error), rm -rf the
|
|
16
|
+
* sandbox dir.
|
|
17
|
+
*
|
|
18
|
+
* Wins:
|
|
19
|
+
* - One buggy agent turn doesn't permanently corrupt the user's
|
|
20
|
+
* project. Diffs are reviewable before commit.
|
|
21
|
+
* - Pair with `git-vfs.ts` (#103 Claude→Codex handoff) — the sandbox
|
|
22
|
+
* dir is a git working tree, so the agent's edits show as
|
|
23
|
+
* untracked changes the user can `git diff` against.
|
|
24
|
+
*
|
|
25
|
+
* Wiring (Mac app rebuild required after this lands):
|
|
26
|
+
* - daemon-omni-hooks.ts: add a PreToolUse hook for `Write` that
|
|
27
|
+
* calls `wrapInSandbox()` before the agent SDK dispatches.
|
|
28
|
+
* - daemon-omni.ts: on agent:done, fire `commitSandbox(sessionId)`.
|
|
29
|
+
* - Add `commit-sandbox` + `discard-sandbox` IPC verbs in
|
|
30
|
+
* packages/cli/src/ipc-server.ts so the FAB can drive a "Review
|
|
31
|
+
* changes → Apply" approval card (#59).
|
|
32
|
+
*/
|
|
33
|
+
import { mkdir, copyFile, rename, rm, stat, readFile, writeFile } from 'node:fs/promises';
|
|
34
|
+
import { dirname, join, normalize as normalizePath, isAbsolute, } from 'node:path';
|
|
35
|
+
import { homedir } from 'node:os';
|
|
36
|
+
import { existsSync } from 'node:fs';
|
|
37
|
+
const SANDBOX_ROOT = join(homedir(), '.beehaven', 'sandbox');
|
|
38
|
+
/**
|
|
39
|
+
* Defense-in-depth path check (note from 2.19.0 audit). Callers today
|
|
40
|
+
* pre-normalize realPath via the path-transform layer, so escape-via-`..`
|
|
41
|
+
* isn't reachable in the current wiring. But this module is library
|
|
42
|
+
* code — future callers may invoke it directly. Reject anything that
|
|
43
|
+
* isn't an absolute, normalized path with no `..` segments.
|
|
44
|
+
*/
|
|
45
|
+
function assertSafeRealPath(realPath) {
|
|
46
|
+
if (typeof realPath !== 'string' || realPath.length === 0) {
|
|
47
|
+
throw new Error('wrapInSandbox: realPath must be a non-empty string');
|
|
48
|
+
}
|
|
49
|
+
if (!isAbsolute(realPath)) {
|
|
50
|
+
throw new Error(`wrapInSandbox: realPath must be absolute (got '${realPath}')`);
|
|
51
|
+
}
|
|
52
|
+
const normalized = normalizePath(realPath);
|
|
53
|
+
if (normalized !== realPath) {
|
|
54
|
+
throw new Error(`wrapInSandbox: realPath must be normalized — '${realPath}' normalizes to '${normalized}'`);
|
|
55
|
+
}
|
|
56
|
+
if (normalized.split('/').includes('..')) {
|
|
57
|
+
throw new Error(`wrapInSandbox: realPath must not contain '..' segments (got '${realPath}')`);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
const _activeSandboxes = new Map();
|
|
61
|
+
/**
|
|
62
|
+
* Wrap a Write call in a sandbox. Returns the sandboxed path the
|
|
63
|
+
* agent should actually write to. The caller is responsible for
|
|
64
|
+
* subbing that path into the agent's tool-input before dispatch.
|
|
65
|
+
*/
|
|
66
|
+
export async function wrapInSandbox(args) {
|
|
67
|
+
assertSafeRealPath(args.realPath);
|
|
68
|
+
// Per-session sandbox dir. Same session reuses the dir so multi-file
|
|
69
|
+
// writes share a coherent staging area.
|
|
70
|
+
const sessionDir = await ensureSessionDir(args.sessionId);
|
|
71
|
+
// Mirror the realPath structure inside the sandbox. For a real path
|
|
72
|
+
// `/Users/me/proj/src/foo.ts` we land at
|
|
73
|
+
// `~/.beehaven/sandbox/<sid>/Users/me/proj/src/foo.ts`. Absolute-path
|
|
74
|
+
// preservation lets the commit step paths-back-1-to-1.
|
|
75
|
+
const sandboxPath = join(sessionDir, args.realPath.replace(/^\//, ''));
|
|
76
|
+
await mkdir(dirname(sandboxPath), { recursive: true });
|
|
77
|
+
// Snapshot existing content (for restore-on-discard). Skip for
|
|
78
|
+
// files > 5 MB — we don't want to balloon disk on a large file
|
|
79
|
+
// sandbox. Those skip restore, and discard becomes "delete only."
|
|
80
|
+
let originalContent;
|
|
81
|
+
let preExisting = false;
|
|
82
|
+
if (existsSync(args.realPath)) {
|
|
83
|
+
preExisting = true;
|
|
84
|
+
try {
|
|
85
|
+
const s = await stat(args.realPath);
|
|
86
|
+
if (s.size < 5 * 1024 * 1024) {
|
|
87
|
+
originalContent = await readFile(args.realPath, 'utf8');
|
|
88
|
+
}
|
|
89
|
+
// Pre-populate the sandbox with the current file so partial
|
|
90
|
+
// edits (e.g. agent appending) start from real state.
|
|
91
|
+
await copyFile(args.realPath, sandboxPath);
|
|
92
|
+
}
|
|
93
|
+
catch {
|
|
94
|
+
// Source unreadable — let the agent write its content fresh
|
|
95
|
+
// into the sandbox.
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
// If the agent supplied content, write it to the sandbox up-front
|
|
99
|
+
// so the inspector RPC can show it without waiting for the agent
|
|
100
|
+
// to finish.
|
|
101
|
+
if (args.content !== undefined) {
|
|
102
|
+
await writeFile(sandboxPath, args.content, 'utf8');
|
|
103
|
+
}
|
|
104
|
+
const sandboxedWrite = {
|
|
105
|
+
sessionId: args.sessionId,
|
|
106
|
+
realPath: args.realPath,
|
|
107
|
+
sandboxPath,
|
|
108
|
+
preExisting,
|
|
109
|
+
originalContent,
|
|
110
|
+
ts: Date.now(),
|
|
111
|
+
};
|
|
112
|
+
const list = _activeSandboxes.get(args.sessionId) ?? [];
|
|
113
|
+
list.push(sandboxedWrite);
|
|
114
|
+
_activeSandboxes.set(args.sessionId, list);
|
|
115
|
+
return { sandboxPath, sandboxedWrite };
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Atomically promote a session's sandboxed writes into their real
|
|
119
|
+
* paths. Uses fs.rename which is atomic on the same filesystem.
|
|
120
|
+
*
|
|
121
|
+
* Returns the list of {realPath, ok, error?} per file so the UI can
|
|
122
|
+
* surface which writes landed cleanly.
|
|
123
|
+
*/
|
|
124
|
+
export async function commitSandbox(sessionId) {
|
|
125
|
+
const writes = _activeSandboxes.get(sessionId) ?? [];
|
|
126
|
+
const results = [];
|
|
127
|
+
for (const w of writes) {
|
|
128
|
+
try {
|
|
129
|
+
await mkdir(dirname(w.realPath), { recursive: true });
|
|
130
|
+
// rename is atomic on same FS; if cross-FS, fall back to copy.
|
|
131
|
+
try {
|
|
132
|
+
await rename(w.sandboxPath, w.realPath);
|
|
133
|
+
}
|
|
134
|
+
catch {
|
|
135
|
+
await copyFile(w.sandboxPath, w.realPath);
|
|
136
|
+
await rm(w.sandboxPath, { force: true });
|
|
137
|
+
}
|
|
138
|
+
results.push({ realPath: w.realPath, ok: true });
|
|
139
|
+
}
|
|
140
|
+
catch (err) {
|
|
141
|
+
results.push({ realPath: w.realPath, ok: false, error: err?.message ?? 'commit-failed' });
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
_activeSandboxes.delete(sessionId);
|
|
145
|
+
// Best-effort: nuke the session dir so the next session starts clean.
|
|
146
|
+
try {
|
|
147
|
+
await rm(await sessionDirPath(sessionId), { recursive: true, force: true });
|
|
148
|
+
}
|
|
149
|
+
catch { }
|
|
150
|
+
return results;
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* Discard a session's sandboxed writes. Restores any pre-existing
|
|
154
|
+
* content from `originalContent` snapshots; deletes new files.
|
|
155
|
+
*/
|
|
156
|
+
export async function discardSandbox(sessionId) {
|
|
157
|
+
const writes = _activeSandboxes.get(sessionId) ?? [];
|
|
158
|
+
let restored = 0;
|
|
159
|
+
for (const w of writes) {
|
|
160
|
+
if (w.preExisting && w.originalContent !== undefined) {
|
|
161
|
+
try {
|
|
162
|
+
await writeFile(w.realPath, w.originalContent, 'utf8');
|
|
163
|
+
restored++;
|
|
164
|
+
}
|
|
165
|
+
catch { }
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
_activeSandboxes.delete(sessionId);
|
|
169
|
+
try {
|
|
170
|
+
await rm(await sessionDirPath(sessionId), { recursive: true, force: true });
|
|
171
|
+
}
|
|
172
|
+
catch { }
|
|
173
|
+
return { discarded: writes.length, restored };
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* List a session's pending writes. Powers the FAB approval card's
|
|
177
|
+
* "Review N changes" affordance.
|
|
178
|
+
*/
|
|
179
|
+
export function listPendingWrites(sessionId) {
|
|
180
|
+
return _activeSandboxes.get(sessionId) ?? [];
|
|
181
|
+
}
|
|
182
|
+
async function ensureSessionDir(sessionId) {
|
|
183
|
+
const dir = await sessionDirPath(sessionId);
|
|
184
|
+
await mkdir(dir, { recursive: true });
|
|
185
|
+
return dir;
|
|
186
|
+
}
|
|
187
|
+
async function sessionDirPath(sessionId) {
|
|
188
|
+
// Sanitize sessionId — agent-supplied. Anything outside [a-z0-9-_] becomes _.
|
|
189
|
+
const safe = sessionId.replace(/[^a-z0-9\-_]/gi, '_').slice(0, 64);
|
|
190
|
+
return join(SANDBOX_ROOT, safe);
|
|
191
|
+
}
|