@clearlysh/beehaven 0.8.23 → 0.8.25
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 +46 -267
- package/dist/commands/agent.js +186 -59
- 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 +8 -3
- package/dist/daemon.js +9 -9
- package/dist/folder-sync.js +1 -1
- package/dist/local-app-manager.js +1 -1
- 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 +9 -0
- package/skills/clearly-canvas/SKILL.md +48 -2
- package/skills/clearly-canvas/authoring.md +16 -5
- 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.
|
|
@@ -880,104 +894,182 @@ async function agentLogin(args, values) {
|
|
|
880
894
|
console.log('');
|
|
881
895
|
}
|
|
882
896
|
/**
|
|
883
|
-
*
|
|
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).
|
|
884
923
|
*
|
|
885
924
|
* Everything here answers a question a returning colleague asks before touching anything, and
|
|
886
925
|
* NOTHING here is printed when it is empty — a brief padded with "0 sessions · 0 memories ·
|
|
887
926
|
* nothing assigned" is a wall an agent learns to skip, which costs the one thing the digest
|
|
888
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.
|
|
889
931
|
*/
|
|
890
|
-
function
|
|
932
|
+
export function formatBrief(brief, now = Date.now()) {
|
|
933
|
+
const out = [];
|
|
891
934
|
if (!brief)
|
|
892
|
-
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 ?? {};
|
|
893
941
|
const sessions = brief.sessions?.recent ?? [];
|
|
894
942
|
const assigned = brief.assignedToMe ?? [];
|
|
895
943
|
const pending = brief.awaitingApproval ?? [];
|
|
896
944
|
const memories = brief.memories ?? [];
|
|
897
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;
|
|
898
952
|
/**
|
|
899
953
|
* ⚠ SS-41 — ADDRESSED TO YOU, AND PRINTED FIRST. Voicemail and @mentions are somebody speaking
|
|
900
954
|
* TO this agent; everything else in the brief is ambient. Both stay until acknowledged.
|
|
901
955
|
*/
|
|
902
|
-
const mentions =
|
|
903
|
-
const voicemail =
|
|
956
|
+
const mentions = ws.mentions ?? [];
|
|
957
|
+
const voicemail = ws.voicemail ?? [];
|
|
904
958
|
/** WIB-1 — people and agents writing TO this agent. Oldest first; reading one clears it. */
|
|
905
|
-
const inbox =
|
|
959
|
+
const inbox = ws.inboxThreads ?? [];
|
|
906
960
|
if (mentions.length || voicemail.length || inbox.length) {
|
|
907
|
-
|
|
908
|
-
console.log(` ${bold('Addressed to you')} ${dim(`(${mentions.length + voicemail.length + inbox.length})`)}`);
|
|
961
|
+
section('Addressed to you', mentions.length + voicemail.length + inbox.length);
|
|
909
962
|
for (const t of inbox.slice(0, 5)) {
|
|
910
|
-
|
|
911
|
-
|
|
963
|
+
out.push(` ${dim('·')} ${cyan('message')} from ${t.from}: “${t.subject}” ${dim(`${t.unread} unread · ${t.threadId}`)}`);
|
|
964
|
+
out.push(` ${oneLine(t.preview, 120)}`);
|
|
912
965
|
}
|
|
913
966
|
if (inbox.length)
|
|
914
|
-
|
|
967
|
+
out.push(dim(` Read with \`beehaven inbox read <id>\`, answer with \`beehaven inbox reply <id> "…"\`, then \`beehaven inbox done <id>\`.`));
|
|
915
968
|
for (const v of voicemail.slice(0, 3)) {
|
|
916
|
-
|
|
969
|
+
out.push(` ${dim('·')} ${cyan('voicemail')} from ${v.from}: ${oneLine(v.text, 100)}`);
|
|
917
970
|
}
|
|
918
971
|
for (const m of mentions.slice(0, 5)) {
|
|
919
972
|
const on = m.on ?? {};
|
|
920
|
-
|
|
921
|
-
|
|
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)}`);
|
|
922
975
|
}
|
|
923
976
|
if (mentions.length)
|
|
924
|
-
|
|
977
|
+
out.push(dim(` Act, reply with \`beehaven comment <ref> "…"\`, then \`beehaven mention ack <id>\`.`));
|
|
925
978
|
}
|
|
926
|
-
|
|
927
|
-
if (!mentions.length && !voicemail.length && !inbox.length)
|
|
928
|
-
console.log(dim(' First visit — nothing to catch up on.'));
|
|
929
|
-
return;
|
|
930
|
-
}
|
|
931
|
-
const ago = (ts) => {
|
|
932
|
-
const d = Date.now() - ts;
|
|
933
|
-
if (d < 60_000)
|
|
934
|
-
return 'just now';
|
|
935
|
-
if (d < 3_600_000)
|
|
936
|
-
return `${Math.round(d / 60_000)}m ago`;
|
|
937
|
-
if (d < 86_400_000)
|
|
938
|
-
return `${Math.round(d / 3_600_000)}h ago`;
|
|
939
|
-
return `${Math.round(d / 86_400_000)}d ago`;
|
|
940
|
-
};
|
|
979
|
+
// ── What you left yourself. The handoff note is the payload; counts are context. ──
|
|
941
980
|
if (sessions.length) {
|
|
942
|
-
|
|
943
|
-
console.log(` ${bold('Last time you were here')}`);
|
|
981
|
+
section('Last time you were here');
|
|
944
982
|
for (const s of sessions.slice(0, 3)) {
|
|
945
|
-
const when = s.startedAt ?
|
|
983
|
+
const when = s.startedAt ? briefAgo(s.startedAt, now) : '';
|
|
946
984
|
const counts = `${s.actions} action${s.actions === 1 ? '' : 's'}${s.errors ? `, ${s.errors} error${s.errors === 1 ? '' : 's'}` : ''}`;
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
if (
|
|
950
|
-
|
|
951
|
-
}
|
|
952
|
-
}
|
|
953
|
-
if (focus.length) {
|
|
954
|
-
console.log('');
|
|
955
|
-
console.log(` ${bold('You were working on')}`);
|
|
956
|
-
for (const f of focus.slice(0, 3)) {
|
|
957
|
-
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)}`);
|
|
958
989
|
}
|
|
959
990
|
}
|
|
960
991
|
if (assigned.length) {
|
|
961
|
-
|
|
962
|
-
console.log(` ${bold('Assigned to you')} ${dim(`(${assigned.length})`)}`);
|
|
992
|
+
section('Assigned to you', assigned.length);
|
|
963
993
|
for (const t of assigned.slice(0, 5)) {
|
|
964
|
-
|
|
994
|
+
out.push(` ${dim('·')} ${t.key ? cyan(t.key) + ' ' : ''}${t.title} ${dim(`[${t.status}]`)}`);
|
|
965
995
|
}
|
|
966
996
|
}
|
|
967
997
|
if (pending.length) {
|
|
968
|
-
|
|
969
|
-
console.log(` ${bold('Waiting on a human')} ${dim(`(${pending.length})`)}`);
|
|
998
|
+
section('Waiting on a human', pending.length);
|
|
970
999
|
for (const c of pending.slice(0, 4)) {
|
|
971
|
-
|
|
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)`)}`);
|
|
972
1016
|
}
|
|
973
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)}`) : ''}`);
|
|
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}`);
|
|
1053
|
+
}
|
|
974
1054
|
if (memories.length) {
|
|
975
|
-
|
|
976
|
-
console.log(` ${bold('You noted')}`);
|
|
1055
|
+
section('You noted');
|
|
977
1056
|
for (const m of memories.slice(0, 4)) {
|
|
978
|
-
|
|
1057
|
+
out.push(` ${dim('·')} ${cyan(m.key)}: ${oneLine(m.value, 90)}`);
|
|
979
1058
|
}
|
|
980
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);
|
|
981
1073
|
}
|
|
982
1074
|
// ── use (prints the export line for eval) ────────────
|
|
983
1075
|
async function agentUse(args) {
|
|
@@ -1092,6 +1184,41 @@ async function agentStatus(args, values) {
|
|
|
1092
1184
|
}
|
|
1093
1185
|
console.log(` ${green('✓')} ${cyan(name)} status: ${String(result.message ?? message)}`);
|
|
1094
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
|
+
}
|
|
1095
1222
|
// ── logout ───────────────────────────────────────────
|
|
1096
1223
|
async function agentLogout(args, values) {
|
|
1097
1224
|
const name = args[0] ?? activeAgentName(values);
|
|
@@ -1109,7 +1236,7 @@ async function agentLogout(args, values) {
|
|
|
1109
1236
|
// it stays in the workspace (source of truth) AND on this device, so you can
|
|
1110
1237
|
// log back in. To remove the current workspace projection use `agent delete <name>`;
|
|
1111
1238
|
// to remove the portable account identity use `agent delete <name> --account --yes`.
|
|
1112
|
-
await endWorkspaceSession(record
|
|
1239
|
+
await endWorkspaceSession(record);
|
|
1113
1240
|
try {
|
|
1114
1241
|
await rpcOnce('agent:disconnect', { agentId: record.agentId ?? name }, 5_000);
|
|
1115
1242
|
}
|
|
@@ -1134,7 +1261,7 @@ async function agentDelete(args, values) {
|
|
|
1134
1261
|
if (values.account === true)
|
|
1135
1262
|
return agentAccountDelete(args, values);
|
|
1136
1263
|
const record = loadAgent(name);
|
|
1137
|
-
await endWorkspaceSession(record
|
|
1264
|
+
await endWorkspaceSession(record);
|
|
1138
1265
|
// 1. Remove from the WORKSPACE (source of truth) — the `agent-delete` RPC
|
|
1139
1266
|
// soft-deletes (archived). Resolve by the record's agentId, else the name.
|
|
1140
1267
|
let cloud = false;
|
|
@@ -1193,7 +1320,7 @@ async function agentAccountDelete(args, values) {
|
|
|
1193
1320
|
console.error(` ${result.error || `Could not delete account agent "${name}".`}`);
|
|
1194
1321
|
process.exit(1);
|
|
1195
1322
|
}
|
|
1196
|
-
await endWorkspaceSession(record
|
|
1323
|
+
await endWorkspaceSession(record);
|
|
1197
1324
|
try {
|
|
1198
1325
|
await rpcOnce('agent:disconnect', { agentId }, 5_000);
|
|
1199
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');
|
|
@@ -3065,15 +3065,20 @@ export async function cmdBroadcast(rawMsg, values) {
|
|
|
3065
3065
|
* calling all along (`Avatar.ts` `replyWithOmni`), which is why Omni was
|
|
3066
3066
|
* reachable from the desktop companion and not from the terminal.
|
|
3067
3067
|
*
|
|
3068
|
-
* The payload matters as much as the name: `agentId: '
|
|
3069
|
-
*
|
|
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
|
|
3070
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.
|
|
3071
3076
|
*/
|
|
3072
3077
|
export async function cmdAgent(prompt, values) {
|
|
3073
3078
|
try {
|
|
3074
3079
|
const result = await rpcOnce('agent-prompt', {
|
|
3075
3080
|
message: prompt,
|
|
3076
|
-
agentId: '
|
|
3081
|
+
agentId: 'builtin:clearly',
|
|
3077
3082
|
wait: true,
|
|
3078
3083
|
// ⚠ NOT `cwd`. The old call passed the shell's working directory, which
|
|
3079
3084
|
// the action does not accept — a local path is meaningless to a cloud
|