@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,286 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dep-installer — daemon-side handler for dependency framework installs.
|
|
3
|
+
*
|
|
4
|
+
* Invoked by daemon.ts when relay broadcasts `agent:deps-install`. Dispatches
|
|
5
|
+
* by `spec.installMethod` to the right download/extract logic, writes to the
|
|
6
|
+
* shared root (`~/Library/Application Support/Clearly/{bin,models,venv}/`),
|
|
7
|
+
* reports progress via a streaming callback the daemon pipes back to the
|
|
8
|
+
* worker's deps-install-progress RPC.
|
|
9
|
+
*
|
|
10
|
+
* Install methods supported:
|
|
11
|
+
* - binary-download — fetch a single executable, mark +x, write to bin/
|
|
12
|
+
* - huggingface-model — fetch N files from a HF repo to models/<variant>/
|
|
13
|
+
* - pip-package — pip install into the shared Python venv
|
|
14
|
+
* - npm-package — informational only (npm deps bundled at app pack time)
|
|
15
|
+
*
|
|
16
|
+
* All writes land under ~/Library/Application Support/Clearly/ — never inside
|
|
17
|
+
* a specific hive's appDir. Rationale + path layout in:
|
|
18
|
+
* docs/ARCHITECTURE-LOCAL-WORKSPACES.md §3
|
|
19
|
+
* docs/HIVE-DEP-VERIFICATION.md
|
|
20
|
+
*
|
|
21
|
+
* SHA256 verification runs when spec.installArgs.sha256 is populated. Until
|
|
22
|
+
* Sprint C2 lands real hashes for the vtracer binary etc., the verify path
|
|
23
|
+
* is best-effort.
|
|
24
|
+
*/
|
|
25
|
+
import { existsSync, mkdirSync, createWriteStream, unlinkSync, chmodSync, statSync, rmSync, } from 'fs';
|
|
26
|
+
import { join, dirname } from 'path';
|
|
27
|
+
import { homedir } from 'os';
|
|
28
|
+
import { createHash } from 'crypto';
|
|
29
|
+
import { Readable } from 'stream';
|
|
30
|
+
import { pipeline } from 'stream/promises';
|
|
31
|
+
// ── Paths ──────────────────────────────────────────────────────────────
|
|
32
|
+
/**
|
|
33
|
+
* Shared root for all dep installs. macOS-canonical app data directory.
|
|
34
|
+
* Per-platform shimming would go here for non-Mac if the SDK ever ships
|
|
35
|
+
* cross-platform.
|
|
36
|
+
*/
|
|
37
|
+
function sharedRoot() {
|
|
38
|
+
return join(homedir(), 'Library', 'Application Support', 'Clearly');
|
|
39
|
+
}
|
|
40
|
+
function binDir() {
|
|
41
|
+
return join(sharedRoot(), 'bin');
|
|
42
|
+
}
|
|
43
|
+
function modelsDir() {
|
|
44
|
+
return join(sharedRoot(), 'models');
|
|
45
|
+
}
|
|
46
|
+
function venvDir() {
|
|
47
|
+
return join(sharedRoot(), 'venv');
|
|
48
|
+
}
|
|
49
|
+
/** Resolve a `~/Library/...` style path to an absolute path. */
|
|
50
|
+
function expandTilde(p) {
|
|
51
|
+
if (p.startsWith('~/'))
|
|
52
|
+
return join(homedir(), p.slice(2));
|
|
53
|
+
return p;
|
|
54
|
+
}
|
|
55
|
+
// ── Public API ─────────────────────────────────────────────────────────
|
|
56
|
+
/**
|
|
57
|
+
* Install a dependency. Returns the install path on success.
|
|
58
|
+
* Throws on failure (caller catches + reports back to the worker).
|
|
59
|
+
*/
|
|
60
|
+
export async function installDep(spec, onProgress) {
|
|
61
|
+
mkdirSync(sharedRoot(), { recursive: true });
|
|
62
|
+
switch (spec.installMethod) {
|
|
63
|
+
case 'binary-download':
|
|
64
|
+
return installBinary(spec, onProgress);
|
|
65
|
+
case 'huggingface-model':
|
|
66
|
+
return installHuggingfaceModel(spec, onProgress);
|
|
67
|
+
case 'pip-package':
|
|
68
|
+
return installPipPackage(spec, onProgress);
|
|
69
|
+
case 'npm-package':
|
|
70
|
+
// npm deps ship bundled with the app. This branch reports "installed"
|
|
71
|
+
// immediately — useful so the worker's dep state stays in sync with
|
|
72
|
+
// what's actually available at runtime.
|
|
73
|
+
onProgress(100, 'npm package — bundled at app pack time');
|
|
74
|
+
return '<bundled>';
|
|
75
|
+
default:
|
|
76
|
+
throw new Error(`unsupported installMethod: ${spec.installMethod}`);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Uninstall a dep — delete its install path. No-op if missing.
|
|
81
|
+
* Returns nothing; throws on permission errors.
|
|
82
|
+
*/
|
|
83
|
+
export async function uninstallDep(spec) {
|
|
84
|
+
const path = resolveInstallPath(spec);
|
|
85
|
+
if (!path)
|
|
86
|
+
return;
|
|
87
|
+
if (!existsSync(path))
|
|
88
|
+
return;
|
|
89
|
+
try {
|
|
90
|
+
rmSync(path, { recursive: true, force: true });
|
|
91
|
+
}
|
|
92
|
+
catch (e) {
|
|
93
|
+
throw new Error(`uninstall-failed: ${e?.message ?? e}`);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
/** Resolve the install path the dep would land at, from its spec. */
|
|
97
|
+
export function resolveInstallPath(spec) {
|
|
98
|
+
const args = spec.installArgs ?? {};
|
|
99
|
+
// Caller-supplied installPath wins.
|
|
100
|
+
if (typeof args.installPath === 'string')
|
|
101
|
+
return expandTilde(args.installPath);
|
|
102
|
+
// Per-method defaults.
|
|
103
|
+
if (spec.installMethod === 'binary-download')
|
|
104
|
+
return join(binDir(), spec.id);
|
|
105
|
+
if (spec.installMethod === 'huggingface-model') {
|
|
106
|
+
const variant = args.variant || spec.id;
|
|
107
|
+
return join(modelsDir(), variant);
|
|
108
|
+
}
|
|
109
|
+
if (spec.installMethod === 'pip-package')
|
|
110
|
+
return venvDir();
|
|
111
|
+
return null;
|
|
112
|
+
}
|
|
113
|
+
// ── Binary download ────────────────────────────────────────────────────
|
|
114
|
+
async function installBinary(spec, onProgress) {
|
|
115
|
+
const args = spec.installArgs ?? {};
|
|
116
|
+
let url = args.downloadUrl;
|
|
117
|
+
// installArgs.downloadUrl can be a string OR a per-arch map like
|
|
118
|
+
// { 'apple-silicon-m1+': '…', 'intel-x86_64': '…' }
|
|
119
|
+
if (typeof url === 'object' && url !== null) {
|
|
120
|
+
const arch = process.arch === 'arm64' ? 'apple-silicon-m1+' : 'intel-x86_64';
|
|
121
|
+
url = url[arch];
|
|
122
|
+
}
|
|
123
|
+
if (typeof url !== 'string') {
|
|
124
|
+
throw new Error(`binary-download: installArgs.downloadUrl missing or not a string for arch=${process.arch}`);
|
|
125
|
+
}
|
|
126
|
+
const installPath = resolveInstallPath(spec);
|
|
127
|
+
mkdirSync(dirname(installPath), { recursive: true });
|
|
128
|
+
const tmpPath = `${installPath}.partial`;
|
|
129
|
+
onProgress(0, `Downloading ${spec.id}…`);
|
|
130
|
+
await downloadToFile(url, tmpPath, spec.downloadBytes, onProgress);
|
|
131
|
+
// Optional SHA256 check
|
|
132
|
+
const expectedSha = args.sha256?.[process.arch === 'arm64' ? 'apple-silicon-m1+' : 'intel-x86_64'];
|
|
133
|
+
if (expectedSha) {
|
|
134
|
+
onProgress(95, 'Verifying SHA256…');
|
|
135
|
+
const actual = await fileSha256(tmpPath);
|
|
136
|
+
if (actual !== expectedSha) {
|
|
137
|
+
try {
|
|
138
|
+
unlinkSync(tmpPath);
|
|
139
|
+
}
|
|
140
|
+
catch { }
|
|
141
|
+
throw new Error(`sha256-mismatch: expected ${expectedSha}, got ${actual}`);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
// Atomic rename
|
|
145
|
+
try {
|
|
146
|
+
unlinkSync(installPath);
|
|
147
|
+
}
|
|
148
|
+
catch { } // best-effort, in case existed
|
|
149
|
+
await import('fs').then((fs) => fs.promises.rename(tmpPath, installPath));
|
|
150
|
+
if (args.executable !== false) {
|
|
151
|
+
chmodSync(installPath, 0o755);
|
|
152
|
+
}
|
|
153
|
+
onProgress(100, 'Installed');
|
|
154
|
+
return installPath;
|
|
155
|
+
}
|
|
156
|
+
// ── HuggingFace model ──────────────────────────────────────────────────
|
|
157
|
+
async function installHuggingfaceModel(spec, onProgress) {
|
|
158
|
+
const args = spec.installArgs ?? {};
|
|
159
|
+
const repo = args.repo;
|
|
160
|
+
const variant = args.variant;
|
|
161
|
+
const files = Array.isArray(args.files) ? args.files : [];
|
|
162
|
+
if (!repo)
|
|
163
|
+
throw new Error('huggingface-model: installArgs.repo required');
|
|
164
|
+
if (files.length === 0)
|
|
165
|
+
throw new Error('huggingface-model: installArgs.files (string[]) required');
|
|
166
|
+
const installPath = resolveInstallPath(spec);
|
|
167
|
+
mkdirSync(installPath, { recursive: true });
|
|
168
|
+
const totalBytes = spec.downloadBytes ?? 0;
|
|
169
|
+
let downloadedBytes = 0;
|
|
170
|
+
for (let i = 0; i < files.length; i++) {
|
|
171
|
+
const file = files[i];
|
|
172
|
+
const url = `https://huggingface.co/${repo}/resolve/main/${file}` +
|
|
173
|
+
(variant ? `?variant=${encodeURIComponent(variant)}` : '');
|
|
174
|
+
const dest = join(installPath, file);
|
|
175
|
+
mkdirSync(dirname(dest), { recursive: true });
|
|
176
|
+
const tmp = `${dest}.partial`;
|
|
177
|
+
onProgress(Math.round((downloadedBytes / Math.max(1, totalBytes)) * 100), `Downloading ${file} (${i + 1}/${files.length})…`);
|
|
178
|
+
await downloadToFile(url, tmp, undefined, (filePct, fileMsg) => {
|
|
179
|
+
// filePct is 0..100 for this single file; project onto overall %.
|
|
180
|
+
const fileShare = totalBytes > 0 ? spec.downloadBytes / files.length : 0;
|
|
181
|
+
const projected = ((downloadedBytes + (filePct / 100) * fileShare) / Math.max(1, totalBytes)) * 100;
|
|
182
|
+
onProgress(Math.min(99, Math.round(projected)), fileMsg);
|
|
183
|
+
});
|
|
184
|
+
try {
|
|
185
|
+
unlinkSync(dest);
|
|
186
|
+
}
|
|
187
|
+
catch { }
|
|
188
|
+
await import('fs').then((fs) => fs.promises.rename(tmp, dest));
|
|
189
|
+
try {
|
|
190
|
+
downloadedBytes += statSync(dest).size;
|
|
191
|
+
}
|
|
192
|
+
catch { }
|
|
193
|
+
}
|
|
194
|
+
onProgress(100, 'Installed');
|
|
195
|
+
return installPath;
|
|
196
|
+
}
|
|
197
|
+
// ── Pip package ────────────────────────────────────────────────────────
|
|
198
|
+
async function installPipPackage(spec, onProgress) {
|
|
199
|
+
const args = spec.installArgs ?? {};
|
|
200
|
+
const pkgName = args.package;
|
|
201
|
+
if (!pkgName)
|
|
202
|
+
throw new Error('pip-package: installArgs.package (string) required');
|
|
203
|
+
// Shared venv path. Caller should have created it via the hive's setup.sh
|
|
204
|
+
// before triggering installs. If it doesn't exist, abort with guidance.
|
|
205
|
+
const pythonBin = join(venvDir(), 'bin', 'python');
|
|
206
|
+
if (!existsSync(pythonBin)) {
|
|
207
|
+
throw new Error(`pip-package: shared venv missing at ${venvDir()}. ` +
|
|
208
|
+
`Run \`beehaven app setup <hive>\` first to bootstrap it.`);
|
|
209
|
+
}
|
|
210
|
+
onProgress(0, `pip install ${pkgName}…`);
|
|
211
|
+
const { spawn } = await import('child_process');
|
|
212
|
+
await new Promise((resolve, reject) => {
|
|
213
|
+
const proc = spawn(pythonBin, ['-m', 'pip', 'install', pkgName + (args.version ? `==${args.version}` : '')], {
|
|
214
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
215
|
+
});
|
|
216
|
+
let stderr = '';
|
|
217
|
+
proc.stderr?.on('data', (chunk) => {
|
|
218
|
+
stderr += chunk.toString();
|
|
219
|
+
});
|
|
220
|
+
proc.on('close', (code) => {
|
|
221
|
+
if (code === 0)
|
|
222
|
+
resolve();
|
|
223
|
+
else
|
|
224
|
+
reject(new Error(`pip-install exit=${code}: ${stderr.slice(0, 500)}`));
|
|
225
|
+
});
|
|
226
|
+
proc.on('error', reject);
|
|
227
|
+
});
|
|
228
|
+
onProgress(100, 'Installed');
|
|
229
|
+
return venvDir();
|
|
230
|
+
}
|
|
231
|
+
// ── Helpers ────────────────────────────────────────────────────────────
|
|
232
|
+
/**
|
|
233
|
+
* Stream a URL to a file with progress reporting. Throws on non-2xx
|
|
234
|
+
* status, network errors, or partial writes. Uses node:fs streams +
|
|
235
|
+
* stream/promises pipeline for backpressure-safe writes.
|
|
236
|
+
*/
|
|
237
|
+
async function downloadToFile(url, destPath, expectedBytes, onProgress) {
|
|
238
|
+
const res = await fetch(url);
|
|
239
|
+
if (!res.ok)
|
|
240
|
+
throw new Error(`download-failed: HTTP ${res.status} ${res.statusText} for ${url}`);
|
|
241
|
+
if (!res.body)
|
|
242
|
+
throw new Error('download-failed: no response body');
|
|
243
|
+
// Prefer the Content-Length header for progress accuracy.
|
|
244
|
+
const totalBytes = expectedBytes ?? (Number(res.headers.get('content-length')) || 0);
|
|
245
|
+
let received = 0;
|
|
246
|
+
let lastReport = 0;
|
|
247
|
+
// Wrap the web stream in a transform that ticks the progress callback.
|
|
248
|
+
const reader = res.body.getReader();
|
|
249
|
+
const file = createWriteStream(destPath);
|
|
250
|
+
const nodeStream = new Readable({
|
|
251
|
+
async read() {
|
|
252
|
+
try {
|
|
253
|
+
const { done, value } = await reader.read();
|
|
254
|
+
if (done) {
|
|
255
|
+
this.push(null);
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
258
|
+
received += value.byteLength;
|
|
259
|
+
const now = Date.now();
|
|
260
|
+
if (now - lastReport > 500) {
|
|
261
|
+
lastReport = now;
|
|
262
|
+
const pct = totalBytes > 0 ? Math.min(99, Math.round((received / totalBytes) * 100)) : 0;
|
|
263
|
+
onProgress(pct, totalBytes > 0
|
|
264
|
+
? `${(received / 1_000_000).toFixed(1)} MB / ${(totalBytes / 1_000_000).toFixed(1)} MB`
|
|
265
|
+
: `${(received / 1_000_000).toFixed(1)} MB`);
|
|
266
|
+
}
|
|
267
|
+
this.push(Buffer.from(value));
|
|
268
|
+
}
|
|
269
|
+
catch (e) {
|
|
270
|
+
this.destroy(e);
|
|
271
|
+
}
|
|
272
|
+
},
|
|
273
|
+
});
|
|
274
|
+
await pipeline(nodeStream, file);
|
|
275
|
+
}
|
|
276
|
+
async function fileSha256(path) {
|
|
277
|
+
const hash = createHash('sha256');
|
|
278
|
+
const fs = await import('fs');
|
|
279
|
+
await new Promise((resolve, reject) => {
|
|
280
|
+
const s = fs.createReadStream(path);
|
|
281
|
+
s.on('data', (chunk) => hash.update(chunk));
|
|
282
|
+
s.on('end', resolve);
|
|
283
|
+
s.on('error', reject);
|
|
284
|
+
});
|
|
285
|
+
return hash.digest('hex');
|
|
286
|
+
}
|
package/dist/dev-mode.js
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DEV MODE — the agent is working ON Clearly, not just in it.
|
|
3
|
+
*
|
|
4
|
+
* ON by default. Turn it off for a session with `CLEARLY_DEV_MODE=0`.
|
|
5
|
+
*
|
|
6
|
+
* WHY IT EXISTS. An agent that hits a broken verb and quietly finds another way is
|
|
7
|
+
* doing its job and destroying the only evidence we get. Every real defect found in
|
|
8
|
+
* the agent stack so far was found by a HUMAN reading code, never reported by the
|
|
9
|
+
* agent that hit it: the pi room ran every turn as Claude for its whole life;
|
|
10
|
+
* `canvas.create-node type:"image"` answered "unknown type" so agents shipped rects
|
|
11
|
+
* standing in for pictures; the system prompt taught a `style:{}` node shape that
|
|
12
|
+
* silently ate every text body it was given. An agent walked into all three and said
|
|
13
|
+
* nothing, because nothing ever asked it to.
|
|
14
|
+
*
|
|
15
|
+
* THE TWIN. apps/electron/electron/treehouse-agents.ts carries the same instruction
|
|
16
|
+
* for the maid chat's Claude, and it must stay in sync BY HAND. That is not laziness:
|
|
17
|
+
* the desktop app does not depend on this package, it COPIES the built dist into
|
|
18
|
+
* Resources/beehaven at package time (electron-builder-dev.yml), so there is no
|
|
19
|
+
* import either direction. Two processes, two builds, one instruction — keep them
|
|
20
|
+
* matching, and if you change one, change the other.
|
|
21
|
+
*
|
|
22
|
+
* There are genuinely TWO Claudes and this is the honest reason both need it:
|
|
23
|
+
* • the maid chat's — electron main, treehouse-agents.ts ClaudeSession
|
|
24
|
+
* • the daemon's — `beehaven claude "<task>"`, daemon-omni.ts promptOmni
|
|
25
|
+
* They are separate Claude Agent SDK integrations in separate processes. Unifying
|
|
26
|
+
* them is a real refactor; giving them the same instruction is not.
|
|
27
|
+
*/
|
|
28
|
+
/** Is dev mode on? Default YES; `CLEARLY_DEV_MODE=0` (or `false`) turns it off. */
|
|
29
|
+
export function devModeEnabled() {
|
|
30
|
+
const v = process.env.CLEARLY_DEV_MODE;
|
|
31
|
+
return v !== '0' && v !== 'false';
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* The instruction itself. Kept byte-identical to the electron twin so the two
|
|
35
|
+
* Claudes behave the same — diff them if you touch either.
|
|
36
|
+
*/
|
|
37
|
+
export const DEV_MODE_INSTRUCTION = [
|
|
38
|
+
`# Dev mode`,
|
|
39
|
+
``,
|
|
40
|
+
`You are working ON Clearly, not just in it. You are the only one who sees this system from the outside, while using it, and that view disappears the moment you route around a problem.`,
|
|
41
|
+
``,
|
|
42
|
+
`So: when something fights you, REPORT IT rather than quietly finding another way. Specifically —`,
|
|
43
|
+
`- a verb/arg that doesn't exist, or exists and is ignored`,
|
|
44
|
+
`- a doc, skill or tool description that says something untrue (the shape, the arg name, the default)`,
|
|
45
|
+
`- a call that "succeeds" but doesn't do what it said`,
|
|
46
|
+
`- a step you only got right by guessing, or by reading source you shouldn't have needed`,
|
|
47
|
+
`- anything you had to do twice because the first, obvious way failed`,
|
|
48
|
+
``,
|
|
49
|
+
`Report it with the \`dev-report\` action — one call per issue:`,
|
|
50
|
+
` beehaven call dev-report '{"kind":"bug","title":"…","tried":"the exact call","got":"what happened","expected":"what you expected","file":"path/if/known.ts"}'`,
|
|
51
|
+
`\`kind\` is "bug" (it lies or loses work) or "request" (it's merely missing). Each report lands as a card on the "claude dev mode" board; \`beehaven call dev-report '{"list":true}'\` shows what's already filed, so check before duplicating.`,
|
|
52
|
+
``,
|
|
53
|
+
`If \`dev-report\` comes back unknown/unavailable, the verb is not deployed on this workspace yet — do NOT drop the report: put the same fields on a canvas composition called "claude dev mode" (one text node per issue, TRIED / GOT / EXPECTED / file) with canvas-act, and say in your reply that dev-report was unreachable.`,
|
|
54
|
+
`Before you file: PROVE IT. Re-run the exact call in isolation and confirm the failure is the tool's, not your first attempt's. Two of the first twelve reports filed this way were wrong — a "missing" feature that existed and was merely undocumented, and a "broken" output that was fine once read properly. A false report costs more than silence: someone fixes what was never broken. If you cannot reproduce it, file it as a DOC gap ("I could not find how to X"), which is true, instead of a BUG ("X is broken"), which may not be.`,
|
|
55
|
+
``,
|
|
56
|
+
`A working workaround is still a bug report: note that you found one. Do not soften this into "some users might" — you hit it; say so plainly, once, with the evidence. And don't stop the task to report: finish what was asked, then file.`,
|
|
57
|
+
].join('\n');
|
|
58
|
+
/**
|
|
59
|
+
* `appendSystemPrompt` for a daemon Claude turn: the caller's own append (if any)
|
|
60
|
+
* plus dev mode. Returns undefined when there is nothing to append, so callers can
|
|
61
|
+
* spread it without sending an empty string.
|
|
62
|
+
*/
|
|
63
|
+
export function withDevMode(append) {
|
|
64
|
+
if (!devModeEnabled())
|
|
65
|
+
return append || undefined;
|
|
66
|
+
return append ? `${append}\n\n---\n\n${DEV_MODE_INSTRUCTION}` : DEV_MODE_INSTRUCTION;
|
|
67
|
+
}
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DocIndexer — indexes documentation from linked projects for cloud agent context.
|
|
3
|
+
*
|
|
4
|
+
* Reads the `docs` field from beehaven.json, chunks files into digestible
|
|
5
|
+
* summaries, and sends them to the cloud DO via the `remember` action.
|
|
6
|
+
* Re-indexes on file change via chokidar.
|
|
7
|
+
*
|
|
8
|
+
* Keys are namespaced: `doc:<project>:<filepath>` so they don't collide
|
|
9
|
+
* with user memories and can be bulk-cleared on unlink.
|
|
10
|
+
*/
|
|
11
|
+
import { existsSync, readFileSync, readdirSync, statSync } from 'fs';
|
|
12
|
+
import { join, relative, extname } from 'path';
|
|
13
|
+
import { createHash } from 'crypto';
|
|
14
|
+
import { EventEmitter } from 'events';
|
|
15
|
+
/** Max size per doc chunk sent to the DO (characters) */
|
|
16
|
+
const MAX_CHUNK_SIZE = 4000;
|
|
17
|
+
/** File extensions to index */
|
|
18
|
+
const INDEXABLE_EXTENSIONS = new Set([
|
|
19
|
+
'.md',
|
|
20
|
+
'.txt',
|
|
21
|
+
'.rst',
|
|
22
|
+
'.adoc',
|
|
23
|
+
'.org',
|
|
24
|
+
'.ts',
|
|
25
|
+
'.js',
|
|
26
|
+
'.py',
|
|
27
|
+
'.rs',
|
|
28
|
+
'.go',
|
|
29
|
+
'.java',
|
|
30
|
+
'.rb',
|
|
31
|
+
'.sh',
|
|
32
|
+
'.json',
|
|
33
|
+
'.yaml',
|
|
34
|
+
'.yml',
|
|
35
|
+
'.toml',
|
|
36
|
+
'.html',
|
|
37
|
+
'.css',
|
|
38
|
+
'.sql',
|
|
39
|
+
]);
|
|
40
|
+
export class DocIndexer extends EventEmitter {
|
|
41
|
+
projectDir;
|
|
42
|
+
projectName;
|
|
43
|
+
docPaths;
|
|
44
|
+
callAction;
|
|
45
|
+
hashes = new Map();
|
|
46
|
+
constructor(opts) {
|
|
47
|
+
super();
|
|
48
|
+
this.projectDir = opts.projectDir;
|
|
49
|
+
this.projectName = opts.projectName;
|
|
50
|
+
this.docPaths = opts.docPaths;
|
|
51
|
+
this.callAction = opts.callAction;
|
|
52
|
+
}
|
|
53
|
+
/** Run a full index pass. Returns count of documents indexed. */
|
|
54
|
+
async index() {
|
|
55
|
+
const files = this.resolveFiles();
|
|
56
|
+
let indexed = 0;
|
|
57
|
+
for (const filePath of files) {
|
|
58
|
+
try {
|
|
59
|
+
const abs = join(this.projectDir, filePath);
|
|
60
|
+
if (!existsSync(abs))
|
|
61
|
+
continue;
|
|
62
|
+
const content = readFileSync(abs, 'utf8');
|
|
63
|
+
const hash = createHash('md5').update(content).digest('hex');
|
|
64
|
+
// Skip if unchanged
|
|
65
|
+
if (this.hashes.get(filePath) === hash)
|
|
66
|
+
continue;
|
|
67
|
+
this.hashes.set(filePath, hash);
|
|
68
|
+
// Chunk large files
|
|
69
|
+
const chunks = this.chunk(content, filePath);
|
|
70
|
+
for (let i = 0; i < chunks.length; i++) {
|
|
71
|
+
const key = chunks.length === 1
|
|
72
|
+
? `doc:${this.projectName}:${filePath}`
|
|
73
|
+
: `doc:${this.projectName}:${filePath}:${i + 1}`;
|
|
74
|
+
await this.callAction('remember', {
|
|
75
|
+
key,
|
|
76
|
+
value: chunks[i],
|
|
77
|
+
category: 'documentation',
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
indexed++;
|
|
81
|
+
}
|
|
82
|
+
catch (err) {
|
|
83
|
+
this.emit('error', `Failed to index ${filePath}: ${err.message}`);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return indexed;
|
|
87
|
+
}
|
|
88
|
+
/** Index a single file (for watch-triggered updates) */
|
|
89
|
+
async indexFile(filePath) {
|
|
90
|
+
const abs = join(this.projectDir, filePath);
|
|
91
|
+
if (!existsSync(abs))
|
|
92
|
+
return;
|
|
93
|
+
const content = readFileSync(abs, 'utf8');
|
|
94
|
+
const hash = createHash('md5').update(content).digest('hex');
|
|
95
|
+
if (this.hashes.get(filePath) === hash)
|
|
96
|
+
return;
|
|
97
|
+
this.hashes.set(filePath, hash);
|
|
98
|
+
const chunks = this.chunk(content, filePath);
|
|
99
|
+
for (let i = 0; i < chunks.length; i++) {
|
|
100
|
+
const key = chunks.length === 1
|
|
101
|
+
? `doc:${this.projectName}:${filePath}`
|
|
102
|
+
: `doc:${this.projectName}:${filePath}:${i + 1}`;
|
|
103
|
+
await this.callAction('remember', { key, value: chunks[i], category: 'documentation' });
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
/** Resolve glob-like doc paths to actual file paths */
|
|
107
|
+
resolveFiles() {
|
|
108
|
+
const files = [];
|
|
109
|
+
for (const docPath of this.docPaths) {
|
|
110
|
+
const abs = join(this.projectDir, docPath);
|
|
111
|
+
if (!existsSync(abs))
|
|
112
|
+
continue;
|
|
113
|
+
const stat = statSync(abs);
|
|
114
|
+
if (stat.isDirectory()) {
|
|
115
|
+
// Recursively find indexable files
|
|
116
|
+
this.walkDir(abs, files);
|
|
117
|
+
}
|
|
118
|
+
else if (stat.isFile() && this.isIndexable(abs)) {
|
|
119
|
+
files.push(relative(this.projectDir, abs));
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
return files;
|
|
123
|
+
}
|
|
124
|
+
walkDir(dir, files, depth = 0) {
|
|
125
|
+
if (depth > 5)
|
|
126
|
+
return; // Prevent deep recursion
|
|
127
|
+
try {
|
|
128
|
+
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
129
|
+
if (entry.name.startsWith('.') || entry.name === 'node_modules')
|
|
130
|
+
continue;
|
|
131
|
+
const full = join(dir, entry.name);
|
|
132
|
+
if (entry.isDirectory()) {
|
|
133
|
+
this.walkDir(full, files, depth + 1);
|
|
134
|
+
}
|
|
135
|
+
else if (this.isIndexable(full)) {
|
|
136
|
+
files.push(relative(this.projectDir, full));
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
catch {
|
|
141
|
+
/* permission errors, etc. */
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
isIndexable(filePath) {
|
|
145
|
+
return INDEXABLE_EXTENSIONS.has(extname(filePath).toLowerCase());
|
|
146
|
+
}
|
|
147
|
+
/** Chunk content into pieces that fit in agent memory */
|
|
148
|
+
chunk(content, filePath) {
|
|
149
|
+
const header = `[${this.projectName}] ${filePath}\n---\n`;
|
|
150
|
+
if (content.length + header.length <= MAX_CHUNK_SIZE) {
|
|
151
|
+
return [header + content];
|
|
152
|
+
}
|
|
153
|
+
// Split by sections (## headers in markdown, or every N chars)
|
|
154
|
+
const sections = content.split(/(?=^##\s)/m);
|
|
155
|
+
const chunks = [];
|
|
156
|
+
let current = header;
|
|
157
|
+
for (const section of sections) {
|
|
158
|
+
if (current.length + section.length > MAX_CHUNK_SIZE && current.length > header.length) {
|
|
159
|
+
chunks.push(current.trim());
|
|
160
|
+
current = header;
|
|
161
|
+
}
|
|
162
|
+
current += section;
|
|
163
|
+
}
|
|
164
|
+
if (current.length > header.length) {
|
|
165
|
+
chunks.push(current.trim());
|
|
166
|
+
}
|
|
167
|
+
return chunks.length > 0 ? chunks : [header + content.slice(0, MAX_CHUNK_SIZE - header.length)];
|
|
168
|
+
}
|
|
169
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
// ============================================================================
|
|
2
|
+
// Beehaven Daemon — End-to-End Encryption
|
|
3
|
+
// ECDH key exchange + AES-256-GCM payload encryption.
|
|
4
|
+
// The Cloudflare relay Worker sees only opaque ciphertext.
|
|
5
|
+
// ============================================================================
|
|
6
|
+
import { createECDH, createCipheriv, createDecipheriv, createHash, randomBytes } from 'crypto';
|
|
7
|
+
export class E2ECrypto {
|
|
8
|
+
ecdh;
|
|
9
|
+
sharedSecret = null;
|
|
10
|
+
_ready = false;
|
|
11
|
+
constructor() {
|
|
12
|
+
this.ecdh = createECDH('prime256v1');
|
|
13
|
+
this.ecdh.generateKeys();
|
|
14
|
+
}
|
|
15
|
+
/** Get our public key (base64) to send in handshake */
|
|
16
|
+
getPublicKey() {
|
|
17
|
+
return this.ecdh.getPublicKey('base64');
|
|
18
|
+
}
|
|
19
|
+
/** Derive shared secret from the peer's public key */
|
|
20
|
+
deriveSecret(peerPublicKeyBase64) {
|
|
21
|
+
const peerKey = Buffer.from(peerPublicKeyBase64, 'base64');
|
|
22
|
+
const shared = this.ecdh.computeSecret(peerKey);
|
|
23
|
+
// SHA-256 of the raw ECDH secret -> 32-byte AES key
|
|
24
|
+
this.sharedSecret = createHash('sha256').update(shared).digest();
|
|
25
|
+
this._ready = true;
|
|
26
|
+
}
|
|
27
|
+
/** Is the E2E channel established? */
|
|
28
|
+
get ready() {
|
|
29
|
+
return this._ready;
|
|
30
|
+
}
|
|
31
|
+
/** Encrypt a message object -> { _e2e: true, iv, ct } */
|
|
32
|
+
encrypt(message) {
|
|
33
|
+
if (!this.sharedSecret)
|
|
34
|
+
throw new Error('E2E not established — call deriveSecret first');
|
|
35
|
+
const iv = randomBytes(12); // 96-bit nonce for GCM
|
|
36
|
+
const cipher = createCipheriv('aes-256-gcm', this.sharedSecret, iv);
|
|
37
|
+
const plaintext = JSON.stringify(message);
|
|
38
|
+
const encrypted = Buffer.concat([cipher.update(plaintext, 'utf8'), cipher.final()]);
|
|
39
|
+
const tag = cipher.getAuthTag(); // 16-byte authentication tag
|
|
40
|
+
return {
|
|
41
|
+
_e2e: true,
|
|
42
|
+
iv: iv.toString('base64'),
|
|
43
|
+
ct: Buffer.concat([encrypted, tag]).toString('base64'), // ciphertext || authTag
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
/** Decrypt a { _e2e, iv, ct } envelope -> original message object */
|
|
47
|
+
decrypt(envelope) {
|
|
48
|
+
if (!this.sharedSecret)
|
|
49
|
+
throw new Error('E2E not established');
|
|
50
|
+
const iv = Buffer.from(envelope.iv, 'base64');
|
|
51
|
+
const data = Buffer.from(envelope.ct, 'base64');
|
|
52
|
+
// Last 16 bytes are the GCM auth tag
|
|
53
|
+
const tag = data.subarray(data.length - 16);
|
|
54
|
+
const ciphertext = data.subarray(0, data.length - 16);
|
|
55
|
+
const decipher = createDecipheriv('aes-256-gcm', this.sharedSecret, iv);
|
|
56
|
+
decipher.setAuthTag(tag);
|
|
57
|
+
const decrypted = Buffer.concat([decipher.update(ciphertext), decipher.final()]);
|
|
58
|
+
return JSON.parse(decrypted.toString('utf8'));
|
|
59
|
+
}
|
|
60
|
+
/** Reset state for a fresh connection (new ephemeral keypair) */
|
|
61
|
+
reset() {
|
|
62
|
+
this.ecdh = createECDH('prime256v1');
|
|
63
|
+
this.ecdh.generateKeys();
|
|
64
|
+
this.sharedSecret = null;
|
|
65
|
+
this._ready = false;
|
|
66
|
+
}
|
|
67
|
+
/** Check if a message is an E2E encrypted envelope */
|
|
68
|
+
static isEncrypted(msg) {
|
|
69
|
+
return msg && msg._e2e === true && typeof msg.iv === 'string' && typeof msg.ct === 'string';
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
// ── Message Classification ──────────────────────────────────────────────────
|
|
73
|
+
/**
|
|
74
|
+
* Message types that carry sensitive data and MUST be encrypted.
|
|
75
|
+
* Handshake/routing messages stay plaintext so the relay can route them.
|
|
76
|
+
*/
|
|
77
|
+
const SENSITIVE_TYPES = new Set([
|
|
78
|
+
'daemon:fs:response', // file contents
|
|
79
|
+
'agent-tool-exec-result', // code, file contents from tool execution
|
|
80
|
+
'local-agent:response', // agent output
|
|
81
|
+
'local-agent:chunk', // streaming agent output
|
|
82
|
+
'local-agent:step', // agent step details
|
|
83
|
+
'raw-event', // may contain code snippets
|
|
84
|
+
'file-teleport-delivery', // file contents (inbound)
|
|
85
|
+
'file-teleport-ack', // file paths + content confirmations
|
|
86
|
+
'telemetry', // system metrics, git info
|
|
87
|
+
'session-events', // session activity
|
|
88
|
+
'pty-output', // terminal output
|
|
89
|
+
'pty-exit', // terminal exit (may leak info via exit codes)
|
|
90
|
+
]);
|
|
91
|
+
/** Returns true if this message type should be encrypted when E2E is available */
|
|
92
|
+
export function shouldEncrypt(msgType) {
|
|
93
|
+
return SENSITIVE_TYPES.has(msgType);
|
|
94
|
+
}
|