@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.
Files changed (171) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +201 -0
  3. package/dist/__tests__/_cli-switch.js +105 -0
  4. package/dist/__tests__/agent-wrap-probe.js +136 -0
  5. package/dist/agent-attribution.js +192 -0
  6. package/dist/agent-identity-daemon.js +135 -0
  7. package/dist/agent-onboard.js +156 -0
  8. package/dist/agent-scope.js +79 -0
  9. package/dist/agent-sessions.js +370 -0
  10. package/dist/agent-state.js +482 -0
  11. package/dist/app-manifest.js +221 -0
  12. package/dist/app-signer.js +54 -0
  13. package/dist/app-signing.js +127 -0
  14. package/dist/audio-play.js +98 -0
  15. package/dist/avatar-protocol.js +25 -0
  16. package/dist/avatar-route.js +79 -0
  17. package/dist/bee/cloud-proxy.js +130 -0
  18. package/dist/blocked-actions.js +120 -0
  19. package/dist/build-channel.js +101 -0
  20. package/dist/buildings/daemon-workspace.js +475 -0
  21. package/dist/claude-bridge.js +310 -0
  22. package/dist/claude-credential-store.js +633 -0
  23. package/dist/clearly-sync.js +518 -0
  24. package/dist/clearly.js +616 -0
  25. package/dist/cli-state.js +334 -0
  26. package/dist/cli.js +6973 -0
  27. package/dist/commands/agent-skills.js +302 -0
  28. package/dist/commands/agent.js +1310 -0
  29. package/dist/commands/app.js +1123 -0
  30. package/dist/commands/auth.js +171 -0
  31. package/dist/commands/building-shell.js +392 -0
  32. package/dist/commands/canvas.js +932 -0
  33. package/dist/commands/claude.js +218 -0
  34. package/dist/commands/compliance.js +387 -0
  35. package/dist/commands/compose.js +507 -0
  36. package/dist/commands/composition.js +95 -0
  37. package/dist/commands/connect-session.js +409 -0
  38. package/dist/commands/conversations.js +160 -0
  39. package/dist/commands/costs.js +443 -0
  40. package/dist/commands/d1.js +253 -0
  41. package/dist/commands/daemon-cmd.js +162 -0
  42. package/dist/commands/deploy.js +164 -0
  43. package/dist/commands/design.js +617 -0
  44. package/dist/commands/dev-generate.js +125 -0
  45. package/dist/commands/dev.js +109 -0
  46. package/dist/commands/devices.js +135 -0
  47. package/dist/commands/explain-router.js +24 -0
  48. package/dist/commands/explain.js +882 -0
  49. package/dist/commands/explore.js +264 -0
  50. package/dist/commands/help.js +2418 -0
  51. package/dist/commands/init.js +408 -0
  52. package/dist/commands/install.js +129 -0
  53. package/dist/commands/library.js +229 -0
  54. package/dist/commands/marketplace.js +138 -0
  55. package/dist/commands/mcp-install.js +476 -0
  56. package/dist/commands/mention.js +257 -0
  57. package/dist/commands/omni.js +529 -0
  58. package/dist/commands/secrets.js +323 -0
  59. package/dist/commands/self-update.js +206 -0
  60. package/dist/commands/sheet.js +264 -0
  61. package/dist/commands/shell-output.js +309 -0
  62. package/dist/commands/shell.js +1048 -0
  63. package/dist/commands/shop.js +368 -0
  64. package/dist/commands/skills.js +311 -0
  65. package/dist/commands/slides.js +418 -0
  66. package/dist/commands/swarm.js +341 -0
  67. package/dist/commands/ticket.js +1301 -0
  68. package/dist/commands/tool.js +312 -0
  69. package/dist/commands/trash.js +106 -0
  70. package/dist/commands/treehouse.js +120 -0
  71. package/dist/commands/user.js +240 -0
  72. package/dist/commands/voice-tts.js +319 -0
  73. package/dist/commands/voice.js +166 -0
  74. package/dist/commands/watch-git.js +401 -0
  75. package/dist/commands/webhook.js +159 -0
  76. package/dist/commands/workspace.js +3354 -0
  77. package/dist/composition-repos.js +91 -0
  78. package/dist/daemon-avatar.js +130 -0
  79. package/dist/daemon-claude-code-auto-install.js +52 -0
  80. package/dist/daemon-client.js +363 -0
  81. package/dist/daemon-codex.js +349 -0
  82. package/dist/daemon-entry.js +101 -0
  83. package/dist/daemon-fetch-allowlist.js +194 -0
  84. package/dist/daemon-gemini.js +224 -0
  85. package/dist/daemon-mcp-clearly.js +353 -0
  86. package/dist/daemon-mcp-sandbox.js +127 -0
  87. package/dist/daemon-omni-company.js +338 -0
  88. package/dist/daemon-omni-hooks.js +375 -0
  89. package/dist/daemon-omni-identity.js +229 -0
  90. package/dist/daemon-omni-pipeline.js +251 -0
  91. package/dist/daemon-omni-policies.js +346 -0
  92. package/dist/daemon-omni-principal.js +580 -0
  93. package/dist/daemon-omni-tool-gate.js +116 -0
  94. package/dist/daemon-omni.js +1442 -0
  95. package/dist/daemon-path-transform.js +120 -0
  96. package/dist/daemon-write-sandbox.js +191 -0
  97. package/dist/daemon.js +4471 -0
  98. package/dist/dashboard.js +846 -0
  99. package/dist/dep-installer.js +286 -0
  100. package/dist/dev-mode.js +67 -0
  101. package/dist/doc-indexer.js +169 -0
  102. package/dist/e2e-crypto.js +94 -0
  103. package/dist/feature-flags.js +154 -0
  104. package/dist/file-browser.js +88 -0
  105. package/dist/first-party-hives.js +211 -0
  106. package/dist/folder-sync.js +226 -0
  107. package/dist/health.js +56 -0
  108. package/dist/ipc-auth.js +299 -0
  109. package/dist/ipc-client.js +495 -0
  110. package/dist/ipc-server.js +684 -0
  111. package/dist/keychain.js +224 -0
  112. package/dist/lib/admin-client.js +73 -0
  113. package/dist/lib/mobile-build-parser.js +156 -0
  114. package/dist/local-app-manager.js +660 -0
  115. package/dist/log-redaction.js +89 -0
  116. package/dist/logger.js +108 -0
  117. package/dist/manifest.js +143 -0
  118. package/dist/marketplace.js +793 -0
  119. package/dist/min-version-copy.js +51 -0
  120. package/dist/min-version.js +214 -0
  121. package/dist/public-surface.js +23 -0
  122. package/dist/relay-presets.js +89 -0
  123. package/dist/relay.js +2723 -0
  124. package/dist/repl.js +619 -0
  125. package/dist/setup-relay.js +363 -0
  126. package/dist/setup-service.js +536 -0
  127. package/dist/shared/bee-memory/index.js +6 -0
  128. package/dist/shared/bee-memory/types.js +682 -0
  129. package/dist/shared/claude-code/daemon-types.js +112 -0
  130. package/dist/shared/claude-code/index.js +2 -0
  131. package/dist/shared/claude-code/types.js +1 -0
  132. package/dist/shared/status/schema.js +133 -0
  133. package/dist/shell-session.js +198 -0
  134. package/dist/speak-lock.js +97 -0
  135. package/dist/spinner.js +57 -0
  136. package/dist/telemetry.js +166 -0
  137. package/dist/terminal/block-parser.js +207 -0
  138. package/dist/terminal/shell-integration.js +108 -0
  139. package/dist/transcript-tailer.js +215 -0
  140. package/dist/tts-qwen3.js +547 -0
  141. package/dist/types.js +4 -0
  142. package/dist/utm.js +38 -0
  143. package/dist/verb-surface.js +331 -0
  144. package/dist/version-utils.js +85 -0
  145. package/dist/watcher.js +207 -0
  146. package/package.json +88 -0
  147. package/python/qwen3_tts.py +230 -0
  148. package/scripts/stamp-build-channel.mjs +115 -0
  149. package/scripts/sync-installed.mjs +159 -0
  150. package/scripts/sync-skills.mjs +112 -0
  151. package/skills/README.md +24 -0
  152. package/skills/agent-doodle-space/SKILL.md +118 -0
  153. package/skills/agent-home/SKILL.md +98 -0
  154. package/skills/beehaven-cli/SKILL.md +144 -0
  155. package/skills/claude-shopper/README.md +90 -0
  156. package/skills/claude-shopper/SKILL.md +151 -0
  157. package/skills/clearly-agent/SKILL.md +183 -0
  158. package/skills/clearly-canvas/SKILL.md +222 -0
  159. package/skills/clearly-canvas/authoring.md +160 -0
  160. package/skills/clearly-canvas/communications.md +32 -0
  161. package/skills/clearly-canvas/composition-patterns.md +84 -0
  162. package/skills/clearly-canvas/example.mjs +78 -0
  163. package/skills/clearly-canvas/project-plans.md +156 -0
  164. package/skills/clearly-canvas/review.mjs +68 -0
  165. package/skills/clearly-workspace/SKILL.md +79 -0
  166. package/skills/clearly-workspace/survey.mjs +101 -0
  167. package/skills/codebase-map/SKILL.md +142 -0
  168. package/skills/design-craft/SKILL.md +300 -0
  169. package/skills/index.json +79 -0
  170. package/skills/layout-systems/SKILL.md +184 -0
  171. 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
+ }
@@ -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
+ }