@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
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Clearly (clearly.sh)
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,201 @@
1
+ # beehaven
2
+
3
+ The command line for **Beehaven** — the Clearly cloud. Command your workspace,
4
+ agents, canvas and apps the way a terminal commands a filesystem, through the
5
+ local daemon that bridges your machine (and your AI agents) to it.
6
+
7
+ Beehaven is the name of the world Clearly runs in — the cloud, the hives, the
8
+ daemon, the bees. It isn't a separate product you buy; it's where Clearly lives,
9
+ and this is how you talk to it from a shell.
10
+
11
+ ## Requirements
12
+
13
+ - Node.js **>= 20** (the CLI is a Node program; the published archive includes its
14
+ runtime dependencies)
15
+
16
+ ## Install
17
+
18
+ For a one-off agent session with Node.js 20 or newer, run the npm package directly:
19
+
20
+ ```bash
21
+ npx --yes @clearlysh/beehaven@latest guide
22
+ ```
23
+
24
+ `guide` works before sign-in. It explains when Clearly is useful, gives the
25
+ human a signup link, and shows the authenticated first artifact. `npx` uses an
26
+ ephemeral npm install; it does not create a Clearly account or publish a file.
27
+ The package also provides the `beehaven` binary for a global npm install:
28
+
29
+ ```bash
30
+ npm install -g @clearlysh/beehaven@latest
31
+ beehaven guide
32
+ ```
33
+
34
+ The self-contained installer remains the recommended persistent install. It does
35
+ not require npm to resolve dependencies on the user's machine:
36
+
37
+ ```bash
38
+ curl -fsSL https://clearly.sh/install.sh | sh
39
+ ```
40
+
41
+ That fetches a versioned tarball and writes `beehaven` to `~/.local/bin`. Published
42
+ archives include their dependencies; older archives may require npm. Add the bin
43
+ directory to your `PATH` if the installer says it isn't there.
44
+
45
+ Verify:
46
+
47
+ ```bash
48
+ beehaven --version
49
+ beehaven doctor # environment + daemon + relay checks
50
+ ```
51
+
52
+ <details>
53
+ <summary>Working inside the monorepo instead?</summary>
54
+
55
+ ```bash
56
+ cd packages/cli && npm install && npm run build && npm link
57
+ ```
58
+
59
+ `npm link` may not win: the installer writes `~/.local/bin/beehaven`, and that
60
+ directory usually precedes the npm prefix on `PATH`. Check `which beehaven` and
61
+ `beehaven --version` before concluding a local change didn't take.
62
+ </details>
63
+
64
+ Both npm and the installer ship the public CLI build. The installer downloads
65
+ a versioned archive from `downloads.clearly.sh` and verifies the SHA-256 in its
66
+ update manifest. npm verifies package integrity through the registry.
67
+
68
+ ## Updates
69
+
70
+ Run `beehaven --version` to see both the version and the file being executed.
71
+ The CLI checks the published version on command startup at most once an hour and
72
+ prints a notice when it finds a newer version. This is a notice, not an interactive
73
+ prompt. A curl-installed CLI (`~/.beehaven/cli`) can be updated by a person or an
74
+ agent with `beehaven update`; it verifies the release archive's SHA-256 before
75
+ installing. No workspace agent login is needed for this local command.
76
+ An npm install updates with npm; an `npx` run can request `@latest` again.
77
+
78
+ When a curl-installed CLI owns a standalone daemon, that daemon checks at startup
79
+ and hourly, installs a verified update, and restarts itself. A daemon started by the
80
+ Mac app leaves updates to the app. Production and staging Mac apps check on launch
81
+ and every four hours, download in the background, and install on quit by default;
82
+ the tray also offers **Check for updates**. A linked development build is updated
83
+ by rebuilding its checkout. Clearly Dev.app also has no release updater; rebuild
84
+ and reinstall that app to update its bundled CLI.
85
+
86
+ ## Quick start
87
+
88
+ ```bash
89
+ beehaven login # authenticate (browser OAuth)
90
+ beehaven connect home # set the active target — your default workspace
91
+ beehaven agent login codex --label "my task" --client cli # identity + one work session + brief
92
+ beehaven actions # what can I call here?
93
+ beehaven call canvas-catalog '{}' # every canvas verb + its args
94
+ ```
95
+
96
+ Three commands cover almost everything: **connect** to a target, ask what
97
+ **actions** it has, **call** one. See `beehaven help` for the full surface, or
98
+ `beehaven help <command>` for any single verb.
99
+
100
+ ## Agent avatar expressions
101
+
102
+ The local avatar is addressable by the active agent identity. Use the short
103
+ flags for the primary expression surface:
104
+
105
+ beehaven --mood curious
106
+ beehaven --emote cheer "tests passed"
107
+
108
+ Persistent moods are: neutral, happy, sad, surprised, angry, confused, curious,
109
+ and flirty. Lifecycle values are `wake` and `sleep`, set with `--lifecycle`; thinking and
110
+ celebrating are activity poses. One-shot emotes are: wave, cheer, hearts, laugh, dance, buzz, nod,
111
+ shake, think, surprise, and sleep. An optional reason after an emote is capped
112
+ at 200 characters.
113
+
114
+ `--emote think` is the one-shot reaction. The compatibility command
115
+ `beehaven avatar think` is the persistent thinking state.
116
+
117
+ An active agent token routes the call to that agent's avatar; without one, the
118
+ house avatar is used. Compatibility commands remain available:
119
+
120
+ beehaven avatar emotion curious
121
+ beehaven avatar react cheer "tests passed"
122
+
123
+ The daemon MCP surface exposes avatar_emote for persistent emotion and
124
+ avatar_react for one-shot reactions. Both surfaces require the avatar:emote
125
+ permission for signed agents.
126
+
127
+ See [AVATAR_EXPRESSIONS.md](../../../docs/AVATAR_EXPRESSIONS.md) for the complete
128
+ cross-surface contract and source-of-truth locations.
129
+
130
+ ## Talking to the cloud — daemon + DO
131
+
132
+ The CLI talks to a local **daemon** that holds the relay connection to the
133
+ Beehaven cloud and routes RPCs to the right Durable Object.
134
+
135
+ ```bash
136
+ beehaven login # authenticate (browser OAuth) — required
137
+ beehaven status # connection + auth state
138
+ beehaven connect home # set the active target (default workspace alias)
139
+ beehaven actions # list callable actions on the target
140
+ beehaven call <action> '{…}' # invoke one
141
+ ```
142
+
143
+ For scripts that need more than one workspace action, the CLI also exposes the
144
+ server's scripting primitives without a second execution engine:
145
+
146
+ ```bash
147
+ beehaven batch @steps.json --json # {"steps":[{"action":"…","input":{}}]}
148
+ beehaven code-run @script.mjs --json # source file; use `-` for stdin
149
+ ```
150
+
151
+ `batch` runs up to 25 sequential steps (400 actions total) and is not
152
+ transactional; `stopOnError` defaults to true. `code-run` is a sandbox for
153
+ computation between workspace calls (200 calls, 20 seconds, 100k source
154
+ characters). Both adapters use the same active target, agent attribution,
155
+ permission checks and non-zero failure semantics as `beehaven call`.
156
+ `batch` also accepts the worker-native `{method,params}` spelling and normalizes
157
+ both forms before dispatch.
158
+
159
+ The daemon normally runs inside the Hive desktop app. For CI / servers / agents
160
+ with no desktop, run it headless:
161
+
162
+ ```bash
163
+ beehaven daemon start # spawn a headless daemon (requires a prior `beehaven login`)
164
+ beehaven daemon status
165
+ ```
166
+
167
+ ## Driving the canvas (agent surface)
168
+
169
+ The CLI can review and drive the Clearly editor canvas in an open browser tab —
170
+ the same actions a human performs in the UI, exposed to agents over the daemon.
171
+
172
+ ```bash
173
+ beehaven canvas status [id] # compact health summary (tab connected? nodes? tool?)
174
+ beehaven canvas perceive [id] # read the live "room": gaze, contents, focus, chrome
175
+ beehaven canvas actions # discover every callable editor action
176
+ beehaven canvas invoke <action> '{…}' # drive one action (returns the handler result)
177
+ beehaven canvas verify [--composition id] # self-test EVERY canvas action, then clean up after itself
178
+ ```
179
+
180
+ `canvas verify` creates scratch nodes, exercises each action against the live
181
+ tab, asserts the result, then deletes everything it created (it diffs against a
182
+ baseline snapshot, so it never touches pre-existing content) and restores print
183
+ settings. It is the smoke test for the agent/canvas surface.
184
+
185
+ ## Security
186
+
187
+ - **Authentication is required.** Every cloud call carries an authenticated
188
+ identity; `canvas-invoke` / `canvas-perceive` reject unauthenticated callers
189
+ and only reach the caller's _own_ connected clients (no cross-account access).
190
+ - **Allowlisted actions only.** Remote canvas control dispatches by action id
191
+ against a fixed registry — there is no arbitrary code execution path.
192
+ - **Local IPC is token-guarded.** The CLI authenticates to the daemon with a
193
+ per-launch token from `~/.beehaven/ipc-auth`.
194
+
195
+ ## Documentation
196
+
197
+ - Per-command help: `beehaven help <command>`
198
+
199
+ ## License
200
+
201
+ MIT © Clearly (clearly.sh)
@@ -0,0 +1,105 @@
1
+ /**
2
+ * THE ONE READING OF `cli.ts`'s DISPATCH SWITCH — shared by every guard that needs it.
3
+ *
4
+ * ⚠⚠ WHY IT IS SHARED RATHER THAN COPIED (CBH2-128, and CBH2-100 one file over). Two guards had
5
+ * their own parser: `verb-surface.test.ts` derived the verb list to check `VERB_SURFACE` covers
6
+ * it, and `limit-capable.test.ts` derived case BODIES to check `LIMIT_CAPABLE` is really wired.
7
+ * A second copy of "what the switch contains" drifts from the first, and the whole point of both
8
+ * guards is that a hand-kept list beside a dispatch goes stale. A guard that goes stale the same
9
+ * way is worth less than no guard, because it reports a pass.
10
+ *
11
+ * ⚠ BOTH TRAPS BELOW WERE PAID FOR ONCE ALREADY AND ARE PRESERVED VERBATIM. Read the comments
12
+ * before touching either function; each one describes a run where the naive version silently
13
+ * reported the wrong answer rather than failing.
14
+ *
15
+ * Not a `.test.ts`, so vitest's default glob does not collect it.
16
+ */
17
+ import { readFileSync } from 'node:fs';
18
+ /** The dispatch, as written. */
19
+ export const CLI_SRC = readFileSync(new URL('../cli.ts', import.meta.url), 'utf8');
20
+ /**
21
+ * The dispatch with comments removed.
22
+ *
23
+ * ⚠ THIS PACKAGE HAS SHIPPED GUARDS THAT MATCHED THEIR OWN DOCSTRING — an assertion forbidding a
24
+ * shape passes because the shape is quoted in the prose explaining why it is forbidden.
25
+ */
26
+ export const CLI_CODE = CLI_SRC
27
+ .replace(/\/\*[\s\S]*?\*\//g, ' ')
28
+ .replace(/(^|[^:])\/\/.*$/gm, '$1 ');
29
+ /**
30
+ * Top-level `case` labels inside the main dispatch switch — 4-space indent, ending at `default:`.
31
+ *
32
+ * ⚠⚠ TWO PASSES, AND BOTH BOUNDS MATTER.
33
+ *
34
+ * INDENT scopes to the top-level switch: `tool`, `admin` and `compose` each open a sub-switch,
35
+ * whose cases (`list`, `users`, `backup`, …) are dispatch labels for a DIFFERENT switch and are
36
+ * not verbs. An un-anchored match pulls in 36 of them.
37
+ *
38
+ * THEN every `case '…':` ON that line, because two can share one: `case 'characters': case
39
+ * 'moni': {`. Anchoring alone silently drops the second and makes its entry read as a ghost.
40
+ *
41
+ * ⚠ IT READS THE RAW SOURCE, deliberately — this is the list the file DECLARES, and stripping
42
+ * comments first would change an answer two guards already depend on. No comment in `cli.ts`
43
+ * carries a 4-space-indented `case '…':`, and the count assertion in `verb-surface.test.ts` is
44
+ * what would notice if one ever did.
45
+ */
46
+ export function switchVerbs() {
47
+ const start = CLI_SRC.indexOf('\n switch (command) {');
48
+ const end = CLI_SRC.indexOf('\n default: {', start);
49
+ if (start < 0 || end < start)
50
+ return [];
51
+ return CLI_SRC.slice(start, end).split('\n')
52
+ .filter((line) => /^ {4}case '/.test(line))
53
+ .flatMap((line) => [...line.matchAll(/case '([^']+)':/g)].map((m) => m[1]));
54
+ }
55
+ /**
56
+ * The body a `case` actually executes, from the COMMENT-STRIPPED source.
57
+ *
58
+ * ⚠⚠ FALL-THROUGH LABELS SHARE ONE BODY, AND IGNORING THAT MAKES A DETECTOR LIE. `case
59
+ * 'conversations': case 'convo': case 'conv': { … }` — slicing each label to the NEXT label gives
60
+ * the first two a body of 21 and 13 characters containing nothing but another label, so three
61
+ * correctly-wired verbs read as unwired.
62
+ *
63
+ * ⚠⚠ BRACE-MATCHED, NOT SLICED TO THE NEXT `case` LABEL. Two independent things break a label
64
+ * scan and BOTH bit on the first run of the guard this came from:
65
+ * · fall-through, above;
66
+ * · nesting — `case 'admin'` contains its OWN switch with ~24 `case` labels, so its body was
67
+ * truncated at the first of them and the `values.limit` reads 300 lines down were invisible.
68
+ * A brace counter is immune to both. Strings are skipped so a `}` in a message cannot close it;
69
+ * comments are already gone.
70
+ *
71
+ * ⚠ A CONSEQUENCE WORTH KNOWING: fall-through labels return the SAME body, so a body-derived
72
+ * signal cannot tell `ticket` from `board`. Any caller distinguishing them must look at what the
73
+ * body does with `command`, not at the body alone.
74
+ */
75
+ export function caseBody(verb) {
76
+ const cli = CLI_CODE;
77
+ const at = cli.indexOf(`case '${verb}':`);
78
+ if (at < 0)
79
+ return '';
80
+ const open = cli.indexOf('{', at);
81
+ if (open < 0)
82
+ return '';
83
+ let depth = 0;
84
+ for (let i = open; i < cli.length; i++) {
85
+ const c = cli[i];
86
+ if (c === "'" || c === '"' || c === '`') {
87
+ for (let j = i + 1; j < cli.length; j++) {
88
+ if (cli[j] === '\\') {
89
+ j++;
90
+ continue;
91
+ }
92
+ if (cli[j] === c) {
93
+ i = j;
94
+ break;
95
+ }
96
+ }
97
+ continue;
98
+ }
99
+ if (c === '{')
100
+ depth++;
101
+ else if (c === '}' && --depth === 0)
102
+ return cli.slice(at, i + 1);
103
+ }
104
+ return cli.slice(at);
105
+ }
@@ -0,0 +1,136 @@
1
+ /**
2
+ * A PROBE THAT RUNS THE WRAP — shared by every guard that used to read it.
3
+ *
4
+ * ⚠⚠ THREE SEPARATE GUARDS SCANNED SOURCE FOR THE ATTRIBUTION WRAP AND ALL THREE FAILED THE DAY IT
5
+ * MOVED, against builds where the behaviour was intact or improved. One of them had ALREADY been
6
+ * rewritten once for exactly that reason and recorded the lesson in its own comment — then broke
7
+ * the same way again. A scan cannot distinguish "this moved" from "this was deleted", so the same
8
+ * failure recurs for as long as the assertion is a scan.
9
+ *
10
+ * ⚠ A CHILD PROCESS WITH A SANDBOXED `HOME`, because `agent-state.ts` resolves the agents
11
+ * directory from `os.homedir()` at import time — an in-process test would read the developer's own
12
+ * identity and pass or fail according to whether they happened to be logged in.
13
+ *
14
+ * ⚠ IT LOADS `dist/`, not the TypeScript. That is deliberate and it is what makes this a guard on
15
+ * the SHIPPED artifact: the CLI users run is `dist/cli.js`. Run `npm run build` first — the repo's
16
+ * other child-process probes (`resolve()` in `agent-scope-persistence.test.ts`) already assume it.
17
+ */
18
+ import { mkdtempSync, mkdirSync, writeFileSync, readFileSync, rmSync } from 'node:fs';
19
+ import { tmpdir } from 'node:os';
20
+ import { join, dirname } from 'node:path';
21
+ import { fileURLToPath } from 'node:url';
22
+ import { execFileSync } from 'node:child_process';
23
+ const SRC = join(dirname(fileURLToPath(import.meta.url)), '..');
24
+ const DIST = join(SRC, '..', 'dist');
25
+ /**
26
+ * Run `activeAgentWrap` for real, with `agent` planted as the active identity (or nobody when it
27
+ * is `null`), and return the whole `{ action, input }` it produced.
28
+ */
29
+ export function probeWrap(agent, action, input = {}, door = 'call') {
30
+ const home = mkdtempSync(join(tmpdir(), 'bh-wrap-'));
31
+ try {
32
+ if (agent) {
33
+ const dir = join(home, '.beehaven', 'agents');
34
+ mkdirSync(dir, { recursive: true });
35
+ writeFileSync(join(dir, 'tester.json'), JSON.stringify({
36
+ agentId: 'id-tester', kind: 'claude-code', displayName: 'tester',
37
+ avatarId: 'tester', userId: 'u', token: 'x', boundScopes: ['family:claude'], ...agent,
38
+ }));
39
+ }
40
+ const mod = JSON.stringify(join(DIST, 'agent-attribution.js'));
41
+ const out = execFileSync(process.execPath, [
42
+ '-e',
43
+ `const a=require(${mod});`
44
+ + `a.setCliDoor(${JSON.stringify(door)});`
45
+ + `process.stdout.write(JSON.stringify(a.activeAgentWrap(${JSON.stringify(action)},`
46
+ + `${JSON.stringify(input)}, {})));`,
47
+ ], { env: { ...process.env, HOME: home, BEEHAVEN_AGENT: 'tester' }, encoding: 'utf8' });
48
+ return JSON.parse(out);
49
+ }
50
+ finally {
51
+ rmSync(home, { recursive: true, force: true });
52
+ }
53
+ }
54
+ /** The action a call comes out as — `agent-invoke` when it was attributed, itself when it was not. */
55
+ export function wrapsAs(agent, action, input = {}) {
56
+ return probeWrap(agent, action, input).action;
57
+ }
58
+ /**
59
+ * ⚠⚠ THE END-TO-END PROBE: what `rpcOnce` PUTS ON THE WIRE.
60
+ *
61
+ * `wrapsAs` exercises the helper; this exercises the CALL SITE, and the distinction is the entire
62
+ * ticket. The wrap was correct for months while twelve of fifteen command modules never reached
63
+ * it — a guard on the helper alone would have been green throughout. So this stands up a fake IPC
64
+ * server, points `rpcOnce` at it, and returns the frame the daemon would have received.
65
+ */
66
+ export function probeRpcFrame(agent, action, input = {}, door = 'call',
67
+ /**
68
+ * ⚠ `persistent` DRIVES THE OTHER SHAPE — a client held across calls (`omni`, `pipe`, `watch`,
69
+ * the interactive shell) rather than `rpcOnce`'s connect-call-disconnect. That distinction is
70
+ * not cosmetic: it is what made `rpcOnce` the wrong chokepoint, and a probe that only exercised
71
+ * the convenience wrapper would have agreed with the wrong answer.
72
+ */
73
+ opts = {}) {
74
+ const home = mkdtempSync(join(tmpdir(), 'bh-rpc-'));
75
+ const sock = join(home, 'ipc.sock');
76
+ try {
77
+ if (agent) {
78
+ const dir = join(home, '.beehaven', 'agents');
79
+ mkdirSync(dir, { recursive: true });
80
+ writeFileSync(join(dir, 'tester.json'), JSON.stringify({
81
+ agentId: 'id-tester', kind: 'claude-code', displayName: 'tester',
82
+ avatarId: 'tester', userId: 'u', token: 'x', boundScopes: ['family:claude'], ...agent,
83
+ }));
84
+ }
85
+ const ipc = JSON.stringify(join(DIST, 'ipc-client.js'));
86
+ const attr = JSON.stringify(join(DIST, 'agent-attribution.js'));
87
+ /**
88
+ * ⚠ `requireAuth:false` IS WHY THE FAKE SERVER IS SIX LINES. `IpcClient.connect` skips the
89
+ * whole token handshake on that flag; a faithful auth simulation would be a second
90
+ * implementation of the thing under test.
91
+ */
92
+ const script = `
93
+ const net=require('net');const fs=require('fs');
94
+ const server=net.createServer((s)=>{
95
+ s.write(JSON.stringify({type:'status',clientId:'c1',daemonPid:1,requireAuth:false})+'\\n');
96
+ let buf='';
97
+ s.on('data',(d)=>{ buf+=d.toString();
98
+ for(const line of buf.split('\\n')){ if(!line.trim())continue;
99
+ let m; try{m=JSON.parse(line);}catch{continue;}
100
+ if(m.type==='rpc'){
101
+ fs.writeFileSync(${JSON.stringify(join(home, 'frame.json'))},
102
+ JSON.stringify({action:m.action,input:m.input}));
103
+ s.write(JSON.stringify({type:'rpc-result',rid:m.rid,result:{ok:true}})+'\\n');
104
+ }
105
+ }
106
+ buf='';
107
+ });
108
+ });
109
+ server.listen(${JSON.stringify(sock)}, async ()=>{
110
+ const a=require(${attr}); a.setCliDoor(${JSON.stringify(door)});
111
+ const c=require(${ipc});
112
+ try{
113
+ if(${opts.persistent ? 'true' : 'false'}){
114
+ const client = await c.connectClient();
115
+ try{ await client.call(${JSON.stringify(action)}, ${JSON.stringify(input)}, 4000); }
116
+ finally{ client.disconnect(); }
117
+ } else {
118
+ await c.rpcOnce(${JSON.stringify(action)}, ${JSON.stringify(input)}, 4000);
119
+ }
120
+ }catch(e){}
121
+ server.close(); process.exit(0);
122
+ });`;
123
+ execFileSync(process.execPath, ['-e', script], {
124
+ env: {
125
+ ...process.env, HOME: home, BEEHAVEN_AGENT: 'tester',
126
+ BEEHAVEN_IPC_SOCKET: sock, BEEHAVEN_IPC_AUTH: 'unused',
127
+ },
128
+ encoding: 'utf8',
129
+ timeout: 20_000,
130
+ });
131
+ return JSON.parse(readFileSync(join(home, 'frame.json'), 'utf8'));
132
+ }
133
+ finally {
134
+ rmSync(home, { recursive: true, force: true });
135
+ }
136
+ }
@@ -0,0 +1,192 @@
1
+ /**
2
+ * ATTRIBUTION — who ran this, and through which door.
3
+ *
4
+ * ⚠⚠ THIS MODULE EXISTS BECAUSE PER-VERB STAMPING MISSED 28 OF 47 REMOTE VERBS.
5
+ *
6
+ * `agent_activity.source` was wired by having a verb's module import `agentRpc` instead of
7
+ * `rpcOnce` — three modules did (`ticket`, `sheet`, `slides`), twelve did not, and nothing
8
+ * anywhere reported the difference. The measured consequence was not a cosmetic gap in a report:
9
+ * a traffic table read "all 22 PROVISIONAL verbs have zero traffic" and the recorded retirement
10
+ * criterion would have deleted `task` and `search` — both demonstrably working — because they
11
+ * emit no activity row AT ALL. **Zero measured the instrumentation, not the usage.**
12
+ *
13
+ * ⚠ THE FIX IS THE PLACEMENT, NOT THE CODE. Everything below already existed in
14
+ * `commands/workspace.ts`; what changed is WHO CALLS IT — the three places an rpc frame is
15
+ * actually built now do, so a new verb is attributed without its author having to know this file
16
+ * exists:
17
+ *
18
+ * · `IpcClient.call` (`ipc-client.ts`) — the transport behind `rpcOnce`, `connectClient`,
19
+ * and every long-lived client (`omni`, `pipe`, …)
20
+ * · `daemonRpc` (`daemon-client.ts`) — the SECOND general transport: `logs`, `shop`,
21
+ * `costs`, the `--building` gateway, the shell
22
+ * · `ClaudeBridge.rpc` (`claude-bridge.ts`) — dormant, but general
23
+ *
24
+ * ⚠ `rpcOnce` WAS TRIED AS THE CHOKEPOINT AND IS ONE LEVEL TOO HIGH. It is a convenience wrapper
25
+ * (connect → call → disconnect); every caller that HOLDS a client goes round it. That was not a
26
+ * theoretical objection — `commands/omni.ts` was doing exactly that, and the guard caught it.
27
+ *
28
+ * ⚠ IT LIVES HERE rather than in `workspace.ts` only so the transports can import it without a
29
+ * cycle (`workspace.ts` imports `rpcOnce`). Its dependencies are `agent-state` → `agent-scope` →
30
+ * node builtins, and nothing else.
31
+ */
32
+ import { resolveActiveAgent } from './agent-state.js';
33
+ /**
34
+ * ⚠⚠ THE VERBS THAT ESTABLISH AN IDENTITY CANNOT BE WRAPPED IN ONE — a cold-start deadlock.
35
+ *
36
+ * `agentInvoke` gates every call through `_checkAgentScope`, which refuses a principal the
37
+ * workspace has no `agents` row for: *"unknown agent 'X'"*. An identity is per WORKSPACE — the
38
+ * token carries `agentId` + `userId` and no workspace, and the row lives in the workspace DO — so
39
+ * arriving in a new workspace you are, correctly, nobody there.
40
+ *
41
+ * The deadlock is that the way to STOP being nobody was also wrapped:
42
+ *
43
+ * · `agent-mint-token` → refused, so `agent login` silently fell through to a LOCAL mint,
44
+ * printed "✓ Signed in" and "◆ Workspace session open", and every later call failed.
45
+ * · `agent-create` → refused, because the call to create the agent ran AS that agent.
46
+ *
47
+ * Reproduced on `checkout-test-9`, which holds ZERO agents and was therefore unreachable from the
48
+ * CLI by any means short of dev-scope `beehaven sql`.
49
+ *
50
+ * ⚠ THIS IS NOT A HOLE IN THE GATE. These dispatch as the ACCOUNT OWNER, exactly as they did
51
+ * before any agent existed, and the owner is who is entitled to create an agent in their own
52
+ * workspace. Attribution is not lost either — there is nothing yet to attribute to.
53
+ *
54
+ * ⚠⚠ THE MEMBERSHIP TEST IS "DOES THIS VERB ESTABLISH AN IDENTITY", NOT A NUMBER. These five all
55
+ * do: mint one on the account, join a workspace with it, or see what is already there. Every
56
+ * OTHER verb must stay wrapped, or "run it as the owner instead" becomes the universal way round
57
+ * the per-agent scope — which is why the guard caps the set rather than merely listing it.
58
+ */
59
+ export const BOOTSTRAP_ACTIONS = new Set([
60
+ // Account-level: mint an identity, then join a workspace with it.
61
+ 'agent-account-mint', 'agent-admit',
62
+ // The workspace-scoped predecessors, kept so an older flow still bootstraps.
63
+ 'agent-mint-token', 'agent-create', 'agent-list',
64
+ ]);
65
+ /**
66
+ * ⚠⚠ ACTIONS THE DAEMON ANSWERS ITSELF — THEY NEVER REACH A WORKSPACE, SO THEY CANNOT BE WRAPPED.
67
+ *
68
+ * `IpcClient.call` sends `type:'rpc'`, and `ipc-server.ts` forwards every one of those to the
69
+ * relay — EXCEPT the eleven the daemon intercepts in its own `ipc.on('rpc')` handler
70
+ * (`daemon.ts`). Wrapping one of those in `agent-invoke` produces an action the daemon does not
71
+ * recognise, and it would be forwarded to the cloud instead of handled locally: `beehaven connect`
72
+ * would stop setting the target.
73
+ *
74
+ * ⚠⚠ ENUMERATED, NEVER A "HAS A COLON" RULE. `admin:store-rpc`, `admin:broadcast`,
75
+ * `admin:store-query`, `_rpc:catalog` and `vector:catalog.presets` are all namespaced AND all
76
+ * genuinely remote. A prefix heuristic would silently un-attribute every one of them, which is
77
+ * this ticket's own defect wearing a tidier shape.
78
+ *
79
+ * ⚠ MIRRORS `daemon.ts`. `attribution.test.ts` reads that file and fails if the two drift — a new
80
+ * daemon-local verb that is not listed here becomes an action wrapped in `agent-invoke` and shipped
81
+ * to the cloud, which fails in a way that names neither cause.
82
+ */
83
+ export const DAEMON_LOCAL_ACTIONS = new Set([
84
+ 'building:connect', 'building:disconnect', 'building:connection',
85
+ 'agent:mint', 'agent:connect', 'agent:disconnect',
86
+ 'daemon:session-status', 'daemon:app-ensure', 'daemon:app-list',
87
+ 'daemon:app-install', 'daemon:app-uninstall',
88
+ ]);
89
+ /**
90
+ * ⚠⚠ WHICH DOOR THIS PROCESS IS — `cli:call` when the user typed `beehaven call`, `cli:ticket`
91
+ * when they typed `beehaven ticket create`, and so on.
92
+ *
93
+ * It answers the one question `agent_activity` could not: whether a typed verb or the raw
94
+ * `call` escape hatch did the work. Nothing else can tell them apart — both arrive as the same
95
+ * action, from the same agent, over the same transport — so the CLI's ~103 verbs have never had
96
+ * a usage figure, and any retirement list is taste until this has run for a while.
97
+ *
98
+ * ⚠ A MODULE GLOBAL, AND THAT IS THE RIGHT SHAPE HERE. One CLI process handles exactly one
99
+ * command; the alternative is threading a door argument through every call site, which is ~169
100
+ * sites today and one forgotten site per verb added — i.e. a mirror, and mirrors in this repo
101
+ * drift. Set once in `cli.ts` from the parsed command, read once here.
102
+ *
103
+ * ⚠ IT IS THE VERB THE USER TYPED, NOT THE ACTION DISPATCHED. `beehaven ticket create` sends
104
+ * `ticket-create`, and labelling the door `cli:ticket-create` would make the column a second
105
+ * copy of `action` — it has to name the SURFACE, or the join that answers "verb or call?"
106
+ * cannot be written.
107
+ *
108
+ * ⚠ EMPTY UNTIL SET. A CLI path that never calls `setCliDoor` logs no door rather than a wrong
109
+ * one, and an unknown door must stay distinguishable from a real one in a `GROUP BY`.
110
+ */
111
+ let CLI_DOOR = '';
112
+ /** Called once from `cli.ts` with the verb the user actually typed. Aliases resolve to the
113
+ * canonical verb by the caller, so `cd` and `connect` do not read as two surfaces. */
114
+ export function setCliDoor(command) {
115
+ const v = String(command || '').trim().toLowerCase();
116
+ // ⚠ Bounded and charset-checked: it lands in a column and in a `GROUP BY`, and an
117
+ // unconstrained caller-supplied label is how a cardinality explosion gets into a report.
118
+ CLI_DOOR = /^[a-z0-9][a-z0-9:_-]{0,32}$/.test(v) ? `cli:${v}` : '';
119
+ }
120
+ export function cliDoor() {
121
+ return CLI_DOOR;
122
+ }
123
+ /**
124
+ * ⚠⚠ IS THIS PAYLOAD ALREADY ATTRIBUTED? — the idempotency test that lets the wrap move down to
125
+ * the transport without double-wrapping the paths that still wrap above it.
126
+ *
127
+ * Two shapes count as attributed, and the second is the one that is easy to miss:
128
+ *
129
+ * · `agent-invoke` itself.
130
+ * · A CROSS-DO PROXY CARRYING ONE. `cmdCall` wraps and THEN calls `resolveBuilding`, because
131
+ * `agent-invoke` must travel INSIDE the proxy so the gate, the activity row and the work all
132
+ * land in the addressed workspace. The wire shape is `{ <type>Id, action:'agent-invoke',
133
+ * input }` (see `gatewayEnvelope`). Wrapping that again would produce
134
+ * `agent-invoke(admin:team-rpc(agent-invoke(…)))` — the outer one attributing in the LOCAL
135
+ * workspace's log for work performed remotely, which is precisely the ordering bug the
136
+ * wrap-then-proxy sequence was introduced to fix.
137
+ *
138
+ * ⚠ ONE LEVEL, NEVER A LOOP. A proxy nested in a proxy is not a shape anything produces, and a
139
+ * recursive walk over caller-supplied input is unbounded work on a value we do not control. The
140
+ * worker's own `proxiedAction` makes the same call for the same reason.
141
+ */
142
+ export function alreadyAttributed(action, input) {
143
+ if (action === 'agent-invoke')
144
+ return true;
145
+ if (!input || typeof input !== 'object')
146
+ return false;
147
+ return input.action === 'agent-invoke';
148
+ }
149
+ /**
150
+ * Wrap a call in `agent-invoke` when an agent is signed in — BEFORE the building envelope.
151
+ *
152
+ * ⚠⚠ THE ORDER IS THE WHOLE POINT. `agent-invoke` must travel INSIDE the proxy so it executes on
153
+ * the workspace being addressed; wrapped outside, it runs on the daemon's default DO and
154
+ * `_checkAgentScope` asks the wrong roster whether the agent is a member. See the note at the call
155
+ * site for the measurement.
156
+ *
157
+ * ⚠ IT RESOLVES THE AGENT ITSELF rather than taking one, because the wrap now happens earlier than
158
+ * the refusal checks in `cmdCall`. `resolveActiveAgent` is file reads only, and resolving twice is
159
+ * cheaper than threading state through a routing decision.
160
+ *
161
+ * ⚠ AN EXPIRED IDENTITY IS NOT WRAPPED HERE — it is REFUSED by `cmdCall`, with a message naming
162
+ * the re-login. Wrapping it would send a token the server rejects and turn a clear local error
163
+ * into a remote one.
164
+ */
165
+ export function activeAgentWrap(action, input, values) {
166
+ if (alreadyAttributed(action, input))
167
+ return { action, input };
168
+ if (BOOTSTRAP_ACTIONS.has(action) || DAEMON_LOCAL_ACTIONS.has(action))
169
+ return { action, input };
170
+ let active = null;
171
+ try {
172
+ active = resolveActiveAgent(values);
173
+ }
174
+ catch {
175
+ return { action, input };
176
+ }
177
+ if (!active || active.expired)
178
+ return { action, input };
179
+ return {
180
+ action: 'agent-invoke',
181
+ input: {
182
+ agentId: active.agentId,
183
+ action,
184
+ input,
185
+ ...(active.record.sessionId ? { sessionId: active.record.sessionId } : {}),
186
+ // ⚠ WHICH DOOR — `cli:call` vs `cli:ticket` vs … See `cliDoor()` and
187
+ // `agent_activity.source`. Set here, at the ONE wrap every CLI path shares, so a new
188
+ // verb is labelled without its author having to know this exists.
189
+ ...(cliDoor() ? { source: cliDoor() } : {}),
190
+ },
191
+ };
192
+ }