@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,1310 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `beehaven agent <sub>` — manage per-agent identities.
|
|
3
|
+
*
|
|
4
|
+
* An agent identity lets a runtime (a terminal Claude Code session, the
|
|
5
|
+
* daemon's harness, Clearly AI) "sign in" as a distinct principal and
|
|
6
|
+
* drive its own avatar. The signing secret lives only in the daemon, so
|
|
7
|
+
* `login` / `rebind` mint tokens over IPC (`agent:mint`); this command
|
|
8
|
+
* just stores the resulting record under ~/.beehaven/agents/<name>.json.
|
|
9
|
+
*
|
|
10
|
+
* Subcommands:
|
|
11
|
+
* login <name> [--label <work>] [--client <client>] [--avatar <id>] [--avatar-skin <skin>] [--kind <k>]
|
|
12
|
+
* use <name> — prints `export BEEHAVEN_AGENT=<name>` (eval it)
|
|
13
|
+
* list — list stored identities, mark the active one
|
|
14
|
+
* whoami — show the active identity
|
|
15
|
+
* logout [<name>] — end a session, keep the identity (default: active)
|
|
16
|
+
* delete <name> — archive the current workspace projection + remove this device copy
|
|
17
|
+
* delete <name> --account --yes
|
|
18
|
+
* — delete the portable account identity (all workspaces) + this device copy
|
|
19
|
+
* rebind <name> --avatar <id> — re-bind to a different avatar
|
|
20
|
+
*
|
|
21
|
+
* Per-terminal activation is via the BEEHAVEN_AGENT env var (inherently
|
|
22
|
+
* per-tty → N windows, N agents); `--agent <name>` overrides per call.
|
|
23
|
+
*/
|
|
24
|
+
import { readFileSync, writeFileSync, mkdirSync, existsSync } from 'node:fs';
|
|
25
|
+
import { join, dirname } from 'node:path';
|
|
26
|
+
import { homedir } from 'node:os';
|
|
27
|
+
import { rpcOnce } from '../ipc-client.js';
|
|
28
|
+
import { saveAgent, loadAgent, listAgents, deleteAgent, findAgentByAvatar, isValidAgentName, resolveActiveAgent, activeAgentName, saveAgentSession, clearAgentSession, decodeAgentTokenClaims, } from '../agent-state.js';
|
|
29
|
+
const VALID_KINDS = ['claude-code', 'daemon-agent', 'clearly-ai'];
|
|
30
|
+
export const FREE_SKINS = [
|
|
31
|
+
'classic',
|
|
32
|
+
'midnight',
|
|
33
|
+
'moss',
|
|
34
|
+
'ember',
|
|
35
|
+
'tuck-classic',
|
|
36
|
+
'tuck-midnight',
|
|
37
|
+
'tuck-moss',
|
|
38
|
+
'tuck-ember',
|
|
39
|
+
'lop',
|
|
40
|
+
'smudge',
|
|
41
|
+
'blot',
|
|
42
|
+
'dab',
|
|
43
|
+
'pip',
|
|
44
|
+
];
|
|
45
|
+
export function pickRandomOwnedSkinCli(excludeSkins) {
|
|
46
|
+
const excluded = new Set(excludeSkins ?? []);
|
|
47
|
+
const unassigned = FREE_SKINS.filter((s) => !excluded.has(s));
|
|
48
|
+
if (unassigned.length > 0) {
|
|
49
|
+
return unassigned[Math.floor(Math.random() * unassigned.length)];
|
|
50
|
+
}
|
|
51
|
+
return FREE_SKINS[Math.floor(Math.random() * FREE_SKINS.length)];
|
|
52
|
+
}
|
|
53
|
+
/** Native Mwah skin ids accepted by `--avatar-skin`; the legacy `--avatar` flag also accepts
|
|
54
|
+
* these values for backwards compatibility. */
|
|
55
|
+
function normalizeCliAvatarSkin(raw) {
|
|
56
|
+
const value = raw.trim().toLowerCase();
|
|
57
|
+
if (value === 'nib')
|
|
58
|
+
return 'classic';
|
|
59
|
+
if (value === 'tuck')
|
|
60
|
+
return 'tuck-classic';
|
|
61
|
+
return FREE_SKINS.includes(value) ? value : null;
|
|
62
|
+
}
|
|
63
|
+
/** End the workspace session before removing its local handle. Closing the daemon's presence
|
|
64
|
+
* socket alone cannot update the Durable Object row, so logout used to leave an `active` session
|
|
65
|
+
* behind until the server's idle sweep noticed it. Best-effort keeps offline logout usable; the
|
|
66
|
+
* 30-minute server reaper is the fallback when the relay is unavailable. */
|
|
67
|
+
async function endWorkspaceSession(record, summary) {
|
|
68
|
+
const sessionId = String(record?.sessionId ?? '').trim();
|
|
69
|
+
if (!sessionId)
|
|
70
|
+
return false;
|
|
71
|
+
try {
|
|
72
|
+
const response = await rpcOnce('agent-session-end', {
|
|
73
|
+
id: sessionId,
|
|
74
|
+
...(record?.agentId ? { agentId: record.agentId } : {}),
|
|
75
|
+
summary,
|
|
76
|
+
}, 5_000);
|
|
77
|
+
const result = response?.result ?? response;
|
|
78
|
+
return result?.ok !== false && !result?.error;
|
|
79
|
+
}
|
|
80
|
+
catch {
|
|
81
|
+
return false;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
const dim = (s) => `\x1b[2m${s}\x1b[0m`;
|
|
85
|
+
const green = (s) => `\x1b[32m${s}\x1b[0m`;
|
|
86
|
+
const bold = (s) => `\x1b[1m${s}\x1b[0m`;
|
|
87
|
+
const cyan = (s) => `\x1b[36m${s}\x1b[0m`;
|
|
88
|
+
const yellow = (s) => `\x1b[33m${s}\x1b[0m`;
|
|
89
|
+
export async function cmdAgentSub(positionals, values) {
|
|
90
|
+
// positionals[0] === 'agent'; positionals[1] === subcommand
|
|
91
|
+
const sub = positionals[1];
|
|
92
|
+
const rest = positionals.slice(2);
|
|
93
|
+
switch (sub) {
|
|
94
|
+
case 'help':
|
|
95
|
+
return printAgentUsage();
|
|
96
|
+
case 'login':
|
|
97
|
+
return agentLogin(rest, values);
|
|
98
|
+
case 'use':
|
|
99
|
+
return agentUse(rest);
|
|
100
|
+
case 'list':
|
|
101
|
+
case 'ls':
|
|
102
|
+
return agentList(values);
|
|
103
|
+
case 'whoami':
|
|
104
|
+
return agentWhoami(values);
|
|
105
|
+
case 'status':
|
|
106
|
+
return agentStatus(rest, values);
|
|
107
|
+
case 'logout':
|
|
108
|
+
return agentLogout(rest, values);
|
|
109
|
+
case 'delete':
|
|
110
|
+
case 'remove':
|
|
111
|
+
return agentDelete(rest, values);
|
|
112
|
+
case 'rebind':
|
|
113
|
+
return agentRebind(rest, values);
|
|
114
|
+
case 'soul':
|
|
115
|
+
return agentSoulCommand(rest, values);
|
|
116
|
+
case 'doodle-space':
|
|
117
|
+
return agentDoodleSpaceCommand(rest, values);
|
|
118
|
+
case 'home':
|
|
119
|
+
case 'agent-home':
|
|
120
|
+
return agentHomeCommand(rest, values);
|
|
121
|
+
default:
|
|
122
|
+
console.error(` Unknown agent subcommand: ${sub ?? '(none)'}`);
|
|
123
|
+
printAgentUsage();
|
|
124
|
+
process.exit(1);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
/** Whether positionals[1] is an identity subcommand (vs. a mwah message). */
|
|
128
|
+
export function isAgentIdentitySub(sub) {
|
|
129
|
+
return (sub === 'login' ||
|
|
130
|
+
sub === 'help' ||
|
|
131
|
+
sub === 'use' ||
|
|
132
|
+
sub === 'list' ||
|
|
133
|
+
sub === 'ls' ||
|
|
134
|
+
sub === 'whoami' ||
|
|
135
|
+
sub === 'status' ||
|
|
136
|
+
sub === 'logout' ||
|
|
137
|
+
sub === 'delete' ||
|
|
138
|
+
sub === 'remove' ||
|
|
139
|
+
sub === 'rebind' ||
|
|
140
|
+
sub === 'soul' ||
|
|
141
|
+
sub === 'doodle-space'
|
|
142
|
+
|| sub === 'home' ||
|
|
143
|
+
sub === 'agent-home');
|
|
144
|
+
}
|
|
145
|
+
function printAgentUsage() {
|
|
146
|
+
console.log('');
|
|
147
|
+
console.log(' Usage: beehaven agent <command>');
|
|
148
|
+
console.log('');
|
|
149
|
+
console.log(' Identity and work-session workflow:');
|
|
150
|
+
console.log(' beehaven env');
|
|
151
|
+
console.log(' beehaven connect home');
|
|
152
|
+
console.log(' beehaven agent login <name> --label "<work>" --client cli');
|
|
153
|
+
console.log('');
|
|
154
|
+
console.log(' login mints or refreshes the account-level agent identity, binds this');
|
|
155
|
+
console.log(' terminal, starts ONE workspace work session, and prints its brief.');
|
|
156
|
+
console.log(' Do not immediately call `agent-login`: that RPC is the advanced alias');
|
|
157
|
+
console.log(' of `agent-session-start` and would create another session.');
|
|
158
|
+
console.log('');
|
|
159
|
+
console.log(' login <name> [--label <work>] [--client <client>] [--avatar <id>] [--avatar-skin <skin>] [--kind <k>]');
|
|
160
|
+
console.log(' use <name> activate in this terminal (eval the output)');
|
|
161
|
+
console.log(' list list stored identities');
|
|
162
|
+
console.log(' whoami show the active identity');
|
|
163
|
+
console.log(' status [<name>] --message <text> set the persistent desktop status label');
|
|
164
|
+
console.log(' logout [<name>] end the session, KEEP the identity (default: active)');
|
|
165
|
+
console.log(' delete <name> archive this workspace projection + remove this device copy');
|
|
166
|
+
console.log(' delete <name> --account --yes delete the portable account identity + this device copy');
|
|
167
|
+
console.log(' rebind <name> --avatar <id> re-bind to a different avatar');
|
|
168
|
+
console.log(' soul get [--agent <name>] read the opt-in Avatar Soul + baseline');
|
|
169
|
+
console.log(' soul update --svg <svg|@file> [--state <state>]');
|
|
170
|
+
console.log(' soul draft get/save read or save a freeform pre-mint draft');
|
|
171
|
+
console.log(' doodle-space get [--session <id>]');
|
|
172
|
+
console.log(' doodle-space draw --svg <svg|@file> [--session <id>] [--title <t>]');
|
|
173
|
+
console.log(' home get [--json] open the persistent grid apartment');
|
|
174
|
+
console.log(' home layout --operations <json> arrange rooms, doors, windows, and furniture');
|
|
175
|
+
console.log(' home memory --value <text> [--key <k>] place a durable memory object');
|
|
176
|
+
console.log(' home journal --entry <text> add a private journal object');
|
|
177
|
+
console.log(' home ad --title <text> [--tagline <t>] create a mini-ad in the Doodle Studio');
|
|
178
|
+
console.log(' home avatar --room <id> --gx <n> --gy <n> move yourself inside the Home');
|
|
179
|
+
console.log(' (requires AGENT_SOUL_SPACE on staging)');
|
|
180
|
+
console.log('');
|
|
181
|
+
}
|
|
182
|
+
function actionPayload(raw) {
|
|
183
|
+
return raw?.result ?? raw;
|
|
184
|
+
}
|
|
185
|
+
function svgArgument(raw) {
|
|
186
|
+
const value = String(raw ?? '').trim();
|
|
187
|
+
if (!value) {
|
|
188
|
+
console.error(' --svg is required. Pass inline SVG or @/path/to/drawing.svg.');
|
|
189
|
+
process.exit(1);
|
|
190
|
+
}
|
|
191
|
+
if (!value.startsWith('@'))
|
|
192
|
+
return value;
|
|
193
|
+
try {
|
|
194
|
+
return readFileSync(value.slice(1), 'utf8').trim();
|
|
195
|
+
}
|
|
196
|
+
catch (err) {
|
|
197
|
+
console.error(` Could not read SVG file ${value.slice(1)}: ${err?.message ?? err}`);
|
|
198
|
+
process.exit(1);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
function soulAgentId(values) {
|
|
202
|
+
const id = String(values.agent ?? '').trim();
|
|
203
|
+
return id || undefined;
|
|
204
|
+
}
|
|
205
|
+
function actionNumber(values, key) {
|
|
206
|
+
const raw = values[key];
|
|
207
|
+
if (raw === undefined || raw === null || String(raw).trim() === '')
|
|
208
|
+
return undefined;
|
|
209
|
+
const value = Number(raw);
|
|
210
|
+
if (!Number.isFinite(value)) {
|
|
211
|
+
console.error(` --${key} must be a finite number.`);
|
|
212
|
+
process.exit(1);
|
|
213
|
+
}
|
|
214
|
+
return value;
|
|
215
|
+
}
|
|
216
|
+
function printSoulAction(raw, fallback, asJson = false) {
|
|
217
|
+
const payload = actionPayload(raw);
|
|
218
|
+
if (payload?.ok === false || payload?.error || raw?.error) {
|
|
219
|
+
console.error(` ${payload?.error ?? raw?.error ?? fallback}`);
|
|
220
|
+
process.exit(1);
|
|
221
|
+
}
|
|
222
|
+
if (asJson)
|
|
223
|
+
console.log(JSON.stringify(payload, null, 2));
|
|
224
|
+
else if (payload?.compositionId)
|
|
225
|
+
console.log(` ${green('✓')} ${fallback}: ${cyan(String(payload.compositionId))}`);
|
|
226
|
+
else if (payload?.nodeId)
|
|
227
|
+
console.log(` ${green('✓')} ${fallback}: ${cyan(String(payload.nodeId))}`);
|
|
228
|
+
else
|
|
229
|
+
console.log(JSON.stringify(payload, null, 2));
|
|
230
|
+
}
|
|
231
|
+
async function agentSoulCommand(args, values) {
|
|
232
|
+
const op = args[0] ?? 'get';
|
|
233
|
+
if (op === 'help') {
|
|
234
|
+
console.log(' Usage: beehaven agent soul get [--agent <name>]');
|
|
235
|
+
console.log(' beehaven agent soul update --svg <svg|@file> [--state <state>] [--note <text>]');
|
|
236
|
+
console.log(' beehaven agent soul draft get [--agent <name>]');
|
|
237
|
+
console.log(' beehaven agent soul draft save --svg <svg|@file> [--agent <name>]');
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
if (op === 'draft') {
|
|
241
|
+
return agentSoulDraftCommand(args.slice(1), values);
|
|
242
|
+
}
|
|
243
|
+
const agentId = soulAgentId(values);
|
|
244
|
+
if (op === 'get') {
|
|
245
|
+
const response = await rpcOnce('agent-soul-get', agentId ? { agentId } : {}, 15_000);
|
|
246
|
+
const payload = actionPayload(response);
|
|
247
|
+
if (payload?.ok === false || payload?.error || response?.error) {
|
|
248
|
+
console.error(` ${payload?.error ?? response?.error ?? 'Could not read Avatar Soul.'}`);
|
|
249
|
+
process.exit(1);
|
|
250
|
+
}
|
|
251
|
+
if (values.json)
|
|
252
|
+
console.log(JSON.stringify(payload, null, 2));
|
|
253
|
+
else {
|
|
254
|
+
console.log(` ${bold('Avatar Soul')} · ${payload.displayName ?? payload.agentId}`);
|
|
255
|
+
console.log(` ${payload.appearance ?? 'No appearance description returned.'}`);
|
|
256
|
+
console.log(` Baseline: ${payload.baseline?.svg ? 'ready' : 'missing'} · revision ${payload.revision ?? 0}`);
|
|
257
|
+
if (payload.current?.state)
|
|
258
|
+
console.log(` Current: ${payload.current.state}`);
|
|
259
|
+
if (payload.desktopAppearance) {
|
|
260
|
+
const d = payload.desktopAppearance;
|
|
261
|
+
console.log(` Desktop: ${d.effectiveMode ?? d.requestedMode ?? 'preset'}${d.agentDrawnEnabled ? ' · agent drawn enabled' : ' · preset fallback'}`);
|
|
262
|
+
}
|
|
263
|
+
console.log(dim(' Use --json when you need the full SVG and soul record.'));
|
|
264
|
+
}
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
267
|
+
if (op === 'update') {
|
|
268
|
+
const svg = svgArgument(values.svg);
|
|
269
|
+
const response = await rpcOnce('agent-soul-avatar-update', {
|
|
270
|
+
...(agentId ? { agentId } : {}),
|
|
271
|
+
svg,
|
|
272
|
+
...(values.state ? { state: String(values.state) } : {}),
|
|
273
|
+
...(values.note ? { note: String(values.note) } : {}),
|
|
274
|
+
...(values.session ? { sessionId: String(values.session) } : {}),
|
|
275
|
+
}, 15_000);
|
|
276
|
+
printSoulAction(response, 'Avatar Soul updated', !!values.json);
|
|
277
|
+
return;
|
|
278
|
+
}
|
|
279
|
+
console.error(` Unknown soul operation: ${op}`);
|
|
280
|
+
console.error(' Use: get | update');
|
|
281
|
+
process.exit(1);
|
|
282
|
+
}
|
|
283
|
+
async function agentSoulDraftCommand(args, values) {
|
|
284
|
+
const op = args[0] ?? 'get';
|
|
285
|
+
const agentId = soulAgentId(values);
|
|
286
|
+
if (op === 'help') {
|
|
287
|
+
console.log(' Usage: beehaven agent soul draft get [--agent <name>]');
|
|
288
|
+
console.log(' beehaven agent soul draft save --svg <svg|@file> [--agent <name>] [--appearance <text>]');
|
|
289
|
+
return;
|
|
290
|
+
}
|
|
291
|
+
if (op === 'get') {
|
|
292
|
+
const response = await rpcOnce('agent-soul-draft-get', agentId ? { agentId } : {}, 15_000);
|
|
293
|
+
const payload = actionPayload(response);
|
|
294
|
+
if (payload?.ok === false || payload?.error || response?.error) {
|
|
295
|
+
console.error(` ${payload?.error ?? response?.error ?? 'Could not read the Avatar Soul draft.'}`);
|
|
296
|
+
process.exit(1);
|
|
297
|
+
}
|
|
298
|
+
if (values.json)
|
|
299
|
+
console.log(JSON.stringify(payload, null, 2));
|
|
300
|
+
else if (payload?.draft) {
|
|
301
|
+
console.log(` ${bold('Avatar Soul draft')} · ${payload.agentId}`);
|
|
302
|
+
console.log(` ${payload.draft.appearance ?? 'Freeform drawing with no description yet.'}`);
|
|
303
|
+
console.log(` SVG: ready · can seed baseline: ${payload.canSeedBaseline ? 'yes' : 'no'}`);
|
|
304
|
+
}
|
|
305
|
+
else {
|
|
306
|
+
console.log(` No pre-mint Avatar Soul draft for ${payload.agentId}.`);
|
|
307
|
+
}
|
|
308
|
+
return;
|
|
309
|
+
}
|
|
310
|
+
if (op === 'save') {
|
|
311
|
+
const svg = svgArgument(values.svg);
|
|
312
|
+
const response = await rpcOnce('agent-soul-draft-save', {
|
|
313
|
+
...(agentId ? { agentId } : {}),
|
|
314
|
+
svg,
|
|
315
|
+
...(values['display-name'] ? { displayName: String(values['display-name']) } : {}),
|
|
316
|
+
...(values.appearance ? { appearance: String(values.appearance) } : {}),
|
|
317
|
+
...(values.note ? { note: String(values.note) } : {}),
|
|
318
|
+
}, 15_000);
|
|
319
|
+
printSoulAction(response, 'Avatar Soul draft saved', !!values.json);
|
|
320
|
+
return;
|
|
321
|
+
}
|
|
322
|
+
console.error(` Unknown soul draft operation: ${op}`);
|
|
323
|
+
console.error(' Use: get | save');
|
|
324
|
+
process.exit(1);
|
|
325
|
+
}
|
|
326
|
+
async function agentDoodleSpaceCommand(args, values) {
|
|
327
|
+
const op = args[0] ?? 'get';
|
|
328
|
+
const agentId = soulAgentId(values);
|
|
329
|
+
if (op === 'help') {
|
|
330
|
+
console.log(' Usage: beehaven agent doodle-space get [--session <id>]');
|
|
331
|
+
console.log(' beehaven agent doodle-space draw --svg <svg|@file> [--session <id>] [--title <text>]');
|
|
332
|
+
return;
|
|
333
|
+
}
|
|
334
|
+
if (op === 'get') {
|
|
335
|
+
const response = await rpcOnce('agent-doodle-space-get', {
|
|
336
|
+
...(agentId ? { agentId } : {}),
|
|
337
|
+
...(values.session ? { sessionId: String(values.session) } : {}),
|
|
338
|
+
...(values.title ? { title: String(values.title) } : {}),
|
|
339
|
+
}, 15_000);
|
|
340
|
+
const payload = actionPayload(response);
|
|
341
|
+
if (payload?.ok === false || payload?.error || response?.error) {
|
|
342
|
+
console.error(` ${payload?.error ?? response?.error ?? 'Could not open Doodle Space.'}`);
|
|
343
|
+
process.exit(1);
|
|
344
|
+
}
|
|
345
|
+
if (values.json)
|
|
346
|
+
console.log(JSON.stringify(payload, null, 2));
|
|
347
|
+
else if (payload?.compositionId) {
|
|
348
|
+
console.log(` ${green('✓')} Doodle Space: ${cyan(String(payload.compositionId))}`);
|
|
349
|
+
if (payload.viewBox)
|
|
350
|
+
console.log(` ViewBox: ${payload.viewBox}`);
|
|
351
|
+
if (payload.latestDoodle?.svg)
|
|
352
|
+
console.log(` Latest drawing: ready · ${payload.desktopAppearance?.doodleBannerEnabled === false ? 'banner hidden' : 'banner available'}`);
|
|
353
|
+
}
|
|
354
|
+
else
|
|
355
|
+
console.log(` No Doodle Space exists for session ${payload?.sessionId ?? 'default'}.`);
|
|
356
|
+
return;
|
|
357
|
+
}
|
|
358
|
+
if (op === 'draw') {
|
|
359
|
+
const svg = svgArgument(values.svg);
|
|
360
|
+
const x = actionNumber(values, 'x');
|
|
361
|
+
const y = actionNumber(values, 'y');
|
|
362
|
+
const w = actionNumber(values, 'w');
|
|
363
|
+
const h = actionNumber(values, 'h');
|
|
364
|
+
const response = await rpcOnce('agent-doodle-space-draw', {
|
|
365
|
+
...(agentId ? { agentId } : {}),
|
|
366
|
+
svg,
|
|
367
|
+
...(values.session ? { sessionId: String(values.session) } : {}),
|
|
368
|
+
...(values.title ? { title: String(values.title) } : {}),
|
|
369
|
+
...(values.state ? { state: String(values.state) } : {}),
|
|
370
|
+
...(values.note ? { note: String(values.note) } : {}),
|
|
371
|
+
...(x !== undefined ? { x } : {}),
|
|
372
|
+
...(y !== undefined ? { y } : {}),
|
|
373
|
+
...(w !== undefined ? { w } : {}),
|
|
374
|
+
...(h !== undefined ? { h } : {}),
|
|
375
|
+
}, 15_000);
|
|
376
|
+
printSoulAction(response, 'Doodle added', !!values.json);
|
|
377
|
+
return;
|
|
378
|
+
}
|
|
379
|
+
console.error(` Unknown doodle-space operation: ${op}`);
|
|
380
|
+
console.error(' Use: get | draw');
|
|
381
|
+
process.exit(1);
|
|
382
|
+
}
|
|
383
|
+
function jsonArgument(raw, label) {
|
|
384
|
+
const value = String(raw ?? '').trim();
|
|
385
|
+
if (!value) {
|
|
386
|
+
console.error(` --${label} is required.`);
|
|
387
|
+
process.exit(1);
|
|
388
|
+
}
|
|
389
|
+
try {
|
|
390
|
+
return JSON.parse(value);
|
|
391
|
+
}
|
|
392
|
+
catch (err) {
|
|
393
|
+
console.error(` --${label} must be valid JSON: ${err?.message ?? err}`);
|
|
394
|
+
process.exit(1);
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
async function agentHomeCommand(args, values) {
|
|
398
|
+
const op = args[0] ?? 'get';
|
|
399
|
+
const agentId = soulAgentId(values);
|
|
400
|
+
const call = async (action, input) => {
|
|
401
|
+
const response = await rpcOnce(action, { ...(agentId ? { agentId } : {}), ...input }, 20_000);
|
|
402
|
+
const payload = actionPayload(response);
|
|
403
|
+
if (payload?.ok === false || payload?.error || response?.error) {
|
|
404
|
+
console.error(` ${payload?.error ?? response?.error ?? `Could not run ${action}.`}`);
|
|
405
|
+
process.exit(1);
|
|
406
|
+
}
|
|
407
|
+
if (values.json)
|
|
408
|
+
console.log(JSON.stringify(payload, null, 2));
|
|
409
|
+
else {
|
|
410
|
+
const id = payload?.compositionId || payload?.memoryId || payload?.id;
|
|
411
|
+
console.log(` ${green('✓')} ${action}${id ? `: ${cyan(String(id))}` : ''}`);
|
|
412
|
+
if (payload?.grid?.unit)
|
|
413
|
+
console.log(` Grid: ${payload.grid.unit}px · ${payload.grid.columns}×${payload.grid.rows} cells`);
|
|
414
|
+
if (payload?.roomId)
|
|
415
|
+
console.log(` Room: ${payload.roomId}`);
|
|
416
|
+
if (payload?.revision != null)
|
|
417
|
+
console.log(` Revision: ${payload.revision}`);
|
|
418
|
+
}
|
|
419
|
+
};
|
|
420
|
+
if (op === 'help') {
|
|
421
|
+
console.log(' Usage: beehaven agent home get [--agent <name>]');
|
|
422
|
+
console.log(' beehaven agent home layout --operations <json> [--revision <n>]');
|
|
423
|
+
console.log(' beehaven agent home memory --value <text> [--key <key>] [--room <id>]');
|
|
424
|
+
console.log(' beehaven agent home journal --entry <text> [--title <text>] [--room <id>]');
|
|
425
|
+
console.log(' beehaven agent home ad --title <text> [--tagline <text>]');
|
|
426
|
+
console.log(' beehaven agent home avatar --room <id> --gx <n> --gy <n>');
|
|
427
|
+
return;
|
|
428
|
+
}
|
|
429
|
+
if (op === 'get') {
|
|
430
|
+
await call('agent-home-get', {
|
|
431
|
+
...(values.title ? { title: String(values.title) } : {}),
|
|
432
|
+
...(values.project ? { projectId: String(values.project) } : {}),
|
|
433
|
+
...(values.create !== undefined ? { create: values.create !== 'false' } : {}),
|
|
434
|
+
});
|
|
435
|
+
return;
|
|
436
|
+
}
|
|
437
|
+
if (op === 'layout') {
|
|
438
|
+
const revision = actionNumber(values, 'revision');
|
|
439
|
+
await call('agent-home-layout', {
|
|
440
|
+
operations: jsonArgument(values.operations, 'operations'),
|
|
441
|
+
...(revision !== undefined ? { expectedRevision: revision } : {}),
|
|
442
|
+
});
|
|
443
|
+
return;
|
|
444
|
+
}
|
|
445
|
+
if (op === 'memory') {
|
|
446
|
+
await call('agent-home-memory', {
|
|
447
|
+
value: String(values.value ?? ''),
|
|
448
|
+
...(values.key ? { key: String(values.key) } : {}),
|
|
449
|
+
...(values.room ? { roomId: String(values.room) } : {}),
|
|
450
|
+
...(values.category ? { category: String(values.category) } : {}),
|
|
451
|
+
...(values.visibility ? { visibility: String(values.visibility) } : {}),
|
|
452
|
+
...(['gx', 'gy', 'gw', 'gh'].reduce((out, key) => { const n = actionNumber(values, key); if (n !== undefined)
|
|
453
|
+
out[key] = n; return out; }, {})),
|
|
454
|
+
});
|
|
455
|
+
return;
|
|
456
|
+
}
|
|
457
|
+
if (op === 'journal') {
|
|
458
|
+
await call('agent-home-journal', {
|
|
459
|
+
entry: String(values.entry ?? ''),
|
|
460
|
+
...(values.title ? { title: String(values.title) } : {}),
|
|
461
|
+
...(values.key ? { key: String(values.key) } : {}),
|
|
462
|
+
...(values.room ? { roomId: String(values.room) } : {}),
|
|
463
|
+
});
|
|
464
|
+
return;
|
|
465
|
+
}
|
|
466
|
+
if (op === 'ad') {
|
|
467
|
+
await call('agent-home-ad', {
|
|
468
|
+
...(values.title ? { title: String(values.title) } : {}),
|
|
469
|
+
...(values.tagline ? { tagline: String(values.tagline) } : {}),
|
|
470
|
+
...(values.accent ? { accent: String(values.accent) } : {}),
|
|
471
|
+
...(values.room ? { roomId: String(values.room) } : {}),
|
|
472
|
+
...(values.svg ? { svg: svgArgument(values.svg) } : {}),
|
|
473
|
+
});
|
|
474
|
+
return;
|
|
475
|
+
}
|
|
476
|
+
if (op === 'avatar') {
|
|
477
|
+
const gx = actionNumber(values, 'gx');
|
|
478
|
+
const gy = actionNumber(values, 'gy');
|
|
479
|
+
await call('agent-home-avatar-location', { ...(values.room ? { roomId: String(values.room) } : {}), ...(gx !== undefined ? { gx } : {}), ...(gy !== undefined ? { gy } : {}) });
|
|
480
|
+
return;
|
|
481
|
+
}
|
|
482
|
+
console.error(` Unknown home operation: ${op}`);
|
|
483
|
+
console.error(' Use: get | layout | memory | journal | ad | avatar');
|
|
484
|
+
process.exit(1);
|
|
485
|
+
}
|
|
486
|
+
// ── login ────────────────────────────────────────────
|
|
487
|
+
async function agentLogin(args, values) {
|
|
488
|
+
const name = args[0];
|
|
489
|
+
if (!name) {
|
|
490
|
+
console.error(' Usage: beehaven agent login <name> [--label <work>] [--client <client>] [--avatar <id>] [--avatar-skin <skin>] [--kind <k>]');
|
|
491
|
+
process.exit(1);
|
|
492
|
+
}
|
|
493
|
+
if (!isValidAgentName(name)) {
|
|
494
|
+
console.error(` Invalid agent name "${name}" — use letters, digits, '.', '_', '-' (max 64).`);
|
|
495
|
+
process.exit(1);
|
|
496
|
+
}
|
|
497
|
+
const requestedAvatarId = String(values.avatar ?? '').trim();
|
|
498
|
+
const requestedSkinRaw = String(values['avatar-skin'] ?? '').trim();
|
|
499
|
+
const requestedSkin = requestedSkinRaw
|
|
500
|
+
? normalizeCliAvatarSkin(requestedSkinRaw)
|
|
501
|
+
: (requestedAvatarId ? normalizeCliAvatarSkin(requestedAvatarId) : null);
|
|
502
|
+
if (requestedSkinRaw && !requestedSkin) {
|
|
503
|
+
console.error(` Unknown avatar skin "${requestedSkinRaw}". Use: ${FREE_SKINS.join(', ')}`);
|
|
504
|
+
process.exit(1);
|
|
505
|
+
}
|
|
506
|
+
const existingRecord = loadAgent(name);
|
|
507
|
+
const kind = VALID_KINDS.includes(values.kind) ? values.kind : 'claude-code';
|
|
508
|
+
const displayName = String(values['display-name'] ?? name);
|
|
509
|
+
// Optional voice binding: --agent-voice <provider[:voiceId]> (e.g. openai:sage,
|
|
510
|
+
// kokoro:claudette, elevenlabs:EXAVITQu4vr4xnSDxMaL). Bound to the identity
|
|
511
|
+
// so this agent's avatar speaks in its own voice — carried in the token's
|
|
512
|
+
// (non-secret) meta and hydrated by the avatar building on each call.
|
|
513
|
+
/**
|
|
514
|
+
* ⚠ `--agent-voice` IS THE REAL FLAG; `--voice` IS KEPT ONLY AS A TOLERATED ALIAS.
|
|
515
|
+
* See the declaration in cli.ts — `--voice` is a boolean (push-to-talk), so the
|
|
516
|
+
* string branch here is unreachable through normal parsing and exists purely so a
|
|
517
|
+
* programmatic caller passing `{ voice: 'qwen3-mlx:ethan' }` still works.
|
|
518
|
+
*/
|
|
519
|
+
const voiceRaw = typeof values['agent-voice'] === 'string' && values['agent-voice'].trim()
|
|
520
|
+
? String(values['agent-voice']).trim()
|
|
521
|
+
: (typeof values.voice === 'string' ? values.voice.trim() : '');
|
|
522
|
+
let voice;
|
|
523
|
+
if (voiceRaw) {
|
|
524
|
+
const { parseVoiceBinding, isKnownVoiceProvider, VOICE_CHOICES } = await import('../agent-onboard.js');
|
|
525
|
+
const parsed = parseVoiceBinding(voiceRaw);
|
|
526
|
+
/**
|
|
527
|
+
* ⚠⚠ A MISSPELLED PROVIDER USED TO BIND SILENTLY AND FAIL AT SPEAK TIME.
|
|
528
|
+
*
|
|
529
|
+
* This took any string, so `--voice kokoru:claudette` minted a perfectly valid
|
|
530
|
+
* identity whose voice named an engine that does not exist. Nothing complained
|
|
531
|
+
* until the agent spoke — and even then the avatar fell through to its default
|
|
532
|
+
* provider, so the symptom was "my agent sounds wrong", noticed much later,
|
|
533
|
+
* with the cause sitting in a token nobody re-reads.
|
|
534
|
+
*
|
|
535
|
+
* ⚠ THE VOICE ID IS DELIBERATELY NOT VALIDATED. Valid ids differ per provider,
|
|
536
|
+
* change over time (ElevenLabs ids are account-scoped) and include names this
|
|
537
|
+
* file cannot know — a Qwen3 cloned-voice profile is created by the user after
|
|
538
|
+
* login. The provider is a closed set; the id is not.
|
|
539
|
+
*/
|
|
540
|
+
if (!parsed || !isKnownVoiceProvider(parsed.provider)) {
|
|
541
|
+
console.error(` Unknown voice provider "${parsed?.provider ?? voiceRaw}".`);
|
|
542
|
+
console.error(` Expected one of: ${VOICE_CHOICES.map((v) => v.provider).join(' | ')}`);
|
|
543
|
+
console.error(` Example: beehaven agent login ${name} --agent-voice qwen3-mlx:ethan`);
|
|
544
|
+
process.exit(1);
|
|
545
|
+
}
|
|
546
|
+
voice = parsed;
|
|
547
|
+
}
|
|
548
|
+
// Determine the persisted Mwah skin before minting so the account registry and this device use
|
|
549
|
+
// the same choice. A new identity uses its selected/auto-assigned skin as its avatar id; a
|
|
550
|
+
// re-login preserves the identity's existing routing id unless the caller explicitly changes it.
|
|
551
|
+
let chosenSkin = requestedSkin || existingRecord?.avatarSkin;
|
|
552
|
+
if (!chosenSkin) {
|
|
553
|
+
const boundSkins = new Set();
|
|
554
|
+
for (const { name: otherName, record: otherRecord } of listAgents()) {
|
|
555
|
+
if (otherName === name)
|
|
556
|
+
continue;
|
|
557
|
+
if (otherRecord.avatarSkin)
|
|
558
|
+
boundSkins.add(otherRecord.avatarSkin);
|
|
559
|
+
else if (otherRecord.avatarId && FREE_SKINS.includes(otherRecord.avatarId)) {
|
|
560
|
+
boundSkins.add(otherRecord.avatarId);
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
try {
|
|
564
|
+
const p = join(homedir(), 'Library', 'Application Support', 'clearly-desktop', 'config.json');
|
|
565
|
+
if (existsSync(p)) {
|
|
566
|
+
const c = JSON.parse(readFileSync(p, 'utf8'));
|
|
567
|
+
for (const [k, v] of Object.entries(c)) {
|
|
568
|
+
if (k.startsWith('avatarSkin_') && typeof v === 'string')
|
|
569
|
+
boundSkins.add(v);
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
catch { }
|
|
574
|
+
chosenSkin = pickRandomOwnedSkinCli(boundSkins);
|
|
575
|
+
}
|
|
576
|
+
const avatarId = requestedAvatarId || existingRecord?.avatarId || chosenSkin || name;
|
|
577
|
+
// Local-first avatar-collision guard (durable guard is the cloud pool).
|
|
578
|
+
const collision = findAgentByAvatar(avatarId, name);
|
|
579
|
+
if (collision) {
|
|
580
|
+
console.error(` Avatar "${avatarId}" is already claimed by agent "${collision}".`);
|
|
581
|
+
console.error(` Pick another: beehaven agent login ${name} --avatar <other-id>`);
|
|
582
|
+
process.exit(1);
|
|
583
|
+
}
|
|
584
|
+
let minted = null;
|
|
585
|
+
let sessionOpen = false;
|
|
586
|
+
/**
|
|
587
|
+
* ⚠⚠ ACCOUNT-LEVEL AND CLOUD-ONLY. THE LOCAL MINT FALLBACK IS GONE, DELIBERATELY.
|
|
588
|
+
*
|
|
589
|
+
* This used to try `agent-mint-token` (which resolves the agent in whatever WORKSPACE the call
|
|
590
|
+
* landed on) and, on any failure, fall through to a LOCAL `agent:mint`. That fallback is what
|
|
591
|
+
* made "✓ Signed in" a lie: the local token is not cloud-verifiable, so the workspace answered
|
|
592
|
+
* `unknown agent` on every subsequent call while the CLI insisted you were logged in. Measured
|
|
593
|
+
* on a workspace with no agents — login reported success and a session, and nothing worked.
|
|
594
|
+
*
|
|
595
|
+
* An identity now comes from `agent-account-mint`: cloud-signed, owned by the ACCOUNT, recorded
|
|
596
|
+
* in the D1 `AgentRegistry` the SDK already describes as *"sign in anywhere, get the same
|
|
597
|
+
* agent"*. It creates no workspace row and spends no agent slot — joining a workspace is
|
|
598
|
+
* `agent-admit`, which `beehaven connect` performs.
|
|
599
|
+
*
|
|
600
|
+
* ⚠ A FAILURE HERE IS FATAL AND SAYS WHY. Half a login is worse than none: it is the state that
|
|
601
|
+
* costs someone an afternoon, because every symptom points at the workspace rather than at the
|
|
602
|
+
* credential.
|
|
603
|
+
*/
|
|
604
|
+
try {
|
|
605
|
+
const cloud = await rpcOnce('agent-account-mint', { agentId: name, avatarId }, 15_000);
|
|
606
|
+
const tok = cloud?.token ?? cloud?.result?.token;
|
|
607
|
+
if (tok) {
|
|
608
|
+
minted = {
|
|
609
|
+
token: tok,
|
|
610
|
+
agentId: cloud.agentId ?? cloud.result?.agentId ?? name,
|
|
611
|
+
avatarId: cloud.avatarId ?? cloud.result?.avatarId ?? avatarId,
|
|
612
|
+
displayName: cloud.displayName ?? cloud.result?.displayName ?? displayName,
|
|
613
|
+
kind,
|
|
614
|
+
// Full context injected on login — the agent's memory bundle
|
|
615
|
+
// (pinned + top memories, agent-scoped + shared) rides in here.
|
|
616
|
+
memory: cloud.memory ?? cloud.result?.memory,
|
|
617
|
+
// Opt-in Avatar Soul capability. The server omits this entirely while
|
|
618
|
+
// AGENT_SOUL_SPACE is disabled, so the login message never implies a
|
|
619
|
+
// second avatar system is available when the flag is off.
|
|
620
|
+
soul: cloud.soul ?? cloud.result?.soul,
|
|
621
|
+
};
|
|
622
|
+
try {
|
|
623
|
+
const conn = await rpcOnce('agent:connect', { agentId: minted.agentId, token: tok }, 15_000);
|
|
624
|
+
sessionOpen = !!(conn?.ok ?? conn?.result?.ok);
|
|
625
|
+
}
|
|
626
|
+
catch {
|
|
627
|
+
/* session is best-effort — the identity is still saved below */
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
catch (err) {
|
|
632
|
+
console.error(` Could not mint identity: ${err?.message ?? err}`);
|
|
633
|
+
console.error(' Is the daemon connected? Check with: beehaven status');
|
|
634
|
+
process.exit(1);
|
|
635
|
+
}
|
|
636
|
+
/**
|
|
637
|
+
* ⚠ NO SILENT DEGRADE. There is nothing to fall back to any more, and that is the point — see
|
|
638
|
+
* the note above. An identity that the cloud did not sign cannot be attributed, cannot be
|
|
639
|
+
* revoked from the account, and is refused by every workspace.
|
|
640
|
+
*/
|
|
641
|
+
if (!minted?.token) {
|
|
642
|
+
console.error(` Could not mint identity: ${minted?.error ?? 'the server returned no token'}`);
|
|
643
|
+
if (String(minted?.code) === 'not-configured') {
|
|
644
|
+
console.error(' The deployment is missing AGENT_IDENTITY_SECRET — this is a server-side fix.');
|
|
645
|
+
}
|
|
646
|
+
process.exit(1);
|
|
647
|
+
}
|
|
648
|
+
if (!minted?.token) {
|
|
649
|
+
console.error(` Mint failed: ${minted?.error ?? 'no token returned'}`);
|
|
650
|
+
process.exit(1);
|
|
651
|
+
}
|
|
652
|
+
/**
|
|
653
|
+
* The workspace token and the desktop token have different verifiers. The former is signed by
|
|
654
|
+
* the staging/prod worker so it can travel through workspace RPCs; the latter is signed by the
|
|
655
|
+
* local daemon's keychain secret so `app-rpc` can safely drive a local avatar. Keeping the two
|
|
656
|
+
* credentials separate prevents a cloud-signed token from being presented to a verifier that
|
|
657
|
+
* cannot know the worker's secret — the failure used to surface as AGENT_INVALID after every
|
|
658
|
+
* Mac app restart, even though `agent login` itself succeeded.
|
|
659
|
+
*/
|
|
660
|
+
let localToken;
|
|
661
|
+
try {
|
|
662
|
+
const local = await rpcOnce('agent:mint', {
|
|
663
|
+
agentId: minted.agentId,
|
|
664
|
+
kind,
|
|
665
|
+
displayName,
|
|
666
|
+
avatarId: minted.avatarId,
|
|
667
|
+
permissions: ['avatar:say', 'avatar:speak', 'avatar:emote'],
|
|
668
|
+
...(voice ? { meta: { voice } } : {}),
|
|
669
|
+
}, 15_000);
|
|
670
|
+
localToken = local?.token ?? local?.result?.token;
|
|
671
|
+
if (!localToken)
|
|
672
|
+
console.error(dim(' ⚠ local avatar credential was not returned; workspace calls remain available'));
|
|
673
|
+
}
|
|
674
|
+
catch (err) {
|
|
675
|
+
// A headless/cloud-only CLI can still use the cloud identity without a local daemon. Preserve
|
|
676
|
+
// that workflow, but make the missing local avatar leg visible instead of silently sending a
|
|
677
|
+
// token the daemon cannot verify.
|
|
678
|
+
console.error(dim(` ⚠ local avatar credential unavailable: ${err?.message ?? err}`));
|
|
679
|
+
}
|
|
680
|
+
/**
|
|
681
|
+
* ⚠ THE BIRTHDAY IS CARRIED FORWARD, NOT RE-STAMPED.
|
|
682
|
+
*
|
|
683
|
+
* `agent login` is also how an existing identity refreshes its token, so this
|
|
684
|
+
* runs over records that already exist. Taking `new Date()` unconditionally
|
|
685
|
+
* would reset every agent's age on a routine refresh — and because `issuedAt`
|
|
686
|
+
* already moves, nothing else would be left that remembers when the agent
|
|
687
|
+
* actually began. `existingRecord` is read before the write for exactly this.
|
|
688
|
+
*/
|
|
689
|
+
const birthday = existingRecord?.birthday ?? new Date().toISOString();
|
|
690
|
+
const previousSessionEnded = await endWorkspaceSession(existingRecord, 'Replaced by a new login session for this agent identity.');
|
|
691
|
+
/**
|
|
692
|
+
* ⚠⚠ FILL THE LIFETIME FIELDS FROM THE TOKEN — the cloud mint path does not
|
|
693
|
+
* return them and a record without `expiresAt` can never be seen as expired.
|
|
694
|
+
* See `decodeAgentTokenClaims` for the full failure. Envelope values win when
|
|
695
|
+
* present (the local mint returns them from verified claims); the token is the
|
|
696
|
+
* fallback, and it is the same data either way.
|
|
697
|
+
*/
|
|
698
|
+
const tokenClaims = decodeAgentTokenClaims(minted.token) ?? {};
|
|
699
|
+
const userId = minted.userId ?? tokenClaims.userId;
|
|
700
|
+
const issuedAt = minted.issuedAt ?? tokenClaims.issuedAt;
|
|
701
|
+
const expiresAt = minted.expiresAt ?? tokenClaims.expiresAt;
|
|
702
|
+
const record = {
|
|
703
|
+
agentId: minted.agentId,
|
|
704
|
+
kind: minted.kind,
|
|
705
|
+
displayName: minted.displayName,
|
|
706
|
+
avatarId: minted.avatarId,
|
|
707
|
+
...(chosenSkin ? { avatarSkin: chosenSkin } : {}),
|
|
708
|
+
userId,
|
|
709
|
+
birthday,
|
|
710
|
+
token: minted.token,
|
|
711
|
+
...(localToken ? { localToken } : {}),
|
|
712
|
+
issuedAt,
|
|
713
|
+
expiresAt,
|
|
714
|
+
...(voice ? { voice } : {}),
|
|
715
|
+
/**
|
|
716
|
+
* ⚠⚠ THE BINDINGS SURVIVE THE RE-MINT — the same rule `birthday` is carried forward for,
|
|
717
|
+
* applied to every DURABLE field rather than to one.
|
|
718
|
+
*
|
|
719
|
+
* This object is built from scratch on each login, so `boundScopes` was DESTROYED by it:
|
|
720
|
+
* log in from a second terminal and the first silently reverted to "not logged in" while
|
|
721
|
+
* holding a stored, valid, unexpired identity. Measured — a login here dropped an existing
|
|
722
|
+
* `claude-507a5e19c44a` binding on the floor.
|
|
723
|
+
*
|
|
724
|
+
* ⚠ `sessionId` too. `saveAgentSession` normally rewrites it moments later, so the loss only
|
|
725
|
+
* surfaces when the workspace is unreachable — offline, where losing the session you were
|
|
726
|
+
* resuming is least recoverable.
|
|
727
|
+
*/
|
|
728
|
+
...(existingRecord?.boundScopes ? { boundScopes: existingRecord.boundScopes } : {}),
|
|
729
|
+
...(!previousSessionEnded && existingRecord?.sessionId ? { sessionId: existingRecord.sessionId } : {}),
|
|
730
|
+
...(existingRecord?.lastLoginAt ? { lastLoginAt: existingRecord.lastLoginAt } : {}),
|
|
731
|
+
};
|
|
732
|
+
saveAgent(name, record);
|
|
733
|
+
/**
|
|
734
|
+
* ⚠⚠ THIS SESSION IS `name` FROM HERE ON, WHATEVER THE WORKSPACE SAYS NEXT.
|
|
735
|
+
*
|
|
736
|
+
* The binding used to happen only after `agent-session-start` succeeded. When the workspace
|
|
737
|
+
* refused (an archived agent), nothing was bound, and every later call resolved through the
|
|
738
|
+
* FAMILY fallback to a sibling identity — `claude` logged in, `jinri-claude` did the work. An
|
|
739
|
+
* explicit login must own its own scope, so a refused identity is refused LOUDLY on each call
|
|
740
|
+
* instead of silently replaced. The family (what un-logged-in sessions inherit) is claimed
|
|
741
|
+
* below, only once a session actually starts.
|
|
742
|
+
*/
|
|
743
|
+
const { deriveAgentScope } = await import('../agent-scope.js');
|
|
744
|
+
const { bindAgentScope, agentForScope, loginSessionOutcome } = await import('../agent-state.js');
|
|
745
|
+
const scope = deriveAgentScope();
|
|
746
|
+
bindAgentScope(name, scope, { family: false });
|
|
747
|
+
// Sync to Electron store if present
|
|
748
|
+
try {
|
|
749
|
+
const p = join(homedir(), 'Library', 'Application Support', 'clearly-desktop', 'config.json');
|
|
750
|
+
if (existsSync(p)) {
|
|
751
|
+
const c = JSON.parse(readFileSync(p, 'utf8'));
|
|
752
|
+
if (chosenSkin && c[`avatarSkin_${record.agentId}`] !== chosenSkin) {
|
|
753
|
+
c[`avatarSkin_${record.agentId}`] = chosenSkin;
|
|
754
|
+
writeFileSync(p, JSON.stringify(c, null, 2), 'utf8');
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
catch { }
|
|
759
|
+
// Materialize the injected context so the agent's Claude Code session can
|
|
760
|
+
// read (and `cat`) its personal notes/memories from login. The bundle rode
|
|
761
|
+
// in on the cloud mint response; the agent can also pull live via the
|
|
762
|
+
// clearly_memory_recall / clearly_memory_search MCP tools.
|
|
763
|
+
let injectedCount = 0;
|
|
764
|
+
let contextPath = '';
|
|
765
|
+
try {
|
|
766
|
+
const mem = minted.memory;
|
|
767
|
+
if (mem?.markdown) {
|
|
768
|
+
contextPath = join(homedir(), '.beehaven', 'agents', `${name}.context.md`);
|
|
769
|
+
mkdirSync(dirname(contextPath), { recursive: true });
|
|
770
|
+
writeFileSync(contextPath, mem.markdown, { mode: 0o600 });
|
|
771
|
+
injectedCount = mem.count ?? 0;
|
|
772
|
+
}
|
|
773
|
+
}
|
|
774
|
+
catch {
|
|
775
|
+
/* context file is best-effort — memory is still live via MCP */
|
|
776
|
+
}
|
|
777
|
+
const voiceLabel = record.voice
|
|
778
|
+
? `, voice: ${record.voice.provider}${record.voice.voiceId ? `:${record.voice.voiceId}` : ''}`
|
|
779
|
+
: '';
|
|
780
|
+
const skinLabel = record.avatarSkin ? `, skin: ${record.avatarSkin}` : '';
|
|
781
|
+
console.log('');
|
|
782
|
+
console.log(` ${green('✓')} Signed in as agent ${cyan(name)} (avatar: ${record.avatarId}${skinLabel}, kind: ${record.kind}${voiceLabel})`);
|
|
783
|
+
if (minted?.soul?.enabled) {
|
|
784
|
+
const appearance = String(minted.soul.appearance ?? '').trim();
|
|
785
|
+
console.log(` ${green('✦')} Avatar Soul ready — baseline minted${appearance ? `. ${appearance}` : '.'}`);
|
|
786
|
+
console.log(dim(' Soul: agent-soul-get · agent-soul-avatar-update · agent-soul-draft-get/save · agent-doodle-space-get/draw'));
|
|
787
|
+
console.log(dim(' Home: agent-home-get · agent-home-layout · agent-home-memory/journal · agent-home-ad · agent-home-avatar-location (24px grid)'));
|
|
788
|
+
}
|
|
789
|
+
if (sessionOpen)
|
|
790
|
+
console.log(` ${green('◆')} Live agent presence connected.`);
|
|
791
|
+
if (contextPath) {
|
|
792
|
+
const what = injectedCount > 0
|
|
793
|
+
? `operating guide + ${injectedCount} ${injectedCount === 1 ? 'memory' : 'memories'}`
|
|
794
|
+
: 'operating guide';
|
|
795
|
+
console.log(` ${green('✎')} Context injected (${what}) → ${cyan(`~/.beehaven/agents/${name}.context.md`)}`);
|
|
796
|
+
console.log(dim(` Read it at the start of every session — it explains the full platform + loads ${name}'s memory.`));
|
|
797
|
+
}
|
|
798
|
+
// ── THE SESSION, AND THE DIGEST ────────────────────────────────────────────────
|
|
799
|
+
//
|
|
800
|
+
// Logging in is exactly the moment an agent needs to know what it already did here, and
|
|
801
|
+
// exactly the moment it has the least context to think of asking. So the session start
|
|
802
|
+
// rides on login and PRINTS its brief — continuity you have to remember to request is
|
|
803
|
+
// continuity most agents will not have.
|
|
804
|
+
//
|
|
805
|
+
// Best-effort for an UNREACHABLE workspace: an identity that mints but cannot reach it is still
|
|
806
|
+
// a usable identity. ⚠ A workspace that ANSWERS and refuses is a different state — see
|
|
807
|
+
// `loginSessionOutcome` — and is reported, never swallowed.
|
|
808
|
+
let started;
|
|
809
|
+
let thrown;
|
|
810
|
+
try {
|
|
811
|
+
started = await rpcOnce('agent-session-start', {
|
|
812
|
+
agentId: record.agentId,
|
|
813
|
+
client: String(values.client ?? 'cli').trim() || 'cli',
|
|
814
|
+
label: String(values.label ?? '').trim() || undefined,
|
|
815
|
+
expiresAt: record.expiresAt,
|
|
816
|
+
}, 15_000);
|
|
817
|
+
}
|
|
818
|
+
catch (err) {
|
|
819
|
+
thrown = err ?? new Error('agent-session-start failed');
|
|
820
|
+
}
|
|
821
|
+
const outcome = loginSessionOutcome(started, thrown);
|
|
822
|
+
if (outcome.kind === 'started') {
|
|
823
|
+
saveAgentSession(name, outcome.sessionId);
|
|
824
|
+
/**
|
|
825
|
+
* ⚠⚠ CLAIM THE FAMILY — the step that makes the export optional.
|
|
826
|
+
*
|
|
827
|
+
* This function used to end by PRINTING `export BEEHAVEN_AGENT=<name>` and hoping.
|
|
828
|
+
* An agent that logged in and did not export was fully authenticated and still
|
|
829
|
+
* calling as nobody: identity minted, workspace session open, and every subsequent
|
|
830
|
+
* `beehaven call` attributed to the user.
|
|
831
|
+
*
|
|
832
|
+
* The exact scope is already bound above; this adds the tool family, so a later session of
|
|
833
|
+
* the same tool that has not logged in inherits an identity the workspace actually admits.
|
|
834
|
+
*/
|
|
835
|
+
try {
|
|
836
|
+
bindAgentScope(name, deriveAgentScope());
|
|
837
|
+
}
|
|
838
|
+
catch { /* binding is a convenience — never fail a good login over it */ }
|
|
839
|
+
console.log('');
|
|
840
|
+
console.log(` ${green('◷')} Workspace work session ${cyan(outcome.sessionId.slice(0, 8))} started.`);
|
|
841
|
+
printBrief(outcome.brief);
|
|
842
|
+
}
|
|
843
|
+
else if (outcome.kind === 'refused') {
|
|
844
|
+
console.log('');
|
|
845
|
+
console.error(` ${yellow('⚠')} Signed in, but this workspace refused ${cyan(name)}: ${outcome.reason}`);
|
|
846
|
+
if (/revoked|not active|unknown agent|not admitted/i.test(`${outcome.scope} ${outcome.reason}`)) {
|
|
847
|
+
console.error(dim(` The agent is archived or not admitted here. Restore it in Settings → Agent access,`));
|
|
848
|
+
console.error(dim(` or log in as an active agent (beehaven call agent-list '{}' shows the roster).`));
|
|
849
|
+
}
|
|
850
|
+
console.error(dim(` Calls from this session run as ${name} and will be refused — they will NOT fall back to another agent.`));
|
|
851
|
+
process.exitCode = 1;
|
|
852
|
+
}
|
|
853
|
+
console.log('');
|
|
854
|
+
{
|
|
855
|
+
// ⚠ Say whether the binding took — CHECKED, not assumed. This line used to print
|
|
856
|
+
// unconditionally, including after the binding had been skipped, which is how a session
|
|
857
|
+
// "bound to claude" spent an afternoon writing as `jinri-claude`.
|
|
858
|
+
if (scope) {
|
|
859
|
+
const resolved = agentForScope(scope);
|
|
860
|
+
if (resolved === name) {
|
|
861
|
+
console.log(` ${green('✓')} Bound to this session (${cyan(scope)}) — every call here now runs as ${cyan(name)}.`);
|
|
862
|
+
console.log(dim(' Other terminals: export BEEHAVEN_AGENT=' + name));
|
|
863
|
+
}
|
|
864
|
+
else {
|
|
865
|
+
console.error(` ${yellow('⚠')} Could not bind this session (${scope}) to ${name}; calls resolve to ${resolved ?? 'nobody'}.`);
|
|
866
|
+
console.error(dim(` Pin it explicitly: export BEEHAVEN_AGENT=${name}`));
|
|
867
|
+
process.exitCode = 1;
|
|
868
|
+
}
|
|
869
|
+
}
|
|
870
|
+
else {
|
|
871
|
+
console.log(' Activate in THIS terminal:');
|
|
872
|
+
console.log(` ${cyan(`eval "$(beehaven agent use ${name})"`)}`);
|
|
873
|
+
console.log(dim(` or: export BEEHAVEN_AGENT=${name}`));
|
|
874
|
+
}
|
|
875
|
+
}
|
|
876
|
+
console.log('');
|
|
877
|
+
}
|
|
878
|
+
/**
|
|
879
|
+
* The login digest.
|
|
880
|
+
*
|
|
881
|
+
* Everything here answers a question a returning colleague asks before touching anything, and
|
|
882
|
+
* NOTHING here is printed when it is empty — a brief padded with "0 sessions · 0 memories ·
|
|
883
|
+
* nothing assigned" is a wall an agent learns to skip, which costs the one thing the digest
|
|
884
|
+
* has to buy: being read.
|
|
885
|
+
*/
|
|
886
|
+
function printBrief(brief) {
|
|
887
|
+
if (!brief)
|
|
888
|
+
return;
|
|
889
|
+
const sessions = brief.sessions?.recent ?? [];
|
|
890
|
+
const assigned = brief.assignedToMe ?? [];
|
|
891
|
+
const pending = brief.awaitingApproval ?? [];
|
|
892
|
+
const memories = brief.memories ?? [];
|
|
893
|
+
const focus = brief.focus ?? [];
|
|
894
|
+
/**
|
|
895
|
+
* ⚠ SS-41 — ADDRESSED TO YOU, AND PRINTED FIRST. Voicemail and @mentions are somebody speaking
|
|
896
|
+
* TO this agent; everything else in the brief is ambient. Both stay until acknowledged.
|
|
897
|
+
*/
|
|
898
|
+
const mentions = brief.workspace?.mentions ?? [];
|
|
899
|
+
const voicemail = brief.workspace?.voicemail ?? [];
|
|
900
|
+
if (mentions.length || voicemail.length) {
|
|
901
|
+
console.log('');
|
|
902
|
+
console.log(` ${bold('Addressed to you')} ${dim(`(${mentions.length + voicemail.length})`)}`);
|
|
903
|
+
for (const v of voicemail.slice(0, 3)) {
|
|
904
|
+
console.log(` ${dim('·')} ${cyan('voicemail')} from ${v.from}: ${String(v.text ?? '').replace(/\s+/g, ' ').slice(0, 100)}`);
|
|
905
|
+
}
|
|
906
|
+
for (const m of mentions.slice(0, 5)) {
|
|
907
|
+
const on = m.on ?? {};
|
|
908
|
+
console.log(` ${dim('·')} ${cyan('@mention')} from ${m.from} on ${on.kind || 'item'} “${on.title || on.id || ''}” ${dim(String(m.id).slice(0, 8))}`);
|
|
909
|
+
console.log(` ${String(m.text ?? '').replace(/\s+/g, ' ').slice(0, 120)}`);
|
|
910
|
+
}
|
|
911
|
+
if (mentions.length)
|
|
912
|
+
console.log(dim(` Act, reply with \`beehaven comment <ref> "…"\`, then \`beehaven mention ack <id>\`.`));
|
|
913
|
+
}
|
|
914
|
+
if (!sessions.length && !assigned.length && !pending.length && !memories.length && !focus.length) {
|
|
915
|
+
if (!mentions.length && !voicemail.length)
|
|
916
|
+
console.log(dim(' First visit — nothing to catch up on.'));
|
|
917
|
+
return;
|
|
918
|
+
}
|
|
919
|
+
const ago = (ts) => {
|
|
920
|
+
const d = Date.now() - ts;
|
|
921
|
+
if (d < 60_000)
|
|
922
|
+
return 'just now';
|
|
923
|
+
if (d < 3_600_000)
|
|
924
|
+
return `${Math.round(d / 60_000)}m ago`;
|
|
925
|
+
if (d < 86_400_000)
|
|
926
|
+
return `${Math.round(d / 3_600_000)}h ago`;
|
|
927
|
+
return `${Math.round(d / 86_400_000)}d ago`;
|
|
928
|
+
};
|
|
929
|
+
if (sessions.length) {
|
|
930
|
+
console.log('');
|
|
931
|
+
console.log(` ${bold('Last time you were here')}`);
|
|
932
|
+
for (const s of sessions.slice(0, 3)) {
|
|
933
|
+
const when = s.startedAt ? ago(s.startedAt) : '';
|
|
934
|
+
const counts = `${s.actions} action${s.actions === 1 ? '' : 's'}${s.errors ? `, ${s.errors} error${s.errors === 1 ? '' : 's'}` : ''}`;
|
|
935
|
+
console.log(` ${dim('·')} ${s.label || '(unlabelled)'} ${dim(`— ${when}, ${counts}`)}`);
|
|
936
|
+
// The summary is the payload. It is the one thing carrying reasoning rather than counts.
|
|
937
|
+
if (s.summary)
|
|
938
|
+
console.log(` ${s.summary}`);
|
|
939
|
+
}
|
|
940
|
+
}
|
|
941
|
+
if (focus.length) {
|
|
942
|
+
console.log('');
|
|
943
|
+
console.log(` ${bold('You were working on')}`);
|
|
944
|
+
for (const f of focus.slice(0, 3)) {
|
|
945
|
+
console.log(` ${dim('·')} ${f.title || f.id} ${dim(`(${f.kind}, ${f.selected.length} selected)`)}`);
|
|
946
|
+
}
|
|
947
|
+
}
|
|
948
|
+
if (assigned.length) {
|
|
949
|
+
console.log('');
|
|
950
|
+
console.log(` ${bold('Assigned to you')} ${dim(`(${assigned.length})`)}`);
|
|
951
|
+
for (const t of assigned.slice(0, 5)) {
|
|
952
|
+
console.log(` ${dim('·')} ${t.key ? cyan(t.key) + ' ' : ''}${t.title} ${dim(`[${t.status}]`)}`);
|
|
953
|
+
}
|
|
954
|
+
}
|
|
955
|
+
if (pending.length) {
|
|
956
|
+
console.log('');
|
|
957
|
+
console.log(` ${bold('Waiting on a human')} ${dim(`(${pending.length})`)}`);
|
|
958
|
+
for (const c of pending.slice(0, 4)) {
|
|
959
|
+
console.log(` ${dim('·')} ${c.title || c.id} ${dim(c.kind ? `(${c.kind})` : '')}`);
|
|
960
|
+
}
|
|
961
|
+
}
|
|
962
|
+
if (memories.length) {
|
|
963
|
+
console.log('');
|
|
964
|
+
console.log(` ${bold('You noted')}`);
|
|
965
|
+
for (const m of memories.slice(0, 4)) {
|
|
966
|
+
console.log(` ${dim('·')} ${cyan(m.key)}: ${m.value.slice(0, 90)}${m.value.length > 90 ? '…' : ''}`);
|
|
967
|
+
}
|
|
968
|
+
}
|
|
969
|
+
}
|
|
970
|
+
// ── use (prints the export line for eval) ────────────
|
|
971
|
+
async function agentUse(args) {
|
|
972
|
+
const name = args[0];
|
|
973
|
+
if (!name) {
|
|
974
|
+
// Usage goes to STDERR so a stray `eval "$(beehaven agent use)"` doesn't eval junk.
|
|
975
|
+
console.error(' Usage: eval "$(beehaven agent use <name>)"');
|
|
976
|
+
process.exit(1);
|
|
977
|
+
}
|
|
978
|
+
const record = loadAgent(name);
|
|
979
|
+
if (!record) {
|
|
980
|
+
console.error(` No such agent "${name}". Create it: beehaven agent login ${name}`);
|
|
981
|
+
process.exit(1);
|
|
982
|
+
}
|
|
983
|
+
// The ONLY thing on STDOUT is the eval-able export line.
|
|
984
|
+
process.stdout.write(`export BEEHAVEN_AGENT=${name}\n`);
|
|
985
|
+
// Everything human goes to STDERR so `eval "$(...)"` captures just the export.
|
|
986
|
+
console.error(dim(`# agent ${name} active (avatar ${record.avatarId})`));
|
|
987
|
+
}
|
|
988
|
+
// ── list ─────────────────────────────────────────────
|
|
989
|
+
async function agentList(values) {
|
|
990
|
+
const agents = listAgents();
|
|
991
|
+
const active = activeAgentName(values);
|
|
992
|
+
const localNames = new Set(agents.map((a) => a.name));
|
|
993
|
+
const localIds = new Set(agents.map((a) => a.record.agentId));
|
|
994
|
+
// The WORKSPACE is the source of truth — pull its agents so the list reflects
|
|
995
|
+
// identities that exist there but aren't signed in on THIS device.
|
|
996
|
+
let workspace = [];
|
|
997
|
+
try {
|
|
998
|
+
const r = await rpcOnce('agent-list', {}, 8_000);
|
|
999
|
+
const raw = r?.agents ?? r?.result?.agents ?? [];
|
|
1000
|
+
workspace = raw
|
|
1001
|
+
.map((x) => ({
|
|
1002
|
+
id: String(x.id ?? x.agentId ?? ''),
|
|
1003
|
+
name: String(x.name ?? x.id ?? ''),
|
|
1004
|
+
}))
|
|
1005
|
+
.filter((x) => x.id && x.id !== 'omni');
|
|
1006
|
+
}
|
|
1007
|
+
catch {
|
|
1008
|
+
/* daemon down / offline — show local only */
|
|
1009
|
+
}
|
|
1010
|
+
if (!agents.length && !workspace.length) {
|
|
1011
|
+
console.log(' No agent identities yet.');
|
|
1012
|
+
console.log(dim(' Create one: beehaven agent login <name>'));
|
|
1013
|
+
return;
|
|
1014
|
+
}
|
|
1015
|
+
console.log('');
|
|
1016
|
+
console.log(' Agent identities');
|
|
1017
|
+
console.log(' ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
|
1018
|
+
if (agents.length) {
|
|
1019
|
+
console.log(dim(' Signed in on this device:'));
|
|
1020
|
+
for (const { name, record } of agents) {
|
|
1021
|
+
const marker = name === active ? green(' ●') : ' ';
|
|
1022
|
+
const ttl = formatRemaining(record.expiresAt);
|
|
1023
|
+
console.log(`${marker} ${cyan(name.padEnd(16))} avatar:${record.avatarId.padEnd(14)} ${dim(record.kind.padEnd(13))} ${ttl}`);
|
|
1024
|
+
}
|
|
1025
|
+
}
|
|
1026
|
+
const remoteOnly = workspace.filter((w) => !localIds.has(w.id) && !localNames.has(w.name));
|
|
1027
|
+
if (remoteOnly.length) {
|
|
1028
|
+
console.log('');
|
|
1029
|
+
console.log(dim(' In the workspace — not signed in here (`agent login <name>` to use):'));
|
|
1030
|
+
for (const w of remoteOnly)
|
|
1031
|
+
console.log(` ${cyan(w.name.padEnd(20))} ${dim(w.id)}`);
|
|
1032
|
+
}
|
|
1033
|
+
console.log('');
|
|
1034
|
+
console.log(dim(` active = $BEEHAVEN_AGENT${active ? ` (${active})` : ' (none)'} · workspace = source of truth`));
|
|
1035
|
+
console.log('');
|
|
1036
|
+
}
|
|
1037
|
+
// ── whoami ───────────────────────────────────────────
|
|
1038
|
+
function agentWhoami(values) {
|
|
1039
|
+
const active = resolveActiveAgent(values);
|
|
1040
|
+
if (!active) {
|
|
1041
|
+
console.log(' No active agent.');
|
|
1042
|
+
console.log(dim(' Activate one: eval "$(beehaven agent use <name>)" · or pass --agent <name>'));
|
|
1043
|
+
return;
|
|
1044
|
+
}
|
|
1045
|
+
const r = active.record;
|
|
1046
|
+
console.log('');
|
|
1047
|
+
console.log(` Agent ${cyan(active.name)}${active.expired ? ' ' + '\x1b[31m(expired)\x1b[0m' : ''}`);
|
|
1048
|
+
console.log(` agentId: ${r.agentId}`);
|
|
1049
|
+
console.log(` kind: ${r.kind}`);
|
|
1050
|
+
console.log(` avatar: ${r.avatarId}`);
|
|
1051
|
+
console.log(` displayName: ${r.displayName}`);
|
|
1052
|
+
console.log(` owner: ${r.userId}`);
|
|
1053
|
+
// ⚠ "unknown" for pre-birthday identities — see the field's note. Showing today's
|
|
1054
|
+
// date would date every legacy agent to whenever someone first ran whoami.
|
|
1055
|
+
console.log(` birthday: ${r.birthday ? formatBirthday(r.birthday) : 'unknown (created before birthdays)'}`);
|
|
1056
|
+
console.log(` voice: ${r.voice?.provider ? (r.voice.voiceId ? `${r.voice.provider}:${r.voice.voiceId}` : `${r.voice.provider} (base voice)`) : 'unbound — speak picks whatever is reachable'}`);
|
|
1057
|
+
console.log(` expires: ${formatRemaining(r.expiresAt)}`);
|
|
1058
|
+
if (active.expired)
|
|
1059
|
+
console.log(dim(` refresh: beehaven agent login ${active.name}`));
|
|
1060
|
+
console.log('');
|
|
1061
|
+
}
|
|
1062
|
+
async function agentStatus(args, values) {
|
|
1063
|
+
const active = resolveActiveAgent(values);
|
|
1064
|
+
const name = args[0] ?? active?.name;
|
|
1065
|
+
const record = name ? loadAgent(name) : null;
|
|
1066
|
+
const message = String(values.message ?? args.slice(name ? 1 : 0).join(' ') ?? '').trim();
|
|
1067
|
+
if (!record || !name) {
|
|
1068
|
+
console.error(' No active agent. Usage: beehaven agent status [<name>] --message "…"');
|
|
1069
|
+
process.exit(1);
|
|
1070
|
+
}
|
|
1071
|
+
if (!message) {
|
|
1072
|
+
console.error(' --message is required. Pass an empty message only through `beehaven call agent-status-set`.');
|
|
1073
|
+
process.exit(1);
|
|
1074
|
+
}
|
|
1075
|
+
const raw = await rpcOnce('agent-status-set', { agentId: record.agentId, message }, 8_000);
|
|
1076
|
+
const result = raw?.result ?? raw;
|
|
1077
|
+
if (result?.ok === false || result?.error) {
|
|
1078
|
+
console.error(` ${result.error || 'Could not update agent status.'}`);
|
|
1079
|
+
process.exit(1);
|
|
1080
|
+
}
|
|
1081
|
+
console.log(` ${green('✓')} ${cyan(name)} status: ${String(result.message ?? message)}`);
|
|
1082
|
+
}
|
|
1083
|
+
// ── logout ───────────────────────────────────────────
|
|
1084
|
+
async function agentLogout(args, values) {
|
|
1085
|
+
const name = args[0] ?? activeAgentName(values);
|
|
1086
|
+
if (!name) {
|
|
1087
|
+
console.error(' Specify an agent name, or set $BEEHAVEN_AGENT.');
|
|
1088
|
+
console.error(' Usage: beehaven agent logout [<name>]');
|
|
1089
|
+
process.exit(1);
|
|
1090
|
+
}
|
|
1091
|
+
const record = loadAgent(name);
|
|
1092
|
+
if (!record) {
|
|
1093
|
+
console.log(` No such agent "${name}" on this device.`);
|
|
1094
|
+
return;
|
|
1095
|
+
}
|
|
1096
|
+
// End the live workspace session (best-effort). The identity is NOT deleted —
|
|
1097
|
+
// it stays in the workspace (source of truth) AND on this device, so you can
|
|
1098
|
+
// log back in. To remove the current workspace projection use `agent delete <name>`;
|
|
1099
|
+
// to remove the portable account identity use `agent delete <name> --account --yes`.
|
|
1100
|
+
await endWorkspaceSession(record, 'Agent explicitly logged out.');
|
|
1101
|
+
try {
|
|
1102
|
+
await rpcOnce('agent:disconnect', { agentId: record.agentId ?? name }, 5_000);
|
|
1103
|
+
}
|
|
1104
|
+
catch {
|
|
1105
|
+
/* daemon down — nothing to close */
|
|
1106
|
+
}
|
|
1107
|
+
clearAgentSession(name);
|
|
1108
|
+
console.log(` ${green('✓')} Signed out agent "${name}" — session ended, identity kept.`);
|
|
1109
|
+
console.log(dim(` Log back in: beehaven agent login ${name} · workspace remove: beehaven agent delete ${name}`));
|
|
1110
|
+
if (activeAgentName(values) === name && !values.agent) {
|
|
1111
|
+
console.log(dim(` This was your active agent — run: unset BEEHAVEN_AGENT`));
|
|
1112
|
+
}
|
|
1113
|
+
}
|
|
1114
|
+
// ── delete (destructive — removes the identity from the workspace + device) ──
|
|
1115
|
+
async function agentDelete(args, values) {
|
|
1116
|
+
const name = args[0] ?? activeAgentName(values);
|
|
1117
|
+
if (!name) {
|
|
1118
|
+
console.error(' Specify an agent name to delete.');
|
|
1119
|
+
console.error(' Usage: beehaven agent delete <name>');
|
|
1120
|
+
process.exit(1);
|
|
1121
|
+
}
|
|
1122
|
+
if (values.account === true)
|
|
1123
|
+
return agentAccountDelete(args, values);
|
|
1124
|
+
const record = loadAgent(name);
|
|
1125
|
+
await endWorkspaceSession(record, 'Agent identity deleted.');
|
|
1126
|
+
// 1. Remove from the WORKSPACE (source of truth) — the `agent-delete` RPC
|
|
1127
|
+
// soft-deletes (archived). Resolve by the record's agentId, else the name.
|
|
1128
|
+
let cloud = false;
|
|
1129
|
+
try {
|
|
1130
|
+
const r = await rpcOnce('agent-delete', { id: record?.agentId ?? name }, 10_000);
|
|
1131
|
+
cloud = !!(r?.ok ?? r?.result?.ok ?? (!r?.error && !r?.result?.error));
|
|
1132
|
+
}
|
|
1133
|
+
catch {
|
|
1134
|
+
/* daemon down / not a workspace agent — local removal still proceeds */
|
|
1135
|
+
}
|
|
1136
|
+
// 2. Close any live session, then remove the LOCAL credential + context file.
|
|
1137
|
+
try {
|
|
1138
|
+
await rpcOnce('agent:disconnect', { agentId: record?.agentId ?? name }, 5_000);
|
|
1139
|
+
}
|
|
1140
|
+
catch {
|
|
1141
|
+
/* ignore */
|
|
1142
|
+
}
|
|
1143
|
+
const removedLocal = deleteAgent(name);
|
|
1144
|
+
if (!cloud && !removedLocal) {
|
|
1145
|
+
console.log(` No such agent "${name}".`);
|
|
1146
|
+
return;
|
|
1147
|
+
}
|
|
1148
|
+
const parts = [cloud ? 'workspace' : '', removedLocal ? 'this device' : '']
|
|
1149
|
+
.filter(Boolean)
|
|
1150
|
+
.join(' + ');
|
|
1151
|
+
console.log(` ${green('✓')} Deleted agent "${name}" from ${parts}.`);
|
|
1152
|
+
if (record)
|
|
1153
|
+
console.log(dim(` Avatar ${record.avatarId} released.`));
|
|
1154
|
+
}
|
|
1155
|
+
// ── account delete (destructive — removes the portable identity) ─────────
|
|
1156
|
+
async function agentAccountDelete(args, values) {
|
|
1157
|
+
const name = args[0] ?? activeAgentName(values);
|
|
1158
|
+
if (!name) {
|
|
1159
|
+
console.error(' Specify an agent name to delete from the account.');
|
|
1160
|
+
console.error(' Usage: beehaven agent delete <name> --account --yes');
|
|
1161
|
+
process.exit(1);
|
|
1162
|
+
}
|
|
1163
|
+
if (values.yes !== true) {
|
|
1164
|
+
console.error(` Account deletion permanently removes the portable identity "${name}" from the account.`);
|
|
1165
|
+
console.error(' It also releases the account avatar and cannot be undone.');
|
|
1166
|
+
console.error(' Re-run with: beehaven agent delete ' + name + ' --account --yes');
|
|
1167
|
+
process.exit(1);
|
|
1168
|
+
}
|
|
1169
|
+
const record = loadAgent(name);
|
|
1170
|
+
const agentId = record?.agentId ?? name;
|
|
1171
|
+
let raw;
|
|
1172
|
+
try {
|
|
1173
|
+
raw = await rpcOnce('agent-account-delete', { agentId }, 15_000);
|
|
1174
|
+
}
|
|
1175
|
+
catch (err) {
|
|
1176
|
+
console.error(` Could not delete account agent "${name}": ${err?.message ?? err}`);
|
|
1177
|
+
process.exit(1);
|
|
1178
|
+
}
|
|
1179
|
+
const result = actionPayload(raw);
|
|
1180
|
+
if (result?.ok === false || result?.error) {
|
|
1181
|
+
console.error(` ${result.error || `Could not delete account agent "${name}".`}`);
|
|
1182
|
+
process.exit(1);
|
|
1183
|
+
}
|
|
1184
|
+
await endWorkspaceSession(record, 'Account agent identity deleted.');
|
|
1185
|
+
try {
|
|
1186
|
+
await rpcOnce('agent:disconnect', { agentId }, 5_000);
|
|
1187
|
+
}
|
|
1188
|
+
catch {
|
|
1189
|
+
/* ignore */
|
|
1190
|
+
}
|
|
1191
|
+
const removedLocal = deleteAgent(name);
|
|
1192
|
+
const parts = ['account', removedLocal ? 'this device' : ''].filter(Boolean).join(' + ');
|
|
1193
|
+
console.log(` ${green('✓')} Deleted agent "${name}" from ${parts}.`);
|
|
1194
|
+
console.log(dim(' This is account-wide. Use `agent delete <name>` without --account for workspace-only removal.'));
|
|
1195
|
+
}
|
|
1196
|
+
// ── rebind ───────────────────────────────────────────
|
|
1197
|
+
async function agentRebind(args, values) {
|
|
1198
|
+
const name = args[0];
|
|
1199
|
+
const newAvatar = values.avatar ? String(values.avatar).trim() : undefined;
|
|
1200
|
+
if (!name || !newAvatar) {
|
|
1201
|
+
console.error(' Usage: beehaven agent rebind <name> --avatar <id>');
|
|
1202
|
+
process.exit(1);
|
|
1203
|
+
}
|
|
1204
|
+
const record = loadAgent(name);
|
|
1205
|
+
if (!record) {
|
|
1206
|
+
console.error(` No such agent "${name}". Create it: beehaven agent login ${name}`);
|
|
1207
|
+
process.exit(1);
|
|
1208
|
+
}
|
|
1209
|
+
if (newAvatar === record.avatarId) {
|
|
1210
|
+
console.log(` Agent "${name}" already drives avatar "${newAvatar}".`);
|
|
1211
|
+
return;
|
|
1212
|
+
}
|
|
1213
|
+
const collision = findAgentByAvatar(newAvatar, name);
|
|
1214
|
+
if (collision) {
|
|
1215
|
+
console.error(` Avatar "${newAvatar}" is already claimed by agent "${collision}".`);
|
|
1216
|
+
process.exit(1);
|
|
1217
|
+
}
|
|
1218
|
+
// Refresh the portable workspace token and the device-local app-rpc token separately. The
|
|
1219
|
+
// daemon cannot verify the worker-signed token, and the worker cannot verify the device token.
|
|
1220
|
+
let cloudMinted;
|
|
1221
|
+
try {
|
|
1222
|
+
cloudMinted = await rpcOnce('agent-account-mint', {
|
|
1223
|
+
agentId: record.agentId,
|
|
1224
|
+
displayName: record.displayName,
|
|
1225
|
+
avatarId: newAvatar,
|
|
1226
|
+
}, 15_000);
|
|
1227
|
+
}
|
|
1228
|
+
catch (err) {
|
|
1229
|
+
console.error(` Could not refresh workspace identity: ${err.message}`);
|
|
1230
|
+
process.exit(1);
|
|
1231
|
+
}
|
|
1232
|
+
if (!cloudMinted?.token) {
|
|
1233
|
+
console.error(` Workspace refresh failed: ${cloudMinted?.error ?? 'no token returned'}`);
|
|
1234
|
+
process.exit(1);
|
|
1235
|
+
}
|
|
1236
|
+
let localToken = record.localToken;
|
|
1237
|
+
try {
|
|
1238
|
+
const local = await rpcOnce('agent:mint', {
|
|
1239
|
+
agentId: record.agentId,
|
|
1240
|
+
kind: record.kind,
|
|
1241
|
+
displayName: record.displayName,
|
|
1242
|
+
avatarId: newAvatar,
|
|
1243
|
+
permissions: ['avatar:say', 'avatar:speak', 'avatar:emote'],
|
|
1244
|
+
...(record.voice ? { meta: { voice: record.voice } } : {}),
|
|
1245
|
+
}, 15_000);
|
|
1246
|
+
localToken = local?.token ?? local?.result?.token ?? localToken;
|
|
1247
|
+
}
|
|
1248
|
+
catch (err) {
|
|
1249
|
+
if (record.localToken) {
|
|
1250
|
+
console.error(dim(` ⚠ local avatar rebind unavailable: ${err?.message ?? err}`));
|
|
1251
|
+
process.exit(1);
|
|
1252
|
+
}
|
|
1253
|
+
}
|
|
1254
|
+
const updated = {
|
|
1255
|
+
...record,
|
|
1256
|
+
avatarId: cloudMinted.avatarId ?? newAvatar,
|
|
1257
|
+
avatarSkin: newAvatar,
|
|
1258
|
+
token: cloudMinted.token,
|
|
1259
|
+
...(localToken ? { localToken } : {}),
|
|
1260
|
+
issuedAt: cloudMinted.issuedAt ?? record.issuedAt,
|
|
1261
|
+
expiresAt: cloudMinted.expiresAt ?? record.expiresAt,
|
|
1262
|
+
};
|
|
1263
|
+
saveAgent(name, updated);
|
|
1264
|
+
try {
|
|
1265
|
+
const p = join(homedir(), 'Library', 'Application Support', 'clearly-desktop', 'config.json');
|
|
1266
|
+
if (existsSync(p)) {
|
|
1267
|
+
const c = JSON.parse(readFileSync(p, 'utf8'));
|
|
1268
|
+
c[`avatarSkin_${updated.agentId}`] = newAvatar;
|
|
1269
|
+
writeFileSync(p, JSON.stringify(c, null, 2), 'utf8');
|
|
1270
|
+
}
|
|
1271
|
+
}
|
|
1272
|
+
catch { }
|
|
1273
|
+
console.log(` ${green('✓')} Rebound agent "${name}" → avatar ${cyan(updated.avatarId)} (was ${record.avatarId}).`);
|
|
1274
|
+
}
|
|
1275
|
+
// ── helpers ──────────────────────────────────────────
|
|
1276
|
+
/**
|
|
1277
|
+
* "2026-08-24 (3 days old)" — the date plus how long ago, because the age is the
|
|
1278
|
+
* part anyone actually wants and subtracting dates in your head is friction.
|
|
1279
|
+
*
|
|
1280
|
+
* ⚠ Returns the raw string for anything unparseable rather than throwing or
|
|
1281
|
+
* printing "Invalid Date": a corrupt field should not take `whoami` down with it.
|
|
1282
|
+
*/
|
|
1283
|
+
function formatBirthday(iso) {
|
|
1284
|
+
const t = Date.parse(iso);
|
|
1285
|
+
if (!Number.isFinite(t))
|
|
1286
|
+
return iso;
|
|
1287
|
+
const days = Math.floor((Date.now() - t) / 86_400_000);
|
|
1288
|
+
const date = new Date(t).toISOString().slice(0, 10);
|
|
1289
|
+
if (days <= 0)
|
|
1290
|
+
return `${date} (today)`;
|
|
1291
|
+
if (days === 1)
|
|
1292
|
+
return `${date} (1 day old)`;
|
|
1293
|
+
if (days < 60)
|
|
1294
|
+
return `${date} (${days} days old)`;
|
|
1295
|
+
const months = Math.floor(days / 30);
|
|
1296
|
+
return `${date} (${months} month${months === 1 ? '' : 's'} old)`;
|
|
1297
|
+
}
|
|
1298
|
+
function formatRemaining(expiresAt) {
|
|
1299
|
+
const ms = expiresAt - Date.now();
|
|
1300
|
+
if (ms <= 0)
|
|
1301
|
+
return '\x1b[31mexpired\x1b[0m';
|
|
1302
|
+
const days = Math.floor(ms / 86_400_000);
|
|
1303
|
+
if (days >= 1)
|
|
1304
|
+
return dim(`${days}d left`);
|
|
1305
|
+
const hours = Math.floor(ms / 3_600_000);
|
|
1306
|
+
if (hours >= 1)
|
|
1307
|
+
return dim(`${hours}h left`);
|
|
1308
|
+
const mins = Math.floor(ms / 60_000);
|
|
1309
|
+
return dim(`${mins}m left`);
|
|
1310
|
+
}
|