beehaven 0.8.22 → 0.8.27
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-state.js +19 -0
- package/dist/browser-login.js +283 -0
- package/dist/build-channel.js +2 -2
- package/dist/cli.js +83 -269
- package/dist/commands/agent-instructions.js +182 -0
- package/dist/commands/agent.js +191 -60
- package/dist/commands/canvas-image.js +57 -3
- package/dist/commands/canvas.js +1 -1
- package/dist/commands/compose.js +2 -2
- package/dist/commands/daemon-cmd.js +32 -0
- package/dist/commands/help.js +2 -0
- package/dist/commands/self-update.js +2 -2
- package/dist/commands/workspace.js +24 -3
- package/dist/daemon.js +9 -9
- package/dist/folder-sync.js +1 -1
- package/dist/local-app-manager.js +1 -1
- package/dist/reserved-type.js +60 -0
- package/dist/setup-relay.js +20 -235
- package/package.json +1 -1
- package/scripts/sync-skills.mjs +1 -0
- package/skills/README.md +20 -18
- package/skills/brand-identity/SKILL.md +248 -0
- package/skills/clearly-agent/SKILL.md +28 -7
- package/skills/clearly-canvas/SKILL.md +48 -2
- package/skills/clearly-canvas/authoring.md +20 -9
- package/skills/design-craft/SKILL.md +4 -0
- package/skills/index.json +10 -3
package/dist/commands/agent.js
CHANGED
|
@@ -25,7 +25,7 @@ import { readFileSync, writeFileSync, mkdirSync, existsSync } from 'node:fs';
|
|
|
25
25
|
import { join, dirname } from 'node:path';
|
|
26
26
|
import { homedir } from 'node:os';
|
|
27
27
|
import { rpcOnce } from '../ipc-client.js';
|
|
28
|
-
import { saveAgent, loadAgent, listAgents, deleteAgent, findAgentByAvatar, isValidAgentName, resolveActiveAgent, activeAgentName, saveAgentSession, clearAgentSession, decodeAgentTokenClaims, } from '../agent-state.js';
|
|
28
|
+
import { saveAgent, loadAgent, listAgents, deleteAgent, findAgentByAvatar, isValidAgentName, resolveActiveAgent, activeAgentName, saveAgentSession, clearAgentSession, clearAgentSessionId, decodeAgentTokenClaims, } from '../agent-state.js';
|
|
29
29
|
const VALID_KINDS = ['claude-code', 'daemon-agent', 'clearly-ai'];
|
|
30
30
|
export const FREE_SKINS = [
|
|
31
31
|
'classic',
|
|
@@ -64,6 +64,16 @@ function normalizeCliAvatarSkin(raw) {
|
|
|
64
64
|
* socket alone cannot update the Durable Object row, so logout used to leave an `active` session
|
|
65
65
|
* behind until the server's idle sweep noticed it. Best-effort keeps offline logout usable; the
|
|
66
66
|
* 30-minute server reaper is the fallback when the relay is unavailable. */
|
|
67
|
+
/**
|
|
68
|
+
* End the agent's workspace work session.
|
|
69
|
+
*
|
|
70
|
+
* ⚠⚠ `summary` IS THE AGENT'S HANDOFF NOTE, NEVER A LIFECYCLE REASON (AWO-225). The next login's
|
|
71
|
+
* brief prints a session's summary as its payload — "what I did, what's next". Every lifecycle end
|
|
72
|
+
* used to pass its reason here ("Replaced by a new login session for this agent identity.",
|
|
73
|
+
* "Agent explicitly logged out."), so the payload slot only ever held boilerplate. Worse, the
|
|
74
|
+
* Worker accepts a summary on an ALREADY-ENDED session, so a login after `agent handoff` replaced
|
|
75
|
+
* the agent's real note with "Replaced by a new login session". Only `agent handoff` passes one.
|
|
76
|
+
*/
|
|
67
77
|
async function endWorkspaceSession(record, summary) {
|
|
68
78
|
const sessionId = String(record?.sessionId ?? '').trim();
|
|
69
79
|
if (!sessionId)
|
|
@@ -72,7 +82,7 @@ async function endWorkspaceSession(record, summary) {
|
|
|
72
82
|
const response = await rpcOnce('agent-session-end', {
|
|
73
83
|
id: sessionId,
|
|
74
84
|
...(record?.agentId ? { agentId: record.agentId } : {}),
|
|
75
|
-
summary,
|
|
85
|
+
...(summary?.trim() ? { summary: summary.trim() } : {}),
|
|
76
86
|
}, 5_000);
|
|
77
87
|
const result = response?.result ?? response;
|
|
78
88
|
return result?.ok !== false && !result?.error;
|
|
@@ -106,6 +116,8 @@ export async function cmdAgentSub(positionals, values) {
|
|
|
106
116
|
return agentStatus(rest, values);
|
|
107
117
|
case 'logout':
|
|
108
118
|
return agentLogout(rest, values);
|
|
119
|
+
case 'handoff':
|
|
120
|
+
return agentHandoff(rest, values);
|
|
109
121
|
case 'delete':
|
|
110
122
|
case 'remove':
|
|
111
123
|
return agentDelete(rest, values);
|
|
@@ -134,6 +146,7 @@ export function isAgentIdentitySub(sub) {
|
|
|
134
146
|
sub === 'whoami' ||
|
|
135
147
|
sub === 'status' ||
|
|
136
148
|
sub === 'logout' ||
|
|
149
|
+
sub === 'handoff' ||
|
|
137
150
|
sub === 'delete' ||
|
|
138
151
|
sub === 'remove' ||
|
|
139
152
|
sub === 'rebind' ||
|
|
@@ -161,6 +174,7 @@ function printAgentUsage() {
|
|
|
161
174
|
console.log(' list list stored identities');
|
|
162
175
|
console.log(' whoami show the active identity');
|
|
163
176
|
console.log(' status [<name>] --message <text> set the persistent desktop status label');
|
|
177
|
+
console.log(' handoff "<summary>" end the work session with a note the next login reads first');
|
|
164
178
|
console.log(' logout [<name>] end the session, KEEP the identity (default: active)');
|
|
165
179
|
console.log(' delete <name> archive this workspace projection + remove this device copy');
|
|
166
180
|
console.log(' delete <name> --account --yes delete the portable account identity + this device copy');
|
|
@@ -687,7 +701,7 @@ async function agentLogin(args, values) {
|
|
|
687
701
|
* actually began. `existingRecord` is read before the write for exactly this.
|
|
688
702
|
*/
|
|
689
703
|
const birthday = existingRecord?.birthday ?? new Date().toISOString();
|
|
690
|
-
const previousSessionEnded = await endWorkspaceSession(existingRecord
|
|
704
|
+
const previousSessionEnded = await endWorkspaceSession(existingRecord);
|
|
691
705
|
/**
|
|
692
706
|
* ⚠⚠ FILL THE LIFETIME FIELDS FROM THE TOKEN — the cloud mint path does not
|
|
693
707
|
* return them and a record without `expiresAt` can never be seen as expired.
|
|
@@ -845,7 +859,11 @@ async function agentLogin(args, values) {
|
|
|
845
859
|
console.error(` ${yellow('⚠')} Signed in, but this workspace refused ${cyan(name)}: ${outcome.reason}`);
|
|
846
860
|
if (/revoked|not active|unknown agent|not admitted/i.test(`${outcome.scope} ${outcome.reason}`)) {
|
|
847
861
|
console.error(dim(` The agent is archived or not admitted here. Restore it in Settings → Agent access,`));
|
|
848
|
-
|
|
862
|
+
// ⚠ The workspace names the live identity in its refusal (PPO-190). The old advice —
|
|
863
|
+
// `beehaven call agent-list` — ran AS this refused identity and was refused in turn.
|
|
864
|
+
if (!/Log in as one:/.test(outcome.reason)) {
|
|
865
|
+
console.error(dim(` or log in as an active agent: beehaven agent login <name> (the Settings → Agent access page lists them).`));
|
|
866
|
+
}
|
|
849
867
|
}
|
|
850
868
|
console.error(dim(` Calls from this session run as ${name} and will be refused — they will NOT fall back to another agent.`));
|
|
851
869
|
process.exitCode = 1;
|
|
@@ -876,104 +894,182 @@ async function agentLogin(args, values) {
|
|
|
876
894
|
console.log('');
|
|
877
895
|
}
|
|
878
896
|
/**
|
|
879
|
-
*
|
|
897
|
+
* Summaries older CLIs wrote when a session ended for a lifecycle reason. They are not handoff
|
|
898
|
+
* notes (AWO-225), so the brief does not print them as one; sessions that carry them still show
|
|
899
|
+
* their label and counts. Kept until no client in the field writes them.
|
|
900
|
+
*/
|
|
901
|
+
const LIFECYCLE_SUMMARIES = new Set([
|
|
902
|
+
'Replaced by a new login session for this agent identity.',
|
|
903
|
+
'Agent explicitly logged out.',
|
|
904
|
+
'Agent identity deleted.',
|
|
905
|
+
'Account agent identity deleted.',
|
|
906
|
+
]);
|
|
907
|
+
const briefAgo = (ts, now) => {
|
|
908
|
+
const d = now - ts;
|
|
909
|
+
if (d < 60_000)
|
|
910
|
+
return 'just now';
|
|
911
|
+
if (d < 3_600_000)
|
|
912
|
+
return `${Math.round(d / 60_000)}m ago`;
|
|
913
|
+
if (d < 86_400_000)
|
|
914
|
+
return `${Math.round(d / 3_600_000)}h ago`;
|
|
915
|
+
return `${Math.round(d / 86_400_000)}d ago`;
|
|
916
|
+
};
|
|
917
|
+
const oneLine = (v, max) => {
|
|
918
|
+
const t = String(v ?? '').replace(/\s+/g, ' ').trim();
|
|
919
|
+
return t.length > max ? `${t.slice(0, max - 1)}…` : t;
|
|
920
|
+
};
|
|
921
|
+
/**
|
|
922
|
+
* The login digest, as lines (AWO-225: pure, so it can be tested without a workspace).
|
|
880
923
|
*
|
|
881
924
|
* Everything here answers a question a returning colleague asks before touching anything, and
|
|
882
925
|
* NOTHING here is printed when it is empty — a brief padded with "0 sessions · 0 memories ·
|
|
883
926
|
* nothing assigned" is a wall an agent learns to skip, which costs the one thing the digest
|
|
884
927
|
* has to buy: being read.
|
|
928
|
+
*
|
|
929
|
+
* Order is the order of obligation: what someone asked you, what you left yourself, what is yours
|
|
930
|
+
* to do, what is waiting on a person, then where you were and what moved while you were away.
|
|
885
931
|
*/
|
|
886
|
-
function
|
|
932
|
+
export function formatBrief(brief, now = Date.now()) {
|
|
933
|
+
const out = [];
|
|
887
934
|
if (!brief)
|
|
888
|
-
return;
|
|
935
|
+
return out;
|
|
936
|
+
const section = (title, count) => {
|
|
937
|
+
out.push('');
|
|
938
|
+
out.push(` ${bold(title)}${count === undefined ? '' : ` ${dim(`(${count})`)}`}`);
|
|
939
|
+
};
|
|
940
|
+
const ws = brief.workspace ?? {};
|
|
889
941
|
const sessions = brief.sessions?.recent ?? [];
|
|
890
942
|
const assigned = brief.assignedToMe ?? [];
|
|
891
943
|
const pending = brief.awaitingApproval ?? [];
|
|
892
944
|
const memories = brief.memories ?? [];
|
|
893
945
|
const focus = brief.focus ?? [];
|
|
946
|
+
const recents = brief.recents ?? [];
|
|
947
|
+
const drafts = brief.drafts ?? [];
|
|
948
|
+
const changed = ws.changed ?? [];
|
|
949
|
+
const otherAgents = ws.otherAgents ?? [];
|
|
950
|
+
const openDecisions = ws.decisions?.open ?? [];
|
|
951
|
+
const pressure = brief.reviewPressure;
|
|
894
952
|
/**
|
|
895
953
|
* ⚠ SS-41 — ADDRESSED TO YOU, AND PRINTED FIRST. Voicemail and @mentions are somebody speaking
|
|
896
954
|
* TO this agent; everything else in the brief is ambient. Both stay until acknowledged.
|
|
897
955
|
*/
|
|
898
|
-
const mentions =
|
|
899
|
-
const voicemail =
|
|
956
|
+
const mentions = ws.mentions ?? [];
|
|
957
|
+
const voicemail = ws.voicemail ?? [];
|
|
900
958
|
/** WIB-1 — people and agents writing TO this agent. Oldest first; reading one clears it. */
|
|
901
|
-
const inbox =
|
|
959
|
+
const inbox = ws.inboxThreads ?? [];
|
|
902
960
|
if (mentions.length || voicemail.length || inbox.length) {
|
|
903
|
-
|
|
904
|
-
console.log(` ${bold('Addressed to you')} ${dim(`(${mentions.length + voicemail.length + inbox.length})`)}`);
|
|
961
|
+
section('Addressed to you', mentions.length + voicemail.length + inbox.length);
|
|
905
962
|
for (const t of inbox.slice(0, 5)) {
|
|
906
|
-
|
|
907
|
-
|
|
963
|
+
out.push(` ${dim('·')} ${cyan('message')} from ${t.from}: “${t.subject}” ${dim(`${t.unread} unread · ${t.threadId}`)}`);
|
|
964
|
+
out.push(` ${oneLine(t.preview, 120)}`);
|
|
908
965
|
}
|
|
909
966
|
if (inbox.length)
|
|
910
|
-
|
|
967
|
+
out.push(dim(` Read with \`beehaven inbox read <id>\`, answer with \`beehaven inbox reply <id> "…"\`, then \`beehaven inbox done <id>\`.`));
|
|
911
968
|
for (const v of voicemail.slice(0, 3)) {
|
|
912
|
-
|
|
969
|
+
out.push(` ${dim('·')} ${cyan('voicemail')} from ${v.from}: ${oneLine(v.text, 100)}`);
|
|
913
970
|
}
|
|
914
971
|
for (const m of mentions.slice(0, 5)) {
|
|
915
972
|
const on = m.on ?? {};
|
|
916
|
-
|
|
917
|
-
|
|
973
|
+
out.push(` ${dim('·')} ${cyan('@mention')} from ${m.from} on ${on.kind || 'item'} “${on.title || on.id || ''}” ${dim(String(m.id).slice(0, 8))}`);
|
|
974
|
+
out.push(` ${oneLine(m.text, 120)}`);
|
|
918
975
|
}
|
|
919
976
|
if (mentions.length)
|
|
920
|
-
|
|
977
|
+
out.push(dim(` Act, reply with \`beehaven comment <ref> "…"\`, then \`beehaven mention ack <id>\`.`));
|
|
921
978
|
}
|
|
922
|
-
|
|
923
|
-
if (!mentions.length && !voicemail.length && !inbox.length)
|
|
924
|
-
console.log(dim(' First visit — nothing to catch up on.'));
|
|
925
|
-
return;
|
|
926
|
-
}
|
|
927
|
-
const ago = (ts) => {
|
|
928
|
-
const d = Date.now() - ts;
|
|
929
|
-
if (d < 60_000)
|
|
930
|
-
return 'just now';
|
|
931
|
-
if (d < 3_600_000)
|
|
932
|
-
return `${Math.round(d / 60_000)}m ago`;
|
|
933
|
-
if (d < 86_400_000)
|
|
934
|
-
return `${Math.round(d / 3_600_000)}h ago`;
|
|
935
|
-
return `${Math.round(d / 86_400_000)}d ago`;
|
|
936
|
-
};
|
|
979
|
+
// ── What you left yourself. The handoff note is the payload; counts are context. ──
|
|
937
980
|
if (sessions.length) {
|
|
938
|
-
|
|
939
|
-
console.log(` ${bold('Last time you were here')}`);
|
|
981
|
+
section('Last time you were here');
|
|
940
982
|
for (const s of sessions.slice(0, 3)) {
|
|
941
|
-
const when = s.startedAt ?
|
|
983
|
+
const when = s.startedAt ? briefAgo(s.startedAt, now) : '';
|
|
942
984
|
const counts = `${s.actions} action${s.actions === 1 ? '' : 's'}${s.errors ? `, ${s.errors} error${s.errors === 1 ? '' : 's'}` : ''}`;
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
if (
|
|
946
|
-
|
|
947
|
-
}
|
|
948
|
-
}
|
|
949
|
-
if (focus.length) {
|
|
950
|
-
console.log('');
|
|
951
|
-
console.log(` ${bold('You were working on')}`);
|
|
952
|
-
for (const f of focus.slice(0, 3)) {
|
|
953
|
-
console.log(` ${dim('·')} ${f.title || f.id} ${dim(`(${f.kind}, ${f.selected.length} selected)`)}`);
|
|
985
|
+
out.push(` ${dim('·')} ${s.label || '(unlabelled)'} ${dim(`— ${when}, ${counts}`)}`);
|
|
986
|
+
const note = String(s.summary ?? '').trim();
|
|
987
|
+
if (note && !LIFECYCLE_SUMMARIES.has(note))
|
|
988
|
+
out.push(` ${cyan('handoff:')} ${oneLine(note, 400)}`);
|
|
954
989
|
}
|
|
955
990
|
}
|
|
956
991
|
if (assigned.length) {
|
|
957
|
-
|
|
958
|
-
console.log(` ${bold('Assigned to you')} ${dim(`(${assigned.length})`)}`);
|
|
992
|
+
section('Assigned to you', assigned.length);
|
|
959
993
|
for (const t of assigned.slice(0, 5)) {
|
|
960
|
-
|
|
994
|
+
out.push(` ${dim('·')} ${t.key ? cyan(t.key) + ' ' : ''}${t.title} ${dim(`[${t.status}]`)}`);
|
|
961
995
|
}
|
|
962
996
|
}
|
|
963
997
|
if (pending.length) {
|
|
964
|
-
|
|
965
|
-
console.log(` ${bold('Waiting on a human')} ${dim(`(${pending.length})`)}`);
|
|
998
|
+
section('Waiting on a human', pending.length);
|
|
966
999
|
for (const c of pending.slice(0, 4)) {
|
|
967
|
-
|
|
1000
|
+
out.push(` ${dim('·')} ${c.title || c.id} ${dim(c.kind ? `(${c.kind})` : '')}`);
|
|
1001
|
+
}
|
|
1002
|
+
}
|
|
1003
|
+
/** WDP-9 — decisions still waiting on a person. Current decisions are context the agent can
|
|
1004
|
+
* re-read with `agent-brief`; open ones change what it should do next. */
|
|
1005
|
+
if (openDecisions.length) {
|
|
1006
|
+
section('Open decisions', openDecisions.length);
|
|
1007
|
+
for (const d of openDecisions.slice(0, 4)) {
|
|
1008
|
+
out.push(` ${dim('·')} ${d.key ? cyan(d.key) + ' ' : ''}${d.title} ${dim(`[${d.state}]`)}`);
|
|
1009
|
+
}
|
|
1010
|
+
}
|
|
1011
|
+
// ── Where you were. Focus is what you had selected; recents are the things themselves. ──
|
|
1012
|
+
if (focus.length) {
|
|
1013
|
+
section('You were working on');
|
|
1014
|
+
for (const f of focus.slice(0, 3)) {
|
|
1015
|
+
out.push(` ${dim('·')} ${f.title || f.id} ${dim(`(${f.kind}, ${f.selected.length} selected)`)}`);
|
|
1016
|
+
}
|
|
1017
|
+
}
|
|
1018
|
+
const shown = new Set([...focus, ...assigned].map((x) => String(x?.id ?? '')));
|
|
1019
|
+
const whereLeft = recents.filter((r) => r?.id && !shown.has(String(r.id)) && r.title).slice(0, 4);
|
|
1020
|
+
if (whereLeft.length) {
|
|
1021
|
+
section('Where you left off');
|
|
1022
|
+
for (const r of whereLeft) {
|
|
1023
|
+
const what = [r.kind, [r.reason || 'touched', r.at ? briefAgo(r.at, now) : ''].filter(Boolean).join(' ')].filter(Boolean).join(', ');
|
|
1024
|
+
out.push(` ${dim('·')} ${oneLine(r.title, 80)} ${dim(`(${what})`)}`);
|
|
1025
|
+
}
|
|
1026
|
+
}
|
|
1027
|
+
// ── What moved while you were away, and who moved it (`by` is the useful field). ──
|
|
1028
|
+
if (changed.length) {
|
|
1029
|
+
const waiting = ws.waiting ?? {};
|
|
1030
|
+
const total = Number(waiting.changedByPeople ?? 0) + Number(waiting.changedByAgents ?? 0) || changed.length;
|
|
1031
|
+
section('Changed since you were here', total);
|
|
1032
|
+
for (const c of changed.slice(0, 4)) {
|
|
1033
|
+
const who = c.actor ? `${c.actor}${c.by === 'agent' ? ' (agent)' : ''}` : c.by === 'agent' ? 'an agent' : 'a person';
|
|
1034
|
+
out.push(` ${dim('·')} ${oneLine(c.title || c.id, 70)} ${dim(`(${c.kind}) — ${c.op || 'changed'} by ${who}${c.at ? `, ${briefAgo(c.at, now)}` : ''}`)}`);
|
|
1035
|
+
}
|
|
1036
|
+
}
|
|
1037
|
+
if (otherAgents.length) {
|
|
1038
|
+
const names = otherAgents.slice(0, 4).map((a) => `${a.agentId} ${dim(`(${a.actions} action${a.actions === 1 ? '' : 's'}${a.last ? `, ${briefAgo(a.last, now)}` : ''})`)}`);
|
|
1039
|
+
out.push('');
|
|
1040
|
+
out.push(` ${bold('Also working here')} ${names.join(', ')}`);
|
|
1041
|
+
}
|
|
1042
|
+
// ── What you have not shared yet. Filing is sharing; an unfiled draft is invisible to the team. ──
|
|
1043
|
+
if (drafts.length) {
|
|
1044
|
+
section('Your unfiled drafts', drafts.length);
|
|
1045
|
+
for (const d of drafts.slice(0, 3)) {
|
|
1046
|
+
out.push(` ${dim('·')} ${d.key ? cyan(d.key) + ' ' : ''}${oneLine(d.title, 80)}${d.updatedAt ? dim(` — ${briefAgo(d.updatedAt, now)}`) : ''}`);
|
|
968
1047
|
}
|
|
1048
|
+
out.push(dim(' Private to you until filed into a project.'));
|
|
1049
|
+
}
|
|
1050
|
+
if (pressure?.overBudget && pressure.instruction) {
|
|
1051
|
+
out.push('');
|
|
1052
|
+
out.push(` ${yellow('⚠')} ${pressure.instruction}`);
|
|
969
1053
|
}
|
|
970
1054
|
if (memories.length) {
|
|
971
|
-
|
|
972
|
-
console.log(` ${bold('You noted')}`);
|
|
1055
|
+
section('You noted');
|
|
973
1056
|
for (const m of memories.slice(0, 4)) {
|
|
974
|
-
|
|
1057
|
+
out.push(` ${dim('·')} ${cyan(m.key)}: ${oneLine(m.value, 90)}`);
|
|
975
1058
|
}
|
|
976
1059
|
}
|
|
1060
|
+
if (!out.length)
|
|
1061
|
+
out.push(dim(' First visit — nothing to catch up on.'));
|
|
1062
|
+
// The one way a session leaves the next one something to read. Printed every time: a handoff
|
|
1063
|
+
// you have to remember to write is a handoff most sessions will not leave.
|
|
1064
|
+
out.push('');
|
|
1065
|
+
out.push(dim(` Before you stop: beehaven agent handoff "what you did · what is next"`));
|
|
1066
|
+
return out;
|
|
1067
|
+
}
|
|
1068
|
+
function printBrief(brief) {
|
|
1069
|
+
if (!brief)
|
|
1070
|
+
return;
|
|
1071
|
+
for (const line of formatBrief(brief))
|
|
1072
|
+
console.log(line);
|
|
977
1073
|
}
|
|
978
1074
|
// ── use (prints the export line for eval) ────────────
|
|
979
1075
|
async function agentUse(args) {
|
|
@@ -1088,6 +1184,41 @@ async function agentStatus(args, values) {
|
|
|
1088
1184
|
}
|
|
1089
1185
|
console.log(` ${green('✓')} ${cyan(name)} status: ${String(result.message ?? message)}`);
|
|
1090
1186
|
}
|
|
1187
|
+
// ── handoff ──────────────────────────────────────────
|
|
1188
|
+
/**
|
|
1189
|
+
* End the current work session WITH a note — the one thing the next login (this agent's, or the
|
|
1190
|
+
* colleague it hands to) reads first, under "Last time you were here" (AWO-225).
|
|
1191
|
+
*
|
|
1192
|
+
* Unlike `logout`, the identity stays signed in and bound: an agent hands off a stretch of work,
|
|
1193
|
+
* not its seat. Only the ended session's id is forgotten (`clearAgentSessionId`), so the next
|
|
1194
|
+
* `agent login` starts a fresh session and this terminal still resolves to this agent.
|
|
1195
|
+
*/
|
|
1196
|
+
async function agentHandoff(args, values) {
|
|
1197
|
+
const name = activeAgentName(values);
|
|
1198
|
+
const summary = (args.join(' ') || String(values.summary ?? '')).trim();
|
|
1199
|
+
if (!name || !summary) {
|
|
1200
|
+
console.error(' Usage: beehaven agent handoff "<what you did · what is next>" [--agent <name>]');
|
|
1201
|
+
if (!name)
|
|
1202
|
+
console.error(' No active agent: log in first, or pass --agent <name>.');
|
|
1203
|
+
process.exit(1);
|
|
1204
|
+
}
|
|
1205
|
+
const record = loadAgent(name);
|
|
1206
|
+
if (!record?.sessionId) {
|
|
1207
|
+
console.error(` ${yellow('⚠')} ${cyan(name)} has no open work session on this device.`);
|
|
1208
|
+
console.error(dim(` Start one with: beehaven agent login ${name} --label "<work>" --client cli`));
|
|
1209
|
+
process.exitCode = 1;
|
|
1210
|
+
return;
|
|
1211
|
+
}
|
|
1212
|
+
const ended = await endWorkspaceSession(record, summary);
|
|
1213
|
+
if (!ended) {
|
|
1214
|
+
console.error(` ${yellow('⚠')} The workspace did not accept the handoff; the session is still open.`);
|
|
1215
|
+
console.error(dim(' Check `beehaven status`, then run the same command again.'));
|
|
1216
|
+
process.exitCode = 1;
|
|
1217
|
+
return;
|
|
1218
|
+
}
|
|
1219
|
+
clearAgentSessionId(name);
|
|
1220
|
+
console.log(` ${green('✓')} Handed off session ${cyan(String(record.sessionId).slice(0, 8))} — the next login for ${cyan(name)} reads this first.`);
|
|
1221
|
+
}
|
|
1091
1222
|
// ── logout ───────────────────────────────────────────
|
|
1092
1223
|
async function agentLogout(args, values) {
|
|
1093
1224
|
const name = args[0] ?? activeAgentName(values);
|
|
@@ -1105,7 +1236,7 @@ async function agentLogout(args, values) {
|
|
|
1105
1236
|
// it stays in the workspace (source of truth) AND on this device, so you can
|
|
1106
1237
|
// log back in. To remove the current workspace projection use `agent delete <name>`;
|
|
1107
1238
|
// to remove the portable account identity use `agent delete <name> --account --yes`.
|
|
1108
|
-
await endWorkspaceSession(record
|
|
1239
|
+
await endWorkspaceSession(record);
|
|
1109
1240
|
try {
|
|
1110
1241
|
await rpcOnce('agent:disconnect', { agentId: record.agentId ?? name }, 5_000);
|
|
1111
1242
|
}
|
|
@@ -1130,7 +1261,7 @@ async function agentDelete(args, values) {
|
|
|
1130
1261
|
if (values.account === true)
|
|
1131
1262
|
return agentAccountDelete(args, values);
|
|
1132
1263
|
const record = loadAgent(name);
|
|
1133
|
-
await endWorkspaceSession(record
|
|
1264
|
+
await endWorkspaceSession(record);
|
|
1134
1265
|
// 1. Remove from the WORKSPACE (source of truth) — the `agent-delete` RPC
|
|
1135
1266
|
// soft-deletes (archived). Resolve by the record's agentId, else the name.
|
|
1136
1267
|
let cloud = false;
|
|
@@ -1189,7 +1320,7 @@ async function agentAccountDelete(args, values) {
|
|
|
1189
1320
|
console.error(` ${result.error || `Could not delete account agent "${name}".`}`);
|
|
1190
1321
|
process.exit(1);
|
|
1191
1322
|
}
|
|
1192
|
-
await endWorkspaceSession(record
|
|
1323
|
+
await endWorkspaceSession(record);
|
|
1193
1324
|
try {
|
|
1194
1325
|
await rpcOnce('agent:disconnect', { agentId }, 5_000);
|
|
1195
1326
|
}
|
|
@@ -25,6 +25,50 @@ function coordinateFlag(raw, label) {
|
|
|
25
25
|
throw new Error(`${label} must be a number`);
|
|
26
26
|
return value;
|
|
27
27
|
}
|
|
28
|
+
/**
|
|
29
|
+
* ⚠ NATIVE SIZE IS READ HERE, FROM THE FILE'S OWN HEADER. The docs promise "omit both size flags to
|
|
30
|
+
* use the image's native dimensions", but nothing measured the image, so the server's placeholder
|
|
31
|
+
* box won: a 1032×320 PNG landed as a 400×400 node with its aspect squashed (CDS-26). PNG, GIF,
|
|
32
|
+
* JPEG and WebP carry their size in the first bytes; AVIF (ISO-BMFF) falls back to the server.
|
|
33
|
+
*/
|
|
34
|
+
export function imageSize(buf, mime) {
|
|
35
|
+
try {
|
|
36
|
+
if (mime === 'image/png' && buf.length >= 24 && buf.toString('ascii', 12, 16) === 'IHDR') {
|
|
37
|
+
return { width: buf.readUInt32BE(16), height: buf.readUInt32BE(20) };
|
|
38
|
+
}
|
|
39
|
+
if (mime === 'image/gif' && buf.length >= 10)
|
|
40
|
+
return { width: buf.readUInt16LE(6), height: buf.readUInt16LE(8) };
|
|
41
|
+
if (mime === 'image/webp' && buf.length >= 30 && buf.toString('ascii', 8, 12) === 'WEBP') {
|
|
42
|
+
const kind = buf.toString('ascii', 12, 16);
|
|
43
|
+
if (kind === 'VP8X')
|
|
44
|
+
return { width: 1 + buf.readUIntLE(24, 3), height: 1 + buf.readUIntLE(27, 3) };
|
|
45
|
+
if (kind === 'VP8L') {
|
|
46
|
+
const b = buf.readUInt32LE(21);
|
|
47
|
+
return { width: (b & 0x3fff) + 1, height: ((b >> 14) & 0x3fff) + 1 };
|
|
48
|
+
}
|
|
49
|
+
if (kind === 'VP8 ')
|
|
50
|
+
return { width: buf.readUInt16LE(26) & 0x3fff, height: buf.readUInt16LE(28) & 0x3fff };
|
|
51
|
+
}
|
|
52
|
+
if (mime === 'image/jpeg' && buf[0] === 0xff && buf[1] === 0xd8) {
|
|
53
|
+
let i = 2;
|
|
54
|
+
while (i + 9 < buf.length) {
|
|
55
|
+
if (buf[i] !== 0xff) {
|
|
56
|
+
i++;
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
const marker = buf[i + 1];
|
|
60
|
+
const len = buf.readUInt16BE(i + 2);
|
|
61
|
+
// SOF0–SOF15 except DHT(C4), JPG(C8), DAC(CC) carry the frame size.
|
|
62
|
+
if (marker >= 0xc0 && marker <= 0xcf && marker !== 0xc4 && marker !== 0xc8 && marker !== 0xcc) {
|
|
63
|
+
return { width: buf.readUInt16BE(i + 7), height: buf.readUInt16BE(i + 5) };
|
|
64
|
+
}
|
|
65
|
+
i += 2 + len;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
catch { /* fall back to the server's default */ }
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
28
72
|
function absoluteRelayUrl(url) {
|
|
29
73
|
if (!url.startsWith('/'))
|
|
30
74
|
return url;
|
|
@@ -54,12 +98,19 @@ export async function cmdCanvasAddImage(positionals, values) {
|
|
|
54
98
|
throw new Error(`Image must be 1 byte to 100 MB (${stat.size} bytes given)`);
|
|
55
99
|
const x = coordinateFlag(values.x, '--x');
|
|
56
100
|
const y = coordinateFlag(values.y, '--y');
|
|
57
|
-
|
|
58
|
-
|
|
101
|
+
let width = positiveFlag(values.width, '--width');
|
|
102
|
+
let height = positiveFlag(values.height, '--height');
|
|
59
103
|
if ((x === undefined) !== (y === undefined))
|
|
60
104
|
throw new Error('Pass --x and --y together');
|
|
61
105
|
if ((width === undefined) !== (height === undefined))
|
|
62
106
|
throw new Error('Pass --width and --height together');
|
|
107
|
+
if (width === undefined) {
|
|
108
|
+
const native = imageSize(readFileSync(path), mimeType);
|
|
109
|
+
if (native && native.width > 0 && native.height > 0) {
|
|
110
|
+
width = native.width;
|
|
111
|
+
height = native.height;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
63
114
|
// Refuse an unknown target before putting bytes in storage for a canvas that cannot use them.
|
|
64
115
|
const target = await rpcOnce('composition-detail', { id: compositionId }, 30_000);
|
|
65
116
|
if (target?.error || target?.composition?.id !== compositionId) {
|
|
@@ -93,7 +144,10 @@ export async function cmdCanvasAddImage(positionals, values) {
|
|
|
93
144
|
if (placed?.ok !== true || !placed?.nodeId && !placed?.blockId) {
|
|
94
145
|
throw new Error(`Image uploaded but could not be placed: ${placed?.error ?? 'unknown error'}. Asset key: ${key}`);
|
|
95
146
|
}
|
|
96
|
-
|
|
147
|
+
// ⚠ `src` IS RETURNED — reusing an upload (a brand logo, a second placement) needed a scene read
|
|
148
|
+
// to find the URL the node stores (CDS-27).
|
|
149
|
+
const src = String(placed?.src ?? placed?.url ?? placed?.node?.src ?? absoluteRelayUrl(String(intent.publicUrl)));
|
|
150
|
+
const result = { ok: true, compositionId, nodeId: placed.nodeId ?? placed.blockId, imageFileId: recorded?.id, name, src, ...(width !== undefined ? { width, height } : {}) };
|
|
97
151
|
if (values.json)
|
|
98
152
|
console.log(JSON.stringify(result, null, 2));
|
|
99
153
|
else
|
package/dist/commands/canvas.js
CHANGED
|
@@ -243,7 +243,7 @@ async function cmdCanvasPublishLanding(positionals, values) {
|
|
|
243
243
|
}
|
|
244
244
|
const block = embed?.block ?? embed?.result?.block;
|
|
245
245
|
console.log(` ✓ Published: site comp ${siteCompositionId} embedded as block ${block?.id ?? '?'} on pasteboard ${targetCompositionId}`);
|
|
246
|
-
console.log(` ✓ Open it: https://clearly.sh/c/${targetCompositionId}`);
|
|
246
|
+
console.log(` ✓ Open it: https://www.clearly.sh/c/${targetCompositionId}`);
|
|
247
247
|
return print(embed, values);
|
|
248
248
|
}
|
|
249
249
|
async function cmdCanvasPasteText(positionals, values) {
|
package/dist/commands/compose.js
CHANGED
|
@@ -401,7 +401,7 @@ export async function cmdComposePush(_values) {
|
|
|
401
401
|
}
|
|
402
402
|
console.log('');
|
|
403
403
|
console.log(` Composition live at:`);
|
|
404
|
-
console.log(` https://clearly.sh/app/compositions/${link.compositionId}`);
|
|
404
|
+
console.log(` https://www.clearly.sh/app/compositions/${link.compositionId}`);
|
|
405
405
|
console.log('');
|
|
406
406
|
console.log(` Create a public share link with:`);
|
|
407
407
|
console.log(` beehaven compose share --password <pw>`);
|
|
@@ -466,7 +466,7 @@ export async function cmdComposeShare(values) {
|
|
|
466
466
|
process.exit(1);
|
|
467
467
|
}
|
|
468
468
|
console.log(` \u2713 Share link created`);
|
|
469
|
-
console.log(` https://clearly.sh/r/${res.id}`);
|
|
469
|
+
console.log(` https://www.clearly.sh/r/${res.id}`);
|
|
470
470
|
if (input.password)
|
|
471
471
|
console.log(` Password: ${input.password}`);
|
|
472
472
|
if (input.expiresAt)
|
|
@@ -138,6 +138,38 @@ export async function startHeadlessDaemon(values) {
|
|
|
138
138
|
? ` Ready — socket answering.`
|
|
139
139
|
: ` Started, but not answering yet after ${Math.round(DAEMON_READY_TIMEOUT_MS / 1000)}s. 'beehaven status' shows state.`);
|
|
140
140
|
}
|
|
141
|
+
/**
|
|
142
|
+
* ⚠⚠ WHO IS ACTUALLY SERVING AFTER A RESPAWN. `restart` used to print the PID it spawned and stop
|
|
143
|
+
* there. On a Mac with the Clearly app open, the tray supervisor respawns ITS daemon the moment the
|
|
144
|
+
* old one dies — so the CLI's spawn lost the single-instance race ("Refusing to start — daemon
|
|
145
|
+
* already running"), exited, and the user was told "Daemon restarted (PID 5706)" about a process
|
|
146
|
+
* that no longer existed (observed 2026-10-01; the next call said "Cannot connect to daemon").
|
|
147
|
+
* This waits for a usable socket and reports the PID that is really live.
|
|
148
|
+
*/
|
|
149
|
+
export async function waitForServingDaemon(spawnedPid) {
|
|
150
|
+
const { isDaemonRunning } = await import('../daemon-client.js');
|
|
151
|
+
const alive = (pid) => { if (!pid)
|
|
152
|
+
return false; try {
|
|
153
|
+
process.kill(pid, 0);
|
|
154
|
+
return true;
|
|
155
|
+
}
|
|
156
|
+
catch {
|
|
157
|
+
return false;
|
|
158
|
+
} };
|
|
159
|
+
const deadline = Date.now() + DAEMON_READY_TIMEOUT_MS;
|
|
160
|
+
let ready = false;
|
|
161
|
+
while (Date.now() < deadline) {
|
|
162
|
+
await new Promise((r) => setTimeout(r, DAEMON_POLL_MS));
|
|
163
|
+
if (await isDaemonRunning()) {
|
|
164
|
+
ready = true;
|
|
165
|
+
break;
|
|
166
|
+
}
|
|
167
|
+
// The spawn died and nobody else took over: no point waiting out the clock.
|
|
168
|
+
if (!alive(spawnedPid) && findLiveDaemons().length === 0)
|
|
169
|
+
break;
|
|
170
|
+
}
|
|
171
|
+
return { ready, spawnedAlive: alive(spawnedPid), livePids: findLiveDaemons() };
|
|
172
|
+
}
|
|
141
173
|
export async function cmdDaemon(sub, values) {
|
|
142
174
|
switch (sub) {
|
|
143
175
|
case 'start':
|
package/dist/commands/help.js
CHANGED
|
@@ -1790,6 +1790,7 @@ const PAGES = {
|
|
|
1790
1790
|
agent use <name> — prints \`export BEEHAVEN_AGENT=<name>\`; eval it
|
|
1791
1791
|
agent list — list stored identities, mark the active one
|
|
1792
1792
|
agent whoami — show the active identity + TTL
|
|
1793
|
+
agent handoff "<summary>" — end the work session with a note; the next login prints it first
|
|
1793
1794
|
agent logout [<name>]— end its local session, keep the identity
|
|
1794
1795
|
agent delete <name> — archive the current workspace projection + remove this device copy
|
|
1795
1796
|
agent delete <name> --account --yes
|
|
@@ -1825,6 +1826,7 @@ const PAGES = {
|
|
|
1825
1826
|
{ cmd: 'beehaven say "hi from monet"', note: "speaks via monet's avatar" },
|
|
1826
1827
|
{ cmd: 'beehaven agent rebind monet --avatar studio-2' },
|
|
1827
1828
|
{ cmd: 'beehaven agent whoami' },
|
|
1829
|
+
{ cmd: 'beehaven agent handoff "Fixed AWO-224 login; next: release the CLI"', note: 'leave the next session a note' },
|
|
1828
1830
|
{ cmd: 'beehaven agent "what can you do?"', note: 'REPL one-shot' },
|
|
1829
1831
|
],
|
|
1830
1832
|
related: ['app', 'pipe', 'pwd'],
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* `checkForUpdates` printed, unconditionally:
|
|
7
7
|
*
|
|
8
8
|
* Update available: 0.7.6 → 0.7.8
|
|
9
|
-
* Run curl -fsSL https://clearly.sh/install.sh | sh to update
|
|
9
|
+
* Run curl -fsSL https://www.clearly.sh/install.sh | sh to update
|
|
10
10
|
*
|
|
11
11
|
* For anyone whose `beehaven` comes from the Mac app — which is EVERY ordinary user, since the
|
|
12
12
|
* app bundles the CLI and installs the wrapper — following that instruction used to be actively
|
|
@@ -117,7 +117,7 @@ export function runSelfUpdate(kind = installKind()) {
|
|
|
117
117
|
return { ok: false, message: updateHint(kind) };
|
|
118
118
|
}
|
|
119
119
|
const feed = (process.env.BEEHAVEN_CLI_FEED || 'https://downloads.clearly.sh/cli/').replace(/\/+$/, '');
|
|
120
|
-
const installerUrl = process.env.BEEHAVEN_INSTALL_URL || 'https://clearly.sh/install.sh';
|
|
120
|
+
const installerUrl = process.env.BEEHAVEN_INSTALL_URL || 'https://www.clearly.sh/install.sh';
|
|
121
121
|
const temp = mkdtempSync(join(tmpdir(), 'beehaven-self-update-'));
|
|
122
122
|
try {
|
|
123
123
|
const manifestPath = join(temp, 'latest.json');
|
|
@@ -860,6 +860,22 @@ export async function cmdCall(action, rawInput, values) {
|
|
|
860
860
|
input = { ...input, action: canonical, name: canonical };
|
|
861
861
|
}
|
|
862
862
|
}
|
|
863
|
+
// ⚠ `type` IS THE WIRE'S ACTION NAME (PPO-190): a top-level `type` would be overwritten by the
|
|
864
|
+
// daemon's `{ ...input, type: action }` and lost with `ok: true`. Move it to the verb's own field
|
|
865
|
+
// or refuse with the right spelling — never drop it in silence. See reserved-type.ts.
|
|
866
|
+
{
|
|
867
|
+
const { reconcileReservedType } = await import('../reserved-type.js');
|
|
868
|
+
const r = reconcileReservedType(action, input);
|
|
869
|
+
if (r.kind === 'refused') {
|
|
870
|
+
console.error(` ${r.error}`);
|
|
871
|
+
process.exit(1);
|
|
872
|
+
}
|
|
873
|
+
if (r.kind === 'moved') {
|
|
874
|
+
input = r.input;
|
|
875
|
+
if (!values.json)
|
|
876
|
+
console.error(` · ${r.note}`);
|
|
877
|
+
}
|
|
878
|
+
}
|
|
863
879
|
// Confirm spend BEFORE resolving a building or touching the daemon — a cancelled call must
|
|
864
880
|
// cost nothing and behave identically whether the input came from argv, a file, or stdin.
|
|
865
881
|
// It runs after the parse because the gate is input-aware: `engine:'handroll'` and any call
|
|
@@ -3049,15 +3065,20 @@ export async function cmdBroadcast(rawMsg, values) {
|
|
|
3049
3065
|
* calling all along (`Avatar.ts` `replyWithOmni`), which is why Omni was
|
|
3050
3066
|
* reachable from the desktop companion and not from the terminal.
|
|
3051
3067
|
*
|
|
3052
|
-
* The payload matters as much as the name: `agentId: '
|
|
3053
|
-
*
|
|
3068
|
+
* The payload matters as much as the name: `agentId: 'builtin:clearly'` selects the
|
|
3069
|
+
* workspace's built-in assistant, and `wait: true` is what makes this a one-shot rather than a
|
|
3054
3070
|
* fire-and-forget that prints an ack and exits before the reply exists.
|
|
3071
|
+
*
|
|
3072
|
+
* ⚠ AWO-226: it sent `'omni'`, the built-in's old name. Since PPO-291 an untyped row reads as an
|
|
3073
|
+
* external identity, and workspaces old enough to carry the legacy `omni` row refused every
|
|
3074
|
+
* `beehaven agent "…"` with "Omni is an external agent" (staging home, 2026-10-06).
|
|
3075
|
+
* `builtin:clearly` is the id the server always resolves to the built-in, on old and new Workers.
|
|
3055
3076
|
*/
|
|
3056
3077
|
export async function cmdAgent(prompt, values) {
|
|
3057
3078
|
try {
|
|
3058
3079
|
const result = await rpcOnce('agent-prompt', {
|
|
3059
3080
|
message: prompt,
|
|
3060
|
-
agentId: '
|
|
3081
|
+
agentId: 'builtin:clearly',
|
|
3061
3082
|
wait: true,
|
|
3062
3083
|
// ⚠ NOT `cwd`. The old call passed the shell's working directory, which
|
|
3063
3084
|
// the action does not accept — a local path is meaningless to a cloud
|