@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,135 @@
1
+ /**
2
+ * agent-identity-daemon — daemon-side minting + verification of
3
+ * AgentIdentity tokens.
4
+ *
5
+ * The daemon is the only party that holds the signing secret. The CLI
6
+ * never sees it — `beehaven agent login` asks the daemon to mint a token
7
+ * over IPC (`agent:mint`), and the daemon verifies tokens on the
8
+ * building-call path (`verifyAgentToken`).
9
+ *
10
+ * Secret resolution (first hit wins):
11
+ * 1. process.env.AGENT_IDENTITY_SECRET — explicit, used in prod/cloud
12
+ * where the same secret must also be set on the Worker (so cloud
13
+ * DOs can verify cloud-routed agent calls).
14
+ * 2. macOS/Linux keychain account "agent-identity-secret" — persists
15
+ * across daemon restarts on this machine.
16
+ * 3. ~/.beehaven/agent-identity-secret (mode 600) — non-keychain hosts.
17
+ * 4. Generate a strong random secret + persist it. Local-dev
18
+ * convenience so the 4-terminals demo works without manual setup;
19
+ * the secret only needs to be stable on THIS machine because mint
20
+ * AND verify both run in the daemon.
21
+ *
22
+ * Secrets shorter than 32 chars are refused (fail closed) — we never
23
+ * mint with a weak/empty key.
24
+ *
25
+ * @clearly-sh/beehaven-core is an optional peer dep, so AgentIdentity is
26
+ * loaded via dynamic import (matching daemon.ts's pattern). The option /
27
+ * claims shapes below mirror core's exported types.
28
+ */
29
+ import { randomBytes } from 'node:crypto';
30
+ import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'node:fs';
31
+ import { homedir } from 'node:os';
32
+ import { join, dirname } from 'node:path';
33
+ import { keychain } from './keychain.js';
34
+ // ── Secret resolution ────────────────────────────────
35
+ const KC_ACCOUNT = 'agent-identity-secret';
36
+ const SECRET_FILE = join(homedir(), '.beehaven', 'agent-identity-secret');
37
+ const MIN_SECRET_LEN = 32;
38
+ let _cachedSecret = null;
39
+ /**
40
+ * Resolve the AgentIdentity signing secret, generating + persisting one
41
+ * on first use if nothing is configured. Always returns a secret that is
42
+ * at least 32 chars. Cached for the process lifetime.
43
+ */
44
+ export function resolveAgentIdentitySecret() {
45
+ if (_cachedSecret)
46
+ return _cachedSecret;
47
+ // 1. Explicit env (prod / cloud parity).
48
+ const fromEnv = process.env.AGENT_IDENTITY_SECRET;
49
+ if (fromEnv && fromEnv.length >= MIN_SECRET_LEN) {
50
+ _cachedSecret = fromEnv;
51
+ return _cachedSecret;
52
+ }
53
+ if (fromEnv && fromEnv.length < MIN_SECRET_LEN) {
54
+ console.warn('[agent-identity] AGENT_IDENTITY_SECRET is set but shorter than 32 chars — ignoring it and using a locally-generated secret. Fix the env var for prod/cloud parity.');
55
+ }
56
+ // 2. Keychain (persists across daemon restarts on this machine).
57
+ try {
58
+ const fromKc = keychain.get(KC_ACCOUNT);
59
+ if (fromKc && fromKc.length >= MIN_SECRET_LEN) {
60
+ _cachedSecret = fromKc;
61
+ return _cachedSecret;
62
+ }
63
+ }
64
+ catch {
65
+ /* keychain unavailable — fall through */
66
+ }
67
+ // 3. File fallback (non-keychain hosts / containers).
68
+ try {
69
+ if (existsSync(SECRET_FILE)) {
70
+ const fromFile = readFileSync(SECRET_FILE, 'utf8').trim();
71
+ if (fromFile.length >= MIN_SECRET_LEN) {
72
+ _cachedSecret = fromFile;
73
+ return _cachedSecret;
74
+ }
75
+ }
76
+ }
77
+ catch {
78
+ /* unreadable — fall through */
79
+ }
80
+ // 4. Generate + persist.
81
+ const generated = randomBytes(48).toString('hex'); // 96 hex chars
82
+ let persisted = false;
83
+ try {
84
+ persisted = keychain.set(KC_ACCOUNT, generated);
85
+ }
86
+ catch {
87
+ /* try file next */
88
+ }
89
+ if (!persisted) {
90
+ try {
91
+ mkdirSync(dirname(SECRET_FILE), { recursive: true });
92
+ writeFileSync(SECRET_FILE, generated, { mode: 0o600 });
93
+ persisted = true;
94
+ }
95
+ catch {
96
+ /* in-memory only for this process lifetime */
97
+ }
98
+ }
99
+ if (!persisted) {
100
+ console.warn('[agent-identity] could not persist the signing secret to keychain or disk — agent tokens will be invalidated on the next daemon restart.');
101
+ }
102
+ _cachedSecret = generated;
103
+ return _cachedSecret;
104
+ }
105
+ // ── Core loader (optional peer dep → dynamic import) ──
106
+ let _AgentIdentity = null;
107
+ async function loadAgentIdentity() {
108
+ if (_AgentIdentity)
109
+ return _AgentIdentity;
110
+ // @ts-ignore — optional peer dependency, resolved at runtime
111
+ const mod = await import('@clearly-sh/beehaven-core/server');
112
+ if (!mod?.AgentIdentity) {
113
+ throw new Error('AgentIdentity not found in @clearly-sh/beehaven-core/server — update the core SDK.');
114
+ }
115
+ _AgentIdentity = mod.AgentIdentity;
116
+ return _AgentIdentity;
117
+ }
118
+ // ── Public API ───────────────────────────────────────
119
+ /** Mint a signed AgentIdentity token. Daemon-only (holds the secret). */
120
+ export async function mintAgentToken(opts) {
121
+ const secret = resolveAgentIdentitySecret();
122
+ if (!secret || secret.length < MIN_SECRET_LEN) {
123
+ throw new Error('Cannot mint agent identity: no signing secret available. Set AGENT_IDENTITY_SECRET (>=32 chars) or run inside the Hive Mac app.');
124
+ }
125
+ const AgentIdentity = await loadAgentIdentity();
126
+ return AgentIdentity.issue(opts, secret);
127
+ }
128
+ /** Verify a token. Returns null if invalid, expired, tampered, or unsigned. */
129
+ export async function verifyAgentToken(token) {
130
+ const secret = resolveAgentIdentitySecret();
131
+ if (!secret || secret.length < MIN_SECRET_LEN)
132
+ return null;
133
+ const AgentIdentity = await loadAgentIdentity();
134
+ return AgentIdentity.verify(token, secret);
135
+ }
@@ -0,0 +1,156 @@
1
+ /**
2
+ * agent-onboard.ts — the walkthrough that turns "you have no agent identity"
3
+ * into one, at the moment someone first needs it.
4
+ *
5
+ * WHY THIS EXISTS AT ALL. `beehaven speak` used to work for anybody: no identity,
6
+ * no voice binding, one shared house voice. That is convenient exactly once — the
7
+ * first time. After that every agent on the machine sounds identical, nothing in
8
+ * the transcript says who spoke, and the avatar cannot hydrate a per-agent voice
9
+ * because there is no agent to hydrate from. The identity is what makes a voice
10
+ * *someone's*, so `speak` now requires one.
11
+ *
12
+ * ⚠⚠ A REQUIREMENT WITHOUT A PATH IS A WALL. Refusing with "run `beehaven agent
13
+ * login <name>`" would be correct and useless: the caller is mid-task, does not
14
+ * know what a `kind` is, has no idea which voices exist, and has just been told
15
+ * that the thing they ran does not work. This asks the three questions that
16
+ * matter, in order, with defaults that are right for the common case — and it
17
+ * ends by RUNNING the login, not by printing it.
18
+ *
19
+ * ⚠ NON-INTERACTIVE CALLERS GET THE COMMAND, NOT A HANG. A prompt written to a
20
+ * pipe blocks forever and looks like the CLI has frozen. `stdin.isTTY` decides,
21
+ * and the non-TTY branch prints the exact one-liner (including the voice flag)
22
+ * so a script or a CI job can be fixed in one edit. This matters more than usual
23
+ * here: the callers most likely to hit this are agents, not people.
24
+ */
25
+ import { createInterface } from 'node:readline';
26
+ import { color } from './spinner.js';
27
+ import { isValidAgentName, loadAgent } from './agent-state.js';
28
+ /**
29
+ * Voice providers an identity may bind to, with the voice id each expects.
30
+ *
31
+ * ⚠ `qwen3-mlx` LEADS because it is the only fully local one that can speak in a
32
+ * CLONED voice, which is the whole reason to give an agent its own voice rather
33
+ * than a shared one. The rest are ordered by how little they need: `macos` needs
34
+ * nothing, `kokoro` needs a downloaded model, the cloud ones need an account.
35
+ */
36
+ export const VOICE_CHOICES = [
37
+ /**
38
+ * ⚠⚠ AN EMPTY VOICE HERE MEANS "A DIFFERENT PERSON EVERY UTTERANCE", NOT "the
39
+ * default one". On the Base checkpoint the model SAMPLES a speaker when none is
40
+ * conditioned, so the base voice is not a voice — it is a draw. Measured spread
41
+ * across repeated runs of one sentence: ~75%.
42
+ *
43
+ * ⚠ AND A PRESET NAME DOES NOT FIX IT — Base discards the name (`ethan` vs
44
+ * `eric` measured 0.6% apart, i.e. identical). Only a REFERENCE RECORDING pins
45
+ * the timbre: a cloned profile measured ~4%.
46
+ *
47
+ * The default stays empty because there is no name this file can assume exists —
48
+ * the registry is per-machine. `beehaven voice clone <name> <audio>` is the step
49
+ * that makes this provider sound the same twice, and the note says so.
50
+ */
51
+ { provider: 'qwen3-mlx', label: 'Qwen3-TTS (on-device, MLX)', defaultVoice: '', note: 'local; needs a `beehaven voice clone` name to sound the same twice' },
52
+ { provider: 'macos', label: 'macOS say', defaultVoice: 'Samantha', note: 'offline, always available, robotic' },
53
+ { provider: 'kokoro', label: 'Kokoro (on-device)', defaultVoice: 'claudette', note: 'local; needs the downloaded model' },
54
+ { provider: 'openai', label: 'GPT Audio', defaultVoice: 'marin', note: 'cloud; included with Clearly' },
55
+ { provider: 'elevenlabs', label: 'ElevenLabs', defaultVoice: 'EXAVITQu4vr4xnSDxMaL', note: 'cloud; needs your own API key' },
56
+ ];
57
+ export function isKnownVoiceProvider(p) {
58
+ return VOICE_CHOICES.some((v) => v.provider === p);
59
+ }
60
+ /** `provider[:voiceId]` → the pair, filling in the provider's default voice. */
61
+ export function parseVoiceBinding(raw) {
62
+ const trimmed = String(raw || '').trim();
63
+ if (!trimmed)
64
+ return null;
65
+ const [provider, ...rest] = trimmed.split(':');
66
+ const p = provider.trim();
67
+ if (!p)
68
+ return null;
69
+ const explicit = rest.length ? rest.join(':').trim() : '';
70
+ const known = VOICE_CHOICES.find((v) => v.provider === p);
71
+ return { provider: p, voiceId: explicit || known?.defaultVoice };
72
+ }
73
+ function ask(rl, q) {
74
+ return new Promise((resolve) => rl.question(q, (a) => resolve(a.trim())));
75
+ }
76
+ /**
77
+ * The command that would produce the same identity — printed to non-interactive
78
+ * callers, and echoed after an interactive run so the choice is reproducible.
79
+ */
80
+ export function loginCommandFor(r) {
81
+ const v = r.voice.voiceId ? `${r.voice.provider}:${r.voice.voiceId}` : r.voice.provider;
82
+ // ⚠ `--agent-voice`, never `--voice` — see the flag declaration in cli.ts.
83
+ return `beehaven agent login ${r.name} --kind ${r.kind} --agent-voice ${v}`;
84
+ }
85
+ /**
86
+ * Ask the three questions. Returns null when the caller cannot be prompted or
87
+ * abandons the walkthrough — the caller decides what to do about that, because
88
+ * "refuse" and "fall back" are different answers for different commands.
89
+ */
90
+ export async function runAgentOnboarding(opts) {
91
+ const suggested = opts.suggestedName && isValidAgentName(opts.suggestedName) ? opts.suggestedName : 'assistant';
92
+ if (!process.stdin.isTTY) {
93
+ console.error(` ${color.red('✗')} ${opts.reason}`);
94
+ console.error('');
95
+ console.error(' This needs an agent identity — it is what binds a voice to a speaker.');
96
+ // ⚠ `--agent-voice`. Printing `--voice` here would hand the reader the boolean
97
+ // flag that silently drops the binding — the exact trap this file documents.
98
+ console.error(` Create one: ${color.cyan(`beehaven agent login ${suggested} --agent-voice qwen3-mlx:ethan`)}`);
99
+ console.error(` Then re-run. Voices: ${VOICE_CHOICES.map((v) => v.provider).join(' | ')}`);
100
+ return null;
101
+ }
102
+ console.error('');
103
+ console.error(` ${color.yellow('●')} ${opts.reason}`);
104
+ console.error('');
105
+ console.error(' An agent identity is who is speaking — it carries the voice, the avatar');
106
+ console.error(' binding and the permissions. Three questions and you are done.');
107
+ console.error('');
108
+ const rl = createInterface({ input: process.stdin, output: process.stderr });
109
+ try {
110
+ // 1 — name
111
+ let name = '';
112
+ for (let i = 0; i < 3 && !name; i++) {
113
+ const answer = await ask(rl, ` Name for this agent [${suggested}]: `);
114
+ const candidate = answer || suggested;
115
+ if (!isValidAgentName(candidate)) {
116
+ console.error(` ${color.red('✗')} letters, digits, '.', '_', '-' only (max 64).`);
117
+ continue;
118
+ }
119
+ // ⚠ REFUSE A NAME ALREADY ON THIS MACHINE rather than silently re-minting
120
+ // over it — that would move an existing agent's avatar and voice without
121
+ // saying so, and the previous binding would simply stop being true.
122
+ if (loadAgent(candidate)) {
123
+ console.error(` ${color.red('✗')} "${candidate}" already exists here. Use it with --agent ${candidate}, or pick another name.`);
124
+ continue;
125
+ }
126
+ name = candidate;
127
+ }
128
+ if (!name)
129
+ return null;
130
+ // 2 — voice
131
+ console.error('');
132
+ console.error(' Voice:');
133
+ VOICE_CHOICES.forEach((v, i) => {
134
+ console.error(` ${i + 1}) ${v.label.padEnd(30)} ${color.dim(v.note)}`);
135
+ });
136
+ let choice = VOICE_CHOICES[0];
137
+ const picked = await ask(rl, ` Pick 1-${VOICE_CHOICES.length} [1]: `);
138
+ const idx = picked ? Number(picked) - 1 : 0;
139
+ if (Number.isInteger(idx) && idx >= 0 && idx < VOICE_CHOICES.length)
140
+ choice = VOICE_CHOICES[idx];
141
+ // ⚠ An empty default is shown as "base voice" rather than as blank brackets,
142
+ // which read as a question with no answer.
143
+ const shown = choice.defaultVoice || 'base voice';
144
+ const voiceId = (await ask(rl, ` Voice id [${shown}]: `)) || choice.defaultVoice;
145
+ // 3 — kind. Asked last and defaulted, because it is the one nobody has an
146
+ // opinion about on their first run.
147
+ const kindRaw = await ask(rl, ' Kind — claude-code | daemon-agent | clearly-ai [claude-code]: ');
148
+ const kind = (['claude-code', 'daemon-agent', 'clearly-ai'].includes(kindRaw)
149
+ ? kindRaw
150
+ : 'claude-code');
151
+ return { name, kind, voice: { provider: choice.provider, voiceId } };
152
+ }
153
+ finally {
154
+ rl.close();
155
+ }
156
+ }
@@ -0,0 +1,79 @@
1
+ /**
2
+ * agent-scope — WHICH PROCESS IS CALLING. Dependency-free, on purpose.
3
+ *
4
+ * ⚠⚠ IT LIVES ALONE BECAUSE BOTH SIDES NEED IT AND NEITHER MAY IMPORT THE OTHER.
5
+ * `cli-state` (the cursor) needs to know which agent is bound to this process, and
6
+ * `agent-state` (the identity) needs to know what this process's scope IS. Putting the
7
+ * derivation in either one makes a cycle; the first attempt papered over that with
8
+ * `require()` inside an ESM package, which throws `ReferenceError` and — because both call
9
+ * sites are wrapped in try/catch for good reasons — was swallowed in silence. `agent login`
10
+ * wrote a correct binding and `pwd` reported "Agent: none" three commands later.
11
+ *
12
+ * ⚠ NOTHING IS IMPORTED HERE. That is the invariant: the moment this file imports either
13
+ * side, the cycle is back.
14
+ *
15
+ * ── WHY NOT TTY ──────────────────────────────────────────────────────────────────────────
16
+ * `electron/terminal-bridge.ts` allocates a REAL pty via `os.forkpty()` — chosen precisely
17
+ * because Electron gives children socket stdio, which breaks `script`. So an agent running
18
+ * in the in-app terminal has `isTTY: true` on all three streams and is indistinguishable
19
+ * from a person. `!isTTY` proves non-interactive; `isTTY` proves nothing at all. A
20
+ * TTY-based check would have missed exactly the agents that run inside this product.
21
+ *
22
+ * So identify an agent by ITS OWN MARKER, which works on a pty.
23
+ */
24
+ /**
25
+ * A stable, per-run handle for the calling process.
26
+ *
27
+ * Returns `''` for a human at a terminal — the one case that must keep sharing the
28
+ * unscoped `target.json`, because anything else silently moves where an existing user's
29
+ * `cd` is recorded.
30
+ */
31
+ export function deriveAgentScope(env = process.env) {
32
+ const explicit = (env.BEEHAVEN_AGENT || '').trim();
33
+ if (explicit)
34
+ return explicit; // an explicit choice always wins
35
+ // Claude Code — its own marker (`daemon-omni.ts` already relies on it: "the SDK refuses to
36
+ // start when CLAUDECODE=1 is set … it's how it refuses to recurse"), plus a per-session id.
37
+ if (env.CLAUDECODE || env.CLAUDE_CODE_ENTRYPOINT) {
38
+ const sid = (env.CLAUDE_CODE_SESSION_ID || '').replace(/[^A-Za-z0-9]/g, '');
39
+ return sid ? `claude-${sid.slice(0, 12)}` : 'claude';
40
+ }
41
+ // Other rooms the desktop hosts. Extend HERE, so the ladder stays in one place.
42
+ if (env.CODEX_SESSION_ID) {
43
+ return `codex-${String(env.CODEX_SESSION_ID).replace(/[^A-Za-z0-9]/g, '').slice(0, 12)}`;
44
+ }
45
+ if (env.GEMINI_CLI_SESSION_ID) {
46
+ return `gemini-${String(env.GEMINI_CLI_SESSION_ID).replace(/[^A-Za-z0-9]/g, '').slice(0, 12)}`;
47
+ }
48
+ /**
49
+ * ⚠ NO MARKER AND NO TTY IS STILL NOT A PERSON. Key on the PARENT pid — stable for the
50
+ * life of one process tree, different for a concurrent one. Not perfect (pids are reused
51
+ * eventually), but the failure mode of a collision here is two runs of the SAME tool,
52
+ * which is far milder than the shared-file failure it replaces: two DIFFERENT agents.
53
+ */
54
+ if (!process.stdin.isTTY && !process.stdout.isTTY)
55
+ return `pid-${process.ppid}`;
56
+ return ''; // a human at a terminal
57
+ }
58
+ export function principalKind(input) {
59
+ const now = input.now ?? Date.now();
60
+ if (input.agentSessionId) {
61
+ const expired = typeof input.agentExpiresAt === 'number' && input.agentExpiresAt <= now;
62
+ return expired ? 'expired' : 'agent';
63
+ }
64
+ // ⚠ An agent runtime must NAME ITSELF; it may not fall through to the human's account.
65
+ if (input.agentRuntime)
66
+ return 'refuse';
67
+ return input.accountUserId && input.accountToken ? 'human' : 'refuse';
68
+ }
69
+ export function isAgentRuntime(env = process.env) {
70
+ if ((env.BEEHAVEN_AGENT || '').trim())
71
+ return true;
72
+ if (env.CLAUDECODE || env.CLAUDE_CODE_ENTRYPOINT)
73
+ return true;
74
+ if (env.CODEX_SESSION_ID)
75
+ return true;
76
+ if (env.GEMINI_CLI_SESSION_ID)
77
+ return true;
78
+ return false;
79
+ }