@clearlysh/beehaven 0.8.16
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +201 -0
- package/dist/__tests__/_cli-switch.js +105 -0
- package/dist/__tests__/agent-wrap-probe.js +136 -0
- package/dist/agent-attribution.js +192 -0
- package/dist/agent-identity-daemon.js +135 -0
- package/dist/agent-onboard.js +156 -0
- package/dist/agent-scope.js +79 -0
- package/dist/agent-sessions.js +370 -0
- package/dist/agent-state.js +482 -0
- package/dist/app-manifest.js +221 -0
- package/dist/app-signer.js +54 -0
- package/dist/app-signing.js +127 -0
- package/dist/audio-play.js +98 -0
- package/dist/avatar-protocol.js +25 -0
- package/dist/avatar-route.js +79 -0
- package/dist/bee/cloud-proxy.js +130 -0
- package/dist/blocked-actions.js +120 -0
- package/dist/build-channel.js +101 -0
- package/dist/buildings/daemon-workspace.js +475 -0
- package/dist/claude-bridge.js +310 -0
- package/dist/claude-credential-store.js +633 -0
- package/dist/clearly-sync.js +518 -0
- package/dist/clearly.js +616 -0
- package/dist/cli-state.js +334 -0
- package/dist/cli.js +6973 -0
- package/dist/commands/agent-skills.js +302 -0
- package/dist/commands/agent.js +1310 -0
- package/dist/commands/app.js +1123 -0
- package/dist/commands/auth.js +171 -0
- package/dist/commands/building-shell.js +392 -0
- package/dist/commands/canvas.js +932 -0
- package/dist/commands/claude.js +218 -0
- package/dist/commands/compliance.js +387 -0
- package/dist/commands/compose.js +507 -0
- package/dist/commands/composition.js +95 -0
- package/dist/commands/connect-session.js +409 -0
- package/dist/commands/conversations.js +160 -0
- package/dist/commands/costs.js +443 -0
- package/dist/commands/d1.js +253 -0
- package/dist/commands/daemon-cmd.js +162 -0
- package/dist/commands/deploy.js +164 -0
- package/dist/commands/design.js +617 -0
- package/dist/commands/dev-generate.js +125 -0
- package/dist/commands/dev.js +109 -0
- package/dist/commands/devices.js +135 -0
- package/dist/commands/explain-router.js +24 -0
- package/dist/commands/explain.js +882 -0
- package/dist/commands/explore.js +264 -0
- package/dist/commands/help.js +2418 -0
- package/dist/commands/init.js +408 -0
- package/dist/commands/install.js +129 -0
- package/dist/commands/library.js +229 -0
- package/dist/commands/marketplace.js +138 -0
- package/dist/commands/mcp-install.js +476 -0
- package/dist/commands/mention.js +257 -0
- package/dist/commands/omni.js +529 -0
- package/dist/commands/secrets.js +323 -0
- package/dist/commands/self-update.js +206 -0
- package/dist/commands/sheet.js +264 -0
- package/dist/commands/shell-output.js +309 -0
- package/dist/commands/shell.js +1048 -0
- package/dist/commands/shop.js +368 -0
- package/dist/commands/skills.js +311 -0
- package/dist/commands/slides.js +418 -0
- package/dist/commands/swarm.js +341 -0
- package/dist/commands/ticket.js +1301 -0
- package/dist/commands/tool.js +312 -0
- package/dist/commands/trash.js +106 -0
- package/dist/commands/treehouse.js +120 -0
- package/dist/commands/user.js +240 -0
- package/dist/commands/voice-tts.js +319 -0
- package/dist/commands/voice.js +166 -0
- package/dist/commands/watch-git.js +401 -0
- package/dist/commands/webhook.js +159 -0
- package/dist/commands/workspace.js +3354 -0
- package/dist/composition-repos.js +91 -0
- package/dist/daemon-avatar.js +130 -0
- package/dist/daemon-claude-code-auto-install.js +52 -0
- package/dist/daemon-client.js +363 -0
- package/dist/daemon-codex.js +349 -0
- package/dist/daemon-entry.js +101 -0
- package/dist/daemon-fetch-allowlist.js +194 -0
- package/dist/daemon-gemini.js +224 -0
- package/dist/daemon-mcp-clearly.js +353 -0
- package/dist/daemon-mcp-sandbox.js +127 -0
- package/dist/daemon-omni-company.js +338 -0
- package/dist/daemon-omni-hooks.js +375 -0
- package/dist/daemon-omni-identity.js +229 -0
- package/dist/daemon-omni-pipeline.js +251 -0
- package/dist/daemon-omni-policies.js +346 -0
- package/dist/daemon-omni-principal.js +580 -0
- package/dist/daemon-omni-tool-gate.js +116 -0
- package/dist/daemon-omni.js +1442 -0
- package/dist/daemon-path-transform.js +120 -0
- package/dist/daemon-write-sandbox.js +191 -0
- package/dist/daemon.js +4471 -0
- package/dist/dashboard.js +846 -0
- package/dist/dep-installer.js +286 -0
- package/dist/dev-mode.js +67 -0
- package/dist/doc-indexer.js +169 -0
- package/dist/e2e-crypto.js +94 -0
- package/dist/feature-flags.js +154 -0
- package/dist/file-browser.js +88 -0
- package/dist/first-party-hives.js +211 -0
- package/dist/folder-sync.js +226 -0
- package/dist/health.js +56 -0
- package/dist/ipc-auth.js +299 -0
- package/dist/ipc-client.js +495 -0
- package/dist/ipc-server.js +684 -0
- package/dist/keychain.js +224 -0
- package/dist/lib/admin-client.js +73 -0
- package/dist/lib/mobile-build-parser.js +156 -0
- package/dist/local-app-manager.js +660 -0
- package/dist/log-redaction.js +89 -0
- package/dist/logger.js +108 -0
- package/dist/manifest.js +143 -0
- package/dist/marketplace.js +793 -0
- package/dist/min-version-copy.js +51 -0
- package/dist/min-version.js +214 -0
- package/dist/public-surface.js +23 -0
- package/dist/relay-presets.js +89 -0
- package/dist/relay.js +2723 -0
- package/dist/repl.js +619 -0
- package/dist/setup-relay.js +363 -0
- package/dist/setup-service.js +536 -0
- package/dist/shared/bee-memory/index.js +6 -0
- package/dist/shared/bee-memory/types.js +682 -0
- package/dist/shared/claude-code/daemon-types.js +112 -0
- package/dist/shared/claude-code/index.js +2 -0
- package/dist/shared/claude-code/types.js +1 -0
- package/dist/shared/status/schema.js +133 -0
- package/dist/shell-session.js +198 -0
- package/dist/speak-lock.js +97 -0
- package/dist/spinner.js +57 -0
- package/dist/telemetry.js +166 -0
- package/dist/terminal/block-parser.js +207 -0
- package/dist/terminal/shell-integration.js +108 -0
- package/dist/transcript-tailer.js +215 -0
- package/dist/tts-qwen3.js +547 -0
- package/dist/types.js +4 -0
- package/dist/utm.js +38 -0
- package/dist/verb-surface.js +331 -0
- package/dist/version-utils.js +85 -0
- package/dist/watcher.js +207 -0
- package/package.json +88 -0
- package/python/qwen3_tts.py +230 -0
- package/scripts/stamp-build-channel.mjs +115 -0
- package/scripts/sync-installed.mjs +159 -0
- package/scripts/sync-skills.mjs +112 -0
- package/skills/README.md +24 -0
- package/skills/agent-doodle-space/SKILL.md +118 -0
- package/skills/agent-home/SKILL.md +98 -0
- package/skills/beehaven-cli/SKILL.md +144 -0
- package/skills/claude-shopper/README.md +90 -0
- package/skills/claude-shopper/SKILL.md +151 -0
- package/skills/clearly-agent/SKILL.md +183 -0
- package/skills/clearly-canvas/SKILL.md +222 -0
- package/skills/clearly-canvas/authoring.md +160 -0
- package/skills/clearly-canvas/communications.md +32 -0
- package/skills/clearly-canvas/composition-patterns.md +84 -0
- package/skills/clearly-canvas/example.mjs +78 -0
- package/skills/clearly-canvas/project-plans.md +156 -0
- package/skills/clearly-canvas/review.mjs +68 -0
- package/skills/clearly-workspace/SKILL.md +79 -0
- package/skills/clearly-workspace/survey.mjs +101 -0
- package/skills/codebase-map/SKILL.md +142 -0
- package/skills/design-craft/SKILL.md +300 -0
- package/skills/index.json +79 -0
- package/skills/layout-systems/SKILL.md +184 -0
- package/skills/pair-on-canvas/SKILL.md +92 -0
|
@@ -0,0 +1,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
|
+
}
|