beehaven 0.8.29 → 0.8.34
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/dist/agent-attribution.js +20 -2
- package/dist/agent-scope.js +55 -10
- package/dist/agent-state.js +44 -0
- package/dist/agents-md-text.js +64 -0
- package/dist/browser-login.js +14 -4
- package/dist/build-channel.js +2 -2
- package/dist/claude-bridge.js +12 -9
- package/dist/clearly.js +4 -3
- package/dist/cli-state.js +45 -3
- package/dist/cli.js +82 -53
- package/dist/commands/agent-skills.js +12 -1
- package/dist/commands/agent.js +235 -28
- package/dist/commands/agents-md.js +118 -0
- package/dist/commands/daemon-cmd.js +2 -0
- package/dist/commands/help.js +21 -0
- package/dist/commands/self-update.js +32 -5
- package/dist/commands/workspace.js +22 -6
- package/dist/daemon-client.js +10 -6
- package/dist/daemon-entry.js +1 -0
- package/dist/daemon-process.js +125 -0
- package/dist/daemon.js +131 -62
- package/dist/dashboard.js +118 -6
- package/dist/ipc-auth.js +3 -1
- package/dist/ipc-client.js +7 -6
- package/dist/ipc-path.js +92 -0
- package/dist/ipc-server.js +33 -33
- package/dist/npm-update.js +138 -0
- package/dist/session-binding.js +135 -0
- package/dist/session-target.js +91 -0
- package/dist/setup-service.js +9 -4
- package/dist/verb-surface.js +1 -0
- package/dist/watcher.js +9 -23
- package/dist/windows-hide.js +77 -0
- package/package.json +2 -2
- package/scripts/sync-skills.mjs +6 -0
- package/skills/agent-doodle-space/SKILL.md +1 -1
- package/skills/agent-home/SKILL.md +1 -1
- package/skills/brand-identity/SKILL.md +22 -5
- package/skills/clearly-agent/SKILL.md +38 -14
- package/skills/clearly-canvas/SKILL.md +31 -5
- package/skills/clearly-canvas/authoring.md +41 -12
- package/skills/clearly-canvas/example.mjs +72 -69
- package/skills/clearly-docs/SKILL.md +151 -0
- package/skills/clearly-init/SKILL.md +135 -0
- package/skills/clearly-workflows/SKILL.md +90 -0
- package/skills/clearly-workspace/SKILL.md +1 -1
- package/skills/codebase-map/SKILL.md +1 -1
- package/skills/design-craft/SKILL.md +9 -6
- package/skills/index.json +45 -10
- package/skills/layout-systems/SKILL.md +14 -11
- package/skills/pair-on-canvas/SKILL.md +1 -1
- package/skills/sticker-pack/SKILL.md +132 -0
- package/skills/visualize/SKILL.md +188 -0
package/dist/cli.js
CHANGED
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
if (!process.env.BEEHAVEN_IPC_SCOPE) {
|
|
17
17
|
process.env.BEEHAVEN_IPC_SCOPE = 'developer';
|
|
18
18
|
}
|
|
19
|
+
import './windows-hide.js'; // FIRST: no console windows flashing on Windows (CAN2-56)
|
|
19
20
|
import { parseArgs } from 'node:util';
|
|
20
21
|
import { devModeAvailable, buildLabel, PUBLIC_BUILD } from './build-channel.js';
|
|
21
22
|
import { readFileSync, existsSync, mkdirSync, writeFileSync, readdirSync, } from 'fs';
|
|
@@ -142,6 +143,7 @@ function printHelp() {
|
|
|
142
143
|
|
|
143
144
|
beehaven guide Agent-first setup and signup handoff
|
|
144
145
|
beehaven signup Create a free Clearly account
|
|
146
|
+
beehaven agents-md Add Clearly to this project's AGENTS.md
|
|
145
147
|
|
|
146
148
|
Personal workspace:
|
|
147
149
|
beehaven cd home
|
|
@@ -448,8 +450,9 @@ ${DEV_ONLY_HELP ? `
|
|
|
448
450
|
|
|
449
451
|
Note: if you also run the Hive menu-bar app, the tray is the
|
|
450
452
|
primary owner of the daemon. CLI start/stop only act when the tray
|
|
451
|
-
isn't running its own daemon. The same socket (/tmp/beehaven-ipc.sock
|
|
452
|
-
is shared in both modes — never two
|
|
453
|
+
isn't running its own daemon. The same socket (/tmp/beehaven-ipc.sock;
|
|
454
|
+
a per-user named pipe on Windows) is shared in both modes — never two
|
|
455
|
+
daemons at once.
|
|
453
456
|
|
|
454
457
|
─────────────────────────────────────────────────────────────────────
|
|
455
458
|
OPTIONS
|
|
@@ -846,48 +849,9 @@ async function printStatus() {
|
|
|
846
849
|
* One function, because four hand-written copies is how one missing `else` became four.
|
|
847
850
|
*/
|
|
848
851
|
async function resolveTargetBuilding(values) {
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
const t = getTarget();
|
|
853
|
-
if (t?.address?.includes('/')) {
|
|
854
|
-
const [bType, ...bIdParts] = t.address.split('/');
|
|
855
|
-
values.building = `${bType}:${bIdParts.join('/')}`;
|
|
856
|
-
return;
|
|
857
|
-
}
|
|
858
|
-
if (t && isHomeAddress(t.address)) {
|
|
859
|
-
/**
|
|
860
|
-
* ⚠ HOME IS A POSITIVE STATEMENT, NOT AN ABSENCE. The daemon's focus is a routing
|
|
861
|
-
* optimisation; the file is where the user said to be. If they disagree the daemon is
|
|
862
|
-
* stale — drop it, so the call lands where `pwd` says it will, and SAY so.
|
|
863
|
-
*/
|
|
864
|
-
try {
|
|
865
|
-
const { daemonGetConnection, daemonDisconnect } = await import('./daemon-client.js');
|
|
866
|
-
const conn = await daemonGetConnection();
|
|
867
|
-
if (conn?.buildingId) {
|
|
868
|
-
await daemonDisconnect();
|
|
869
|
-
console.error(`\x1b[2m (dropped a stale daemon focus on ${conn.buildingType}/${conn.buildingId}`
|
|
870
|
-
+ ` — your target is home)\x1b[0m`);
|
|
871
|
-
}
|
|
872
|
-
}
|
|
873
|
-
catch { /* no daemon, or it refused — home is the default route anyway */ }
|
|
874
|
-
return;
|
|
875
|
-
}
|
|
876
|
-
if (!t) {
|
|
877
|
-
/**
|
|
878
|
-
* ⚠ NO TARGET EVER SET — inherit the daemon's connection. The machine-global fallback
|
|
879
|
-
* `cli-state.ts` warns about: another room, the Mac app or a human's terminal can set
|
|
880
|
-
* it, so an agent that never runs `connect` inherits someone else's cursor. Kept for
|
|
881
|
-
* back-compat; `connect home` is what opts out, and this fix makes that opt-out real.
|
|
882
|
-
*/
|
|
883
|
-
try {
|
|
884
|
-
const { daemonGetConnection } = await import('./daemon-client.js');
|
|
885
|
-
const conn = await daemonGetConnection();
|
|
886
|
-
if (conn)
|
|
887
|
-
values.building = `${conn.buildingType}:${conn.buildingId}`;
|
|
888
|
-
}
|
|
889
|
-
catch { /* fall through to home */ }
|
|
890
|
-
}
|
|
852
|
+
// ⚠ ONE resolver for every verb, typed or raw (AWO-393/394) — see session-target.ts.
|
|
853
|
+
const { resolveTargetBuilding: resolve } = await import('./session-target.js');
|
|
854
|
+
await resolve(values);
|
|
891
855
|
}
|
|
892
856
|
/**
|
|
893
857
|
* ⚠⚠ AN IDENTIFIABLE AGENT MUST LOG IN BEFORE IT ACTS.
|
|
@@ -954,6 +918,11 @@ const AGENT_LOGIN_EXEMPT = new Set([
|
|
|
954
918
|
* would refuse the one person it is for — the agent whose user has not signed up yet (SGS-73).
|
|
955
919
|
*/
|
|
956
920
|
'handoff',
|
|
921
|
+
/**
|
|
922
|
+
* ⚠ `agents-md` WRITES A FILE IN THIS REPO AND NOTHING ELSE — it never opens a workspace, and it is
|
|
923
|
+
* part of setting a project up, which must not require the login it helps a teammate do (CAN2-66).
|
|
924
|
+
*/
|
|
925
|
+
'agents-md',
|
|
957
926
|
]);
|
|
958
927
|
/**
|
|
959
928
|
* ⚠ ALIAS → CANONICAL VERB, for the activity log's `source` column only.
|
|
@@ -1095,8 +1064,10 @@ async function requireAgentSession(command, values) {
|
|
|
1095
1064
|
cfg = {};
|
|
1096
1065
|
}
|
|
1097
1066
|
// ⚠ ONE DECISION, EXECUTED BY A GUARD (`principalKind`). This branch does nothing but act on it.
|
|
1067
|
+
const { activeSessionBinding: sessionForGate } = await import('./agent-state.js');
|
|
1098
1068
|
const who = principalKind({
|
|
1099
|
-
|
|
1069
|
+
// This session's own work session first (AWO-394); the shared record's for sessions bound before it.
|
|
1070
|
+
agentSessionId: sessionForGate(name)?.sessionId ?? rec?.sessionId,
|
|
1100
1071
|
agentExpiresAt: rec?.expiresAt,
|
|
1101
1072
|
agentRuntime: isAgentRuntime(),
|
|
1102
1073
|
accountUserId: cfg.userId,
|
|
@@ -1369,6 +1340,9 @@ async function run() {
|
|
|
1369
1340
|
state: { type: 'string' },
|
|
1370
1341
|
goal: { type: 'string' },
|
|
1371
1342
|
remove: { type: 'boolean', default: false },
|
|
1343
|
+
// `agents-md`: the MCP block instead of the CLI one; print it instead of writing it (CAN2-66).
|
|
1344
|
+
mcp: { type: 'boolean', default: false },
|
|
1345
|
+
print: { type: 'boolean', default: false },
|
|
1372
1346
|
/**
|
|
1373
1347
|
* ⚠⚠ `beehaven sheet` / `beehaven slides` — AND AN UNDECLARED FLAG IS A FATAL, NOT A NO-OP.
|
|
1374
1348
|
*
|
|
@@ -2226,6 +2200,14 @@ async function run() {
|
|
|
2226
2200
|
// workspace via daemonRpc('shell:exec', …) — see
|
|
2227
2201
|
// commands/shell.ts:resolveDefaultBuilding + execRemote.
|
|
2228
2202
|
const { HOME_ADDRESSES: HOME_ADDRS, CANONICAL_HOME_ADDRESS } = await import('./cli-state.js');
|
|
2203
|
+
/**
|
|
2204
|
+
* ⚠⚠ A SIGNED-IN SESSION MOVES ONLY ITSELF (AWO-394). The daemon's focus is ONE variable for
|
|
2205
|
+
* the machine, so `connect` used to re-point every other terminal that had no cursor of its
|
|
2206
|
+
* own — agent A connecting to a Shopify store moved agent B. A session's own cursor file is
|
|
2207
|
+
* the whole of its routing; the daemon is not told.
|
|
2208
|
+
*/
|
|
2209
|
+
const { activeSessionBinding: sessionOf } = await import('./agent-state.js');
|
|
2210
|
+
const ownSession = sessionOf();
|
|
2229
2211
|
if (HOME_ADDRS.has(target)) {
|
|
2230
2212
|
const { setTarget } = await import('./cli-state.js');
|
|
2231
2213
|
// Canonical label is 'workspace' (no "home DO" — the daemon's relay is to the
|
|
@@ -2236,7 +2218,11 @@ async function run() {
|
|
|
2236
2218
|
setTarget(CANONICAL_HOME_ADDRESS, 'cloud');
|
|
2237
2219
|
// Best-effort daemon-side cleanup of any prior connect()
|
|
2238
2220
|
// focus — we want home to be the default routing target.
|
|
2239
|
-
if (
|
|
2221
|
+
if (ownSession) {
|
|
2222
|
+
const { moveAgentSession } = await import('./commands/agent.js');
|
|
2223
|
+
await moveAgentSession(CANONICAL_HOME_ADDRESS);
|
|
2224
|
+
}
|
|
2225
|
+
else if (daemonUp) {
|
|
2240
2226
|
try {
|
|
2241
2227
|
await daemonDisconnect();
|
|
2242
2228
|
}
|
|
@@ -2279,6 +2265,15 @@ async function run() {
|
|
|
2279
2265
|
// where we meant to be, and we can retry without a re-`cd`.
|
|
2280
2266
|
const { setTarget } = await import('./cli-state.js');
|
|
2281
2267
|
setTarget(`${bType}/${bId}`, 'cloud');
|
|
2268
|
+
if (ownSession) {
|
|
2269
|
+
const { moveAgentSession } = await import('./commands/agent.js');
|
|
2270
|
+
await moveAgentSession(`${bType}/${bId}`);
|
|
2271
|
+
if (bType === 'shopifyStore' || bType === 'store') {
|
|
2272
|
+
console.log(color.dim(` Store DOs take operator calls through: beehaven admin do ${bType}/${bId} <action> '<json>' (platform admins)`));
|
|
2273
|
+
}
|
|
2274
|
+
console.log(` ${color.green('✓')} This session → ${color.green(`${bType}/${bId}`)}`);
|
|
2275
|
+
break;
|
|
2276
|
+
}
|
|
2282
2277
|
try {
|
|
2283
2278
|
await daemonConnect(bType, bId);
|
|
2284
2279
|
}
|
|
@@ -2388,7 +2383,13 @@ async function run() {
|
|
|
2388
2383
|
const { clearTarget: clearDisconn } = await import('./cli-state.js');
|
|
2389
2384
|
clearDisconn();
|
|
2390
2385
|
const { isDaemonRunning: isDaemonRunningDisc, daemonDisconnect: daemonDisconnectDisc } = await import('./daemon-client.js');
|
|
2391
|
-
|
|
2386
|
+
const { activeSessionBinding: sessionOfDisc } = await import('./agent-state.js');
|
|
2387
|
+
if (sessionOfDisc()) {
|
|
2388
|
+
// AWO-394 — a signed-in session goes home alone; the machine-wide focus is someone else's.
|
|
2389
|
+
const { moveAgentSession } = await import('./commands/agent.js');
|
|
2390
|
+
await moveAgentSession('workspace');
|
|
2391
|
+
}
|
|
2392
|
+
else if (await isDaemonRunningDisc()) {
|
|
2392
2393
|
try {
|
|
2393
2394
|
await daemonDisconnectDisc();
|
|
2394
2395
|
}
|
|
@@ -2446,7 +2447,22 @@ async function run() {
|
|
|
2446
2447
|
// ⚠ `values` HERE TOO — `beehaven status --agent alice` must report who that call would
|
|
2447
2448
|
// run as, not who the shell happens to be bound to. See `requireAgentSession`.
|
|
2448
2449
|
const who = activeAgentName(values);
|
|
2449
|
-
|
|
2450
|
+
const { activeSessionBinding } = await import('./agent-state.js');
|
|
2451
|
+
const { describeAddress, currentEnvName } = await import('./session-binding.js');
|
|
2452
|
+
const bound = activeSessionBinding(who);
|
|
2453
|
+
if (who && bound) {
|
|
2454
|
+
// AWO-394 — one session: identity + workspace + env, owned by this scope.
|
|
2455
|
+
const { loadAgent } = await import('./agent-state.js');
|
|
2456
|
+
const r = loadAgent(who);
|
|
2457
|
+
const gone = typeof r?.expiresAt === 'number' && r.expiresAt <= Date.now();
|
|
2458
|
+
const envNow = currentEnvName();
|
|
2459
|
+
console.log(gone
|
|
2460
|
+
? ` Agent: ${color.green(who)} ${'\x1b[31m'}TOKEN EXPIRED\x1b[0m${color.dim(' — calls are refused until `beehaven agent login ' + who + '`')}`
|
|
2461
|
+
: ` Agent: ${color.green(who)}${color.dim(' (this session — every call here runs as this agent)')}`);
|
|
2462
|
+
console.log(` Session: ${color.green(bound.scope)}${color.dim(` work session ${bound.sessionId ? bound.sessionId.slice(0, 8) : 'none'} · in ${describeAddress(bound.workspace)}`)}`);
|
|
2463
|
+
console.log(` Env: ${bound.env === envNow ? color.green(bound.env) : `\x1b[31m${bound.env} — daemon is on ${envNow}; calls are refused\x1b[0m`}`);
|
|
2464
|
+
}
|
|
2465
|
+
else if (who && eff === who) {
|
|
2450
2466
|
// Identity, workspace session and cursor are one handle.
|
|
2451
2467
|
const { loadAgent } = await import('./agent-state.js');
|
|
2452
2468
|
const r = loadAgent(who);
|
|
@@ -2461,6 +2477,11 @@ async function run() {
|
|
|
2461
2477
|
: ` Agent: ${color.green(who)}${color.dim(' (logged in — calls run as this agent)')}`);
|
|
2462
2478
|
console.log(` Scope: ${color.green(eff)}${color.dim(' (cursor follows the agent, not the session)')}`);
|
|
2463
2479
|
}
|
|
2480
|
+
else if (raw.startsWith('term-')) {
|
|
2481
|
+
// AWO-394 — a tab nobody signed in from is the person, on the shared cursor.
|
|
2482
|
+
console.log(` Scope: ${color.green(raw)}${color.dim(' (this tab — `beehaven agent login <name>` here signs in THIS tab only)')}`);
|
|
2483
|
+
console.log(` Agent: ${color.dim('none — calls run as you, on the cursor your other terminals share')}`);
|
|
2484
|
+
}
|
|
2464
2485
|
else if (raw) {
|
|
2465
2486
|
console.log(` Scope: ${color.green(raw)}${color.dim(' (private cursor — concurrent agents cannot clobber it)')}`);
|
|
2466
2487
|
console.log(` Agent: ${color.dim('none — calls run as you. `beehaven agent login <name>` to act as an agent')}`);
|
|
@@ -3143,6 +3164,15 @@ async function run() {
|
|
|
3143
3164
|
console.log(' (or quit and relaunch the Hive Mac app if it owns the daemon).');
|
|
3144
3165
|
break;
|
|
3145
3166
|
}
|
|
3167
|
+
case 'agents-md': {
|
|
3168
|
+
const { cmdAgentsMd } = await import('./commands/agents-md.js');
|
|
3169
|
+
process.exitCode = cmdAgentsMd(positionals[1], {
|
|
3170
|
+
mcp: !!values.mcp,
|
|
3171
|
+
print: !!values.print,
|
|
3172
|
+
remove: !!values.remove,
|
|
3173
|
+
});
|
|
3174
|
+
break;
|
|
3175
|
+
}
|
|
3146
3176
|
case 'handoff': {
|
|
3147
3177
|
const { cmdHandoff } = await import('./commands/handoff.js');
|
|
3148
3178
|
process.exitCode = await cmdHandoff(positionals[1], {
|
|
@@ -3171,11 +3201,10 @@ async function run() {
|
|
|
3171
3201
|
console.log(` Sign up at ${color.cyan(signupUrl)}.`);
|
|
3172
3202
|
console.log(' Then run "beehaven login" to wire the CLI to your account.');
|
|
3173
3203
|
console.log('');
|
|
3174
|
-
|
|
3175
|
-
|
|
3176
|
-
|
|
3177
|
-
|
|
3178
|
-
catch { }
|
|
3204
|
+
// The shared opener: per-platform (Windows has no `open`), and a spawn failure prints the
|
|
3205
|
+
// URL instead of crashing on an unhandled child 'error' event.
|
|
3206
|
+
const { openInBrowser } = await import('./browser-login.js');
|
|
3207
|
+
openInBrowser(signupUrl);
|
|
3179
3208
|
break;
|
|
3180
3209
|
}
|
|
3181
3210
|
case 'treehouse': {
|
|
@@ -236,14 +236,25 @@ export function installAgentSkills() {
|
|
|
236
236
|
* ⚠ NO STAMP ⇒ TREATED AS UNTOUCHED. Every skill installed before stamping existed has
|
|
237
237
|
* none, and assuming an edit there would freeze them permanently — updates would silently
|
|
238
238
|
* stop landing, which is the failure mode this whole change exists to remove.
|
|
239
|
+
*
|
|
240
|
+
* ⚠ IDENTICAL TO WHAT SHIPS ⇒ CURRENT, WHATEVER THE STAMP SAYS (PPO-476). The Mac app writes
|
|
241
|
+
* these same files on every launch, and a copy can only equal the release by coming from
|
|
242
|
+
* it. Asked first, a stale stamp called every skill "edited locally" and none was ever
|
|
243
|
+
* re-stamped, so the next real update never landed either.
|
|
239
244
|
*/
|
|
240
245
|
if (existing) {
|
|
246
|
+
const local = localSha(destDir, skillFiles);
|
|
247
|
+
if (entry?.sha && local === entry.sha) {
|
|
248
|
+
stamp(destDir, skillFiles, warnings);
|
|
249
|
+
steps.push(`${agent}/${name}=current`);
|
|
250
|
+
continue;
|
|
251
|
+
}
|
|
241
252
|
let stamped = '';
|
|
242
253
|
try {
|
|
243
254
|
stamped = JSON.parse(fs.readFileSync(path.join(destDir, STAMP), 'utf8')).sha ?? '';
|
|
244
255
|
}
|
|
245
256
|
catch { /* unstamped */ }
|
|
246
|
-
if (stamped &&
|
|
257
|
+
if (stamped && local !== stamped) {
|
|
247
258
|
warnings.push(`${agent}/${name}: edited locally — left alone (delete it to take the shipped copy)`);
|
|
248
259
|
steps.push(`${agent}/${name}=locally-modified`);
|
|
249
260
|
continue;
|
package/dist/commands/agent.js
CHANGED
|
@@ -91,6 +91,60 @@ async function endWorkspaceSession(record, summary) {
|
|
|
91
91
|
return false;
|
|
92
92
|
}
|
|
93
93
|
}
|
|
94
|
+
/** `team/<id>` → the `--building` form `resolveBuilding` takes; home → undefined. */
|
|
95
|
+
function buildingFor(address) {
|
|
96
|
+
if (!address || !address.includes('/'))
|
|
97
|
+
return undefined;
|
|
98
|
+
const i = address.indexOf('/');
|
|
99
|
+
return `${address.slice(0, i)}:${address.slice(i + 1)}`;
|
|
100
|
+
}
|
|
101
|
+
/** Workspace DOs hold agent work sessions; a store or a local app does not. */
|
|
102
|
+
function holdsWorkSessions(address) {
|
|
103
|
+
return !address.includes('/') || /^(team|workspace)\//.test(address);
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Run an action in a given workspace (AWO-394) — the session lives in the workspace it works in,
|
|
107
|
+
* not in whichever one the daemon happens to dial. Home goes direct; others through the proxy.
|
|
108
|
+
*/
|
|
109
|
+
async function rpcIn(address, action, input, timeout) {
|
|
110
|
+
const { resolveBuilding } = await import('./workspace.js');
|
|
111
|
+
const routed = resolveBuilding(action, input, buildingFor(address));
|
|
112
|
+
return rpcOnce(routed.action, routed.input, timeout);
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* "hive2 (workspace/98bef9…)" rather than a bare id — what an agent reads to know where it is.
|
|
116
|
+
* Best-effort: the account roster (`workspace:list`), falling back to the address itself.
|
|
117
|
+
*/
|
|
118
|
+
async function workspaceLabel(address) {
|
|
119
|
+
const { describeAddress } = await import('../session-binding.js');
|
|
120
|
+
const plain = describeAddress(address);
|
|
121
|
+
if (address.includes('/') && !/^(team|workspace)\//.test(address))
|
|
122
|
+
return plain;
|
|
123
|
+
try {
|
|
124
|
+
const { daemonRpc } = await import('../daemon-client.js');
|
|
125
|
+
const res = await Promise.race([daemonRpc('workspace:list', {}), new Promise((r) => setTimeout(() => r(null), 4_000))]);
|
|
126
|
+
const inner = res?.result ?? res;
|
|
127
|
+
const id = address.includes('/') ? address.slice(address.indexOf('/') + 1) : String(inner?.current ?? '');
|
|
128
|
+
const hit = (inner?.workspaces ?? []).find((w) => String(w?.id ?? '') === id);
|
|
129
|
+
if (hit?.name)
|
|
130
|
+
return address.includes('/') ? `${hit.name} (${plain})` : `${hit.name} — home (your default workspace)`;
|
|
131
|
+
}
|
|
132
|
+
catch { /* the roster is a nicety */ }
|
|
133
|
+
return plain;
|
|
134
|
+
}
|
|
135
|
+
/** End one work session where it lives. Best-effort, like `endWorkspaceSession`. */
|
|
136
|
+
async function endSessionIn(address, sessionId, agentId, summary) {
|
|
137
|
+
try {
|
|
138
|
+
const r = await rpcIn(address, 'agent-session-end', {
|
|
139
|
+
id: sessionId, agentId, ...(summary?.trim() ? { summary: summary.trim() } : {}),
|
|
140
|
+
}, 5_000);
|
|
141
|
+
const result = r?.result ?? r;
|
|
142
|
+
return result?.ok !== false && !result?.error;
|
|
143
|
+
}
|
|
144
|
+
catch {
|
|
145
|
+
return false;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
94
148
|
const dim = (s) => `\x1b[2m${s}\x1b[0m`;
|
|
95
149
|
const green = (s) => `\x1b[32m${s}\x1b[0m`;
|
|
96
150
|
const bold = (s) => `\x1b[1m${s}\x1b[0m`;
|
|
@@ -701,7 +755,38 @@ async function agentLogin(args, values) {
|
|
|
701
755
|
* actually began. `existingRecord` is read before the write for exactly this.
|
|
702
756
|
*/
|
|
703
757
|
const birthday = existingRecord?.birthday ?? new Date().toISOString();
|
|
704
|
-
|
|
758
|
+
/**
|
|
759
|
+
* ⚠⚠ ONE SESSION = THIS PROCESS SCOPE (AWO-394). A re-login replaces the session THIS scope had,
|
|
760
|
+
* and nothing else: it used to end `record.sessionId` — one field shared by every terminal using
|
|
761
|
+
* the name — so logging in from a second terminal closed the first terminal's session and both
|
|
762
|
+
* then wrote under the new one.
|
|
763
|
+
*/
|
|
764
|
+
const { deriveAgentScope } = await import('../agent-scope.js');
|
|
765
|
+
const { readSessionBinding, writeSessionBinding, currentEnvName, normalizeWorkspaceAddress, describeAddress, } = await import('../session-binding.js');
|
|
766
|
+
const { writeTargetForScope, readTargetForKey } = await import('../cli-state.js');
|
|
767
|
+
const scope = deriveAgentScope();
|
|
768
|
+
const priorBinding = scope ? readSessionBinding(scope) : null;
|
|
769
|
+
if (priorBinding?.sessionId)
|
|
770
|
+
await endSessionIn(priorBinding.workspace, priorBinding.sessionId, minted.agentId);
|
|
771
|
+
/**
|
|
772
|
+
* WHERE THIS SESSION WORKS: `--workspace`, else where this identity last worked, else home. The
|
|
773
|
+
* identity's last workspace is what keeps a durable agent from losing its place each conversation
|
|
774
|
+
* — the reason the cursor used to be keyed on the agent name — without letting one live session
|
|
775
|
+
* move another.
|
|
776
|
+
*/
|
|
777
|
+
const requestedWorkspace = typeof values.workspace === 'string' ? values.workspace.trim() : '';
|
|
778
|
+
const normalizedRequest = requestedWorkspace ? normalizeWorkspaceAddress(requestedWorkspace) : null;
|
|
779
|
+
if (requestedWorkspace && !normalizedRequest) {
|
|
780
|
+
console.error(` Unrecognised workspace "${requestedWorkspace}".`);
|
|
781
|
+
console.error(' Use: home · team/<id> · workspace/<id> · a 64-character workspace id (beehaven workspaces lists them)');
|
|
782
|
+
process.exit(1);
|
|
783
|
+
}
|
|
784
|
+
const legacyCursor = readTargetForKey(name)?.address;
|
|
785
|
+
const chosenWorkspace = normalizedRequest
|
|
786
|
+
|| existingRecord?.lastWorkspace
|
|
787
|
+
|| (legacyCursor ? normalizeWorkspaceAddress(legacyCursor) : null)
|
|
788
|
+
|| 'workspace';
|
|
789
|
+
const sessionEnv = currentEnvName();
|
|
705
790
|
/**
|
|
706
791
|
* ⚠⚠ FILL THE LIFETIME FIELDS FROM THE TOKEN — the cloud mint path does not
|
|
707
792
|
* return them and a record without `expiresAt` can never be seen as expired.
|
|
@@ -740,8 +825,9 @@ async function agentLogin(args, values) {
|
|
|
740
825
|
* resuming is least recoverable.
|
|
741
826
|
*/
|
|
742
827
|
...(existingRecord?.boundScopes ? { boundScopes: existingRecord.boundScopes } : {}),
|
|
743
|
-
...(
|
|
828
|
+
...(existingRecord?.sessionId ? { sessionId: existingRecord.sessionId } : {}),
|
|
744
829
|
...(existingRecord?.lastLoginAt ? { lastLoginAt: existingRecord.lastLoginAt } : {}),
|
|
830
|
+
lastWorkspace: chosenWorkspace,
|
|
745
831
|
};
|
|
746
832
|
saveAgent(name, record);
|
|
747
833
|
/**
|
|
@@ -754,10 +840,14 @@ async function agentLogin(args, values) {
|
|
|
754
840
|
* instead of silently replaced. The family (what un-logged-in sessions inherit) is claimed
|
|
755
841
|
* below, only once a session actually starts.
|
|
756
842
|
*/
|
|
757
|
-
const { deriveAgentScope } = await import('../agent-scope.js');
|
|
758
843
|
const { bindAgentScope, agentForScope, loginSessionOutcome } = await import('../agent-state.js');
|
|
759
|
-
const scope = deriveAgentScope();
|
|
760
844
|
bindAgentScope(name, scope, { family: false });
|
|
845
|
+
// The session record: identity + workspace + env, owned by this scope alone. Written before the
|
|
846
|
+
// workspace answers for the same reason the binding is — a refused login must still own its scope.
|
|
847
|
+
if (scope) {
|
|
848
|
+
writeSessionBinding({ scope, agent: name, workspace: chosenWorkspace, env: sessionEnv, boundAt: Date.now() });
|
|
849
|
+
writeTargetForScope(scope, chosenWorkspace, chosenWorkspace === 'workspace' ? 'cloud' : undefined);
|
|
850
|
+
}
|
|
761
851
|
// Sync to Electron store if present
|
|
762
852
|
try {
|
|
763
853
|
const p = join(homedir(), 'Library', 'Application Support', 'clearly-desktop', 'config.json');
|
|
@@ -821,18 +911,36 @@ async function agentLogin(args, values) {
|
|
|
821
911
|
// `loginSessionOutcome` — and is reported, never swallowed.
|
|
822
912
|
let started;
|
|
823
913
|
let thrown;
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
914
|
+
const sessionInput = {
|
|
915
|
+
agentId: record.agentId,
|
|
916
|
+
client: String(values.client ?? 'cli').trim() || 'cli',
|
|
917
|
+
label: String(values.label ?? '').trim() || undefined,
|
|
918
|
+
expiresAt: record.expiresAt,
|
|
919
|
+
};
|
|
920
|
+
if (holdsWorkSessions(chosenWorkspace)) {
|
|
921
|
+
try {
|
|
922
|
+
// ⚠ IN THE SESSION'S WORKSPACE (AWO-394) — it used to open in the daemon's default
|
|
923
|
+
// workspace whatever the cursor said, and that id then rode on calls to other workspaces.
|
|
924
|
+
started = await rpcIn(chosenWorkspace, 'agent-session-start', sessionInput, 15_000);
|
|
925
|
+
const first = loginSessionOutcome(started);
|
|
926
|
+
// An identity is the ACCOUNT's; each workspace admits it once. Same self-heal `call` does.
|
|
927
|
+
if (first.kind === 'refused' && chosenWorkspace !== 'workspace' && /unknown agent|not admitted|unknown-agent/i.test(`${first.scope} ${first.reason}`)) {
|
|
928
|
+
const admitted = await rpcIn(chosenWorkspace, 'agent-admit', { agentId: record.agentId }, 20_000).catch(() => null);
|
|
929
|
+
if ((admitted?.result ?? admitted)?.ok)
|
|
930
|
+
started = await rpcIn(chosenWorkspace, 'agent-session-start', sessionInput, 15_000);
|
|
931
|
+
}
|
|
932
|
+
}
|
|
933
|
+
catch (err) {
|
|
934
|
+
thrown = err ?? new Error('agent-session-start failed');
|
|
935
|
+
}
|
|
831
936
|
}
|
|
832
|
-
|
|
833
|
-
|
|
937
|
+
const outcome = holdsWorkSessions(chosenWorkspace)
|
|
938
|
+
? loginSessionOutcome(started, thrown)
|
|
939
|
+
: { kind: 'unreachable', reason: 'a store keeps no agent work sessions' };
|
|
940
|
+
if (outcome.kind === 'started' && scope) {
|
|
941
|
+
const { updateSessionBinding } = await import('../session-binding.js');
|
|
942
|
+
updateSessionBinding(scope, { sessionId: outcome.sessionId });
|
|
834
943
|
}
|
|
835
|
-
const outcome = loginSessionOutcome(started, thrown);
|
|
836
944
|
if (outcome.kind === 'started') {
|
|
837
945
|
saveAgentSession(name, outcome.sessionId);
|
|
838
946
|
/**
|
|
@@ -876,8 +984,11 @@ async function agentLogin(args, values) {
|
|
|
876
984
|
if (scope) {
|
|
877
985
|
const resolved = agentForScope(scope);
|
|
878
986
|
if (resolved === name) {
|
|
879
|
-
console.log(`
|
|
880
|
-
console.log(dim('
|
|
987
|
+
console.log(` This session ${cyan(scope)}`);
|
|
988
|
+
console.log(` Agent ${cyan(name)}${dim(' every beehaven command here runs as this agent — nothing to pass per call')}`);
|
|
989
|
+
console.log(` Workspace ${cyan(await workspaceLabel(chosenWorkspace))}${dim(' switch: beehaven connect <workspace>')}`);
|
|
990
|
+
console.log(` Env ${cyan(sessionEnv)}${dim(' calls are refused if the daemon moves to another env')}`);
|
|
991
|
+
console.log(dim(' Other sessions and terminal tabs keep their own agent and workspace.'));
|
|
881
992
|
}
|
|
882
993
|
else {
|
|
883
994
|
console.error(` ${yellow('⚠')} Could not bind this session (${scope}) to ${name}; calls resolve to ${resolved ?? 'nobody'}.`);
|
|
@@ -886,9 +997,10 @@ async function agentLogin(args, values) {
|
|
|
886
997
|
}
|
|
887
998
|
}
|
|
888
999
|
else {
|
|
889
|
-
|
|
1000
|
+
// No per-session handle: a terminal that does not say which tab it is.
|
|
1001
|
+
console.log(' This terminal has no session id, so the sign-in is shared by every such terminal.');
|
|
1002
|
+
console.log(' To keep it to THIS terminal:');
|
|
890
1003
|
console.log(` ${cyan(`eval "$(beehaven agent use ${name})"`)}`);
|
|
891
|
-
console.log(dim(` or: export BEEHAVEN_AGENT=${name}`));
|
|
892
1004
|
}
|
|
893
1005
|
}
|
|
894
1006
|
console.log('');
|
|
@@ -1159,6 +1271,14 @@ function agentWhoami(values) {
|
|
|
1159
1271
|
console.log(` birthday: ${r.birthday ? formatBirthday(r.birthday) : 'unknown (created before birthdays)'}`);
|
|
1160
1272
|
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'}`);
|
|
1161
1273
|
console.log(` expires: ${formatRemaining(r.expiresAt)}`);
|
|
1274
|
+
if (active.session) {
|
|
1275
|
+
console.log(` session: ${active.session.scope} (work session ${active.session.sessionId ? active.session.sessionId.slice(0, 8) : 'none'})`);
|
|
1276
|
+
console.log(` workspace: ${active.session.workspace}`);
|
|
1277
|
+
console.log(` env: ${active.session.env}`);
|
|
1278
|
+
}
|
|
1279
|
+
else {
|
|
1280
|
+
console.log(dim(' session: none bound here — `beehaven agent login ' + active.name + '` binds this session'));
|
|
1281
|
+
}
|
|
1162
1282
|
if (active.expired)
|
|
1163
1283
|
console.log(dim(` refresh: beehaven agent login ${active.name}`));
|
|
1164
1284
|
console.log('');
|
|
@@ -1184,6 +1304,53 @@ async function agentStatus(args, values) {
|
|
|
1184
1304
|
}
|
|
1185
1305
|
console.log(` ${green('✓')} ${cyan(name)} status: ${String(result.message ?? message)}`);
|
|
1186
1306
|
}
|
|
1307
|
+
// ── connect, for a signed-in session ─────────────────
|
|
1308
|
+
/**
|
|
1309
|
+
* `beehaven connect <address>` in a signed-in session (AWO-394): move THIS session — its cursor,
|
|
1310
|
+
* its work session and its binding — and nothing else. The cursor file is already this session's;
|
|
1311
|
+
* this moves the work session to the workspace it now works in (a session lives in one workspace's
|
|
1312
|
+
* DO, and its id used to ride on calls to others), and remembers the workspace on the identity so
|
|
1313
|
+
* the next login starts there.
|
|
1314
|
+
*/
|
|
1315
|
+
export async function moveAgentSession(address) {
|
|
1316
|
+
const { activeSessionBinding } = await import('../agent-state.js');
|
|
1317
|
+
const { updateSessionBinding, describeAddress } = await import('../session-binding.js');
|
|
1318
|
+
const bound = activeSessionBinding();
|
|
1319
|
+
if (!bound)
|
|
1320
|
+
return;
|
|
1321
|
+
const record = loadAgent(bound.agent);
|
|
1322
|
+
if (!record)
|
|
1323
|
+
return;
|
|
1324
|
+
if (bound.workspace === address)
|
|
1325
|
+
return;
|
|
1326
|
+
if (bound.sessionId)
|
|
1327
|
+
await endSessionIn(bound.workspace, bound.sessionId, record.agentId);
|
|
1328
|
+
let sessionId;
|
|
1329
|
+
if (holdsWorkSessions(address)) {
|
|
1330
|
+
const input = { agentId: record.agentId, client: 'cli', expiresAt: record.expiresAt };
|
|
1331
|
+
const { loginSessionOutcome } = await import('../agent-state.js');
|
|
1332
|
+
try {
|
|
1333
|
+
let r = await rpcIn(address, 'agent-session-start', input, 15_000);
|
|
1334
|
+
let o = loginSessionOutcome(r);
|
|
1335
|
+
if (o.kind === 'refused' && address !== 'workspace' && /unknown agent|not admitted|unknown-agent/i.test(`${o.scope} ${o.reason}`)) {
|
|
1336
|
+
const admitted = await rpcIn(address, 'agent-admit', { agentId: record.agentId }, 20_000).catch(() => null);
|
|
1337
|
+
if ((admitted?.result ?? admitted)?.ok) {
|
|
1338
|
+
console.error(dim(` ${bound.agent} joined this workspace.`));
|
|
1339
|
+
r = await rpcIn(address, 'agent-session-start', input, 15_000);
|
|
1340
|
+
o = loginSessionOutcome(r);
|
|
1341
|
+
}
|
|
1342
|
+
}
|
|
1343
|
+
if (o.kind === 'started')
|
|
1344
|
+
sessionId = o.sessionId;
|
|
1345
|
+
else if (o.kind === 'refused')
|
|
1346
|
+
console.error(` ${yellow('⚠')} ${bound.agent} has no work session here: ${o.reason}`);
|
|
1347
|
+
}
|
|
1348
|
+
catch { /* unreachable — calls still route; the next login opens a session */ }
|
|
1349
|
+
}
|
|
1350
|
+
updateSessionBinding(bound.scope, { workspace: address, sessionId });
|
|
1351
|
+
saveAgent(bound.agent, { ...record, lastWorkspace: address });
|
|
1352
|
+
console.log(dim(` Session ${bound.scope} (${bound.agent}) now works in ${await workspaceLabel(address)}${sessionId ? ` · work session ${sessionId.slice(0, 8)}` : ''}. Other sessions are unaffected.`));
|
|
1353
|
+
}
|
|
1187
1354
|
// ── handoff ──────────────────────────────────────────
|
|
1188
1355
|
/**
|
|
1189
1356
|
* End the current work session WITH a note — the one thing the next login (this agent's, or the
|
|
@@ -1203,6 +1370,22 @@ async function agentHandoff(args, values) {
|
|
|
1203
1370
|
process.exit(1);
|
|
1204
1371
|
}
|
|
1205
1372
|
const record = loadAgent(name);
|
|
1373
|
+
const { activeSessionBinding } = await import('../agent-state.js');
|
|
1374
|
+
const bound = activeSessionBinding(name);
|
|
1375
|
+
if (bound?.sessionId && record) {
|
|
1376
|
+
// AWO-394 — hand off THIS session, where it lives; other sessions of the name are untouched.
|
|
1377
|
+
if (!await endSessionIn(bound.workspace, bound.sessionId, record.agentId, summary)) {
|
|
1378
|
+
console.error(` ${yellow('⚠')} The workspace did not accept the handoff; the session is still open.`);
|
|
1379
|
+
process.exitCode = 1;
|
|
1380
|
+
return;
|
|
1381
|
+
}
|
|
1382
|
+
const { updateSessionBinding } = await import('../session-binding.js');
|
|
1383
|
+
updateSessionBinding(bound.scope, { sessionId: undefined });
|
|
1384
|
+
if (record.sessionId === bound.sessionId)
|
|
1385
|
+
clearAgentSessionId(name);
|
|
1386
|
+
console.log(` ${green('✓')} Handed off session ${cyan(bound.sessionId.slice(0, 8))} — the next login for ${cyan(name)} reads this first.`);
|
|
1387
|
+
return;
|
|
1388
|
+
}
|
|
1206
1389
|
if (!record?.sessionId) {
|
|
1207
1390
|
console.error(` ${yellow('⚠')} ${cyan(name)} has no open work session on this device.`);
|
|
1208
1391
|
console.error(dim(` Start one with: beehaven agent login ${name} --label "<work>" --client cli`));
|
|
@@ -1236,18 +1419,42 @@ async function agentLogout(args, values) {
|
|
|
1236
1419
|
// it stays in the workspace (source of truth) AND on this device, so you can
|
|
1237
1420
|
// log back in. To remove the current workspace projection use `agent delete <name>`;
|
|
1238
1421
|
// to remove the portable account identity use `agent delete <name> --account --yes`.
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1422
|
+
/**
|
|
1423
|
+
* ⚠ AWO-394 — signing out ends THIS session (its binding, its work session, its cursor) and
|
|
1424
|
+
* leaves every other session signed in as the same name running. It used to clear the shared
|
|
1425
|
+
* record's session, which locked every other terminal using the name out at once.
|
|
1426
|
+
*/
|
|
1427
|
+
const { activeSessionBinding, unbindAgentScope } = await import('../agent-state.js');
|
|
1428
|
+
const { clearSessionBinding, listSessionBindings } = await import('../session-binding.js');
|
|
1429
|
+
const bound = activeSessionBinding(name);
|
|
1430
|
+
if (bound) {
|
|
1431
|
+
if (bound.sessionId)
|
|
1432
|
+
await endSessionIn(bound.workspace, bound.sessionId, record.agentId);
|
|
1433
|
+
clearSessionBinding(bound.scope);
|
|
1434
|
+
// …and this scope stops resolving to the name, or the next call here would still act as it.
|
|
1435
|
+
unbindAgentScope(name, bound.scope);
|
|
1436
|
+
if (record.sessionId && record.sessionId === bound.sessionId)
|
|
1437
|
+
clearAgentSessionId(name);
|
|
1438
|
+
}
|
|
1439
|
+
else {
|
|
1440
|
+
await endWorkspaceSession(record);
|
|
1441
|
+
clearAgentSession(name);
|
|
1442
|
+
}
|
|
1443
|
+
const others = listSessionBindings().filter((b) => b.agent === name);
|
|
1444
|
+
if (!others.length) {
|
|
1445
|
+
try {
|
|
1446
|
+
await rpcOnce('agent:disconnect', { agentId: record.agentId ?? name }, 5_000);
|
|
1447
|
+
}
|
|
1448
|
+
catch {
|
|
1449
|
+
/* daemon down — nothing to close */
|
|
1450
|
+
}
|
|
1245
1451
|
}
|
|
1246
|
-
|
|
1247
|
-
|
|
1452
|
+
console.log(` ${green('✓')} Signed out agent "${name}" ${bound ? 'in this session' : ''} — session ended, identity kept.`);
|
|
1453
|
+
if (others.length)
|
|
1454
|
+
console.log(dim(` ${others.length} other session${others.length === 1 ? ' is' : 's are'} still signed in as ${name}.`));
|
|
1248
1455
|
console.log(dim(` Log back in: beehaven agent login ${name} · workspace remove: beehaven agent delete ${name}`));
|
|
1249
|
-
if (
|
|
1250
|
-
console.log(dim(`
|
|
1456
|
+
if (process.env.BEEHAVEN_AGENT === name && !values.agent) {
|
|
1457
|
+
console.log(dim(` BEEHAVEN_AGENT still names ${name} in this shell — run: unset BEEHAVEN_AGENT`));
|
|
1251
1458
|
}
|
|
1252
1459
|
}
|
|
1253
1460
|
// ── delete (destructive — removes the identity from the workspace + device) ──
|