beehaven 0.8.24 → 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.
@@ -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, 'Replaced by a new login session for this agent identity.');
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
- * The login digest.
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 printBrief(brief) {
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 = brief.workspace?.mentions ?? [];
903
- const voicemail = brief.workspace?.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 = brief.workspace?.inboxThreads ?? [];
959
+ const inbox = ws.inboxThreads ?? [];
906
960
  if (mentions.length || voicemail.length || inbox.length) {
907
- console.log('');
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
- console.log(` ${dim('·')} ${cyan('message')} from ${t.from}: “${t.subject}” ${dim(`${t.unread} unread · ${t.threadId}`)}`);
911
- console.log(` ${String(t.preview ?? '').replace(/\s+/g, ' ').slice(0, 120)}`);
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
- console.log(dim(` Read with \`beehaven inbox read <id>\`, answer with \`beehaven inbox reply <id> "…"\`, then \`beehaven inbox done <id>\`.`));
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
- console.log(` ${dim('·')} ${cyan('voicemail')} from ${v.from}: ${String(v.text ?? '').replace(/\s+/g, ' ').slice(0, 100)}`);
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
- console.log(` ${dim('·')} ${cyan('@mention')} from ${m.from} on ${on.kind || 'item'} “${on.title || on.id || ''}” ${dim(String(m.id).slice(0, 8))}`);
921
- console.log(` ${String(m.text ?? '').replace(/\s+/g, ' ').slice(0, 120)}`);
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
- console.log(dim(` Act, reply with \`beehaven comment <ref> "…"\`, then \`beehaven mention ack <id>\`.`));
977
+ out.push(dim(` Act, reply with \`beehaven comment <ref> "…"\`, then \`beehaven mention ack <id>\`.`));
925
978
  }
926
- if (!sessions.length && !assigned.length && !pending.length && !memories.length && !focus.length) {
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
- console.log('');
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 ? ago(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
- console.log(` ${dim('·')} ${s.label || '(unlabelled)'} ${dim(`— ${when}, ${counts}`)}`);
948
- // The summary is the payload. It is the one thing carrying reasoning rather than counts.
949
- if (s.summary)
950
- console.log(` ${s.summary}`);
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
- console.log('');
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
- console.log(` ${dim('·')} ${t.key ? cyan(t.key) + ' ' : ''}${t.title} ${dim(`[${t.status}]`)}`);
994
+ out.push(` ${dim('·')} ${t.key ? cyan(t.key) + ' ' : ''}${t.title} ${dim(`[${t.status}]`)}`);
965
995
  }
966
996
  }
967
997
  if (pending.length) {
968
- console.log('');
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
- console.log(` ${dim('·')} ${c.title || c.id} ${dim(c.kind ? `(${c.kind})` : '')}`);
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
- console.log('');
976
- console.log(` ${bold('You noted')}`);
1055
+ section('You noted');
977
1056
  for (const m of memories.slice(0, 4)) {
978
- console.log(` ${dim('·')} ${cyan(m.key)}: ${m.value.slice(0, 90)}${m.value.length > 90 ? '…' : ''}`);
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, 'Agent explicitly logged out.');
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, 'Agent identity deleted.');
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, 'Account agent identity deleted.');
1323
+ await endWorkspaceSession(record);
1197
1324
  try {
1198
1325
  await rpcOnce('agent:disconnect', { agentId }, 5_000);
1199
1326
  }
@@ -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) {
@@ -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)
@@ -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: 'omni'` selects the
3069
- * orchestrator, and `wait: true` is what makes this a one-shot rather than a
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: 'omni',
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
package/dist/daemon.js CHANGED
@@ -171,14 +171,14 @@ async function simulateCloudAppRpc(params) {
171
171
  * Curated "available to install" app list served by /api/marketplace.
172
172
  *
173
173
  * This is a stub catalog. Once the cloud marketplace is live, the daemon
174
- * will fetch from https://clearly.sh/apps/catalog.json instead of this
174
+ * will fetch from https://www.clearly.sh/apps/catalog.json instead of this
175
175
  * hardcoded array. The installed-app set is subtracted at serve time so
176
176
  * users don't see things they already have.
177
177
  *
178
178
  * Shapes roughly mirror AgentHiveCatalogEntry minus the live "registered"
179
179
  * state — a catalog entry is a promise of what installing would give you.
180
180
  */
181
- const CATALOG_URL = 'https://clearly.sh/apps/catalog.json';
181
+ const CATALOG_URL = 'https://www.clearly.sh/apps/catalog.json';
182
182
  const CATALOG_CACHE_MS = 5 * 60_000; // 5 min
183
183
  let _catalogCache = null;
184
184
  let _catalogFetchedAt = 0;
@@ -234,7 +234,7 @@ const STATIC_FALLBACK_APPS = [
234
234
  tier: 'pro',
235
235
  tags: ['commerce', 'cloud', 'api'],
236
236
  kind: 'cloud',
237
- install: { source: 'url', url: 'https://clearly.sh/apps/shopify/shopify-0.1.0.hive' },
237
+ install: { source: 'url', url: 'https://www.clearly.sh/apps/shopify/shopify-0.1.0.hive' },
238
238
  frontend: { title: 'Shopify', icon: '🛍️' },
239
239
  },
240
240
  {
@@ -245,7 +245,7 @@ const STATIC_FALLBACK_APPS = [
245
245
  tier: 'free',
246
246
  tags: ['creative', '3d', 'local'],
247
247
  kind: 'local',
248
- install: { source: 'url', url: 'https://clearly.sh/apps/blender/blender-0.1.0.hive' },
248
+ install: { source: 'url', url: 'https://www.clearly.sh/apps/blender/blender-0.1.0.hive' },
249
249
  frontend: { title: 'Blender', icon: '🧊' },
250
250
  },
251
251
  {
@@ -256,7 +256,7 @@ const STATIC_FALLBACK_APPS = [
256
256
  tier: 'pro',
257
257
  tags: ['design', 'cloud', 'api'],
258
258
  kind: 'cloud',
259
- install: { source: 'url', url: 'https://clearly.sh/apps/figma/figma-0.1.0.hive' },
259
+ install: { source: 'url', url: 'https://www.clearly.sh/apps/figma/figma-0.1.0.hive' },
260
260
  frontend: { title: 'Figma', icon: '🎨' },
261
261
  },
262
262
  {
@@ -267,7 +267,7 @@ const STATIC_FALLBACK_APPS = [
267
267
  tier: 'pro',
268
268
  tags: ['creative', 'audio', 'local'],
269
269
  kind: 'local',
270
- install: { source: 'url', url: 'https://clearly.sh/apps/logic-pro/logic-pro-0.1.0.hive' },
270
+ install: { source: 'url', url: 'https://www.clearly.sh/apps/logic-pro/logic-pro-0.1.0.hive' },
271
271
  frontend: { title: 'Logic Pro', icon: '🎚️' },
272
272
  },
273
273
  {
@@ -278,7 +278,7 @@ const STATIC_FALLBACK_APPS = [
278
278
  tier: 'free',
279
279
  tags: ['smart-home', 'local'],
280
280
  kind: 'local',
281
- install: { source: 'url', url: 'https://clearly.sh/apps/hue/hue-0.1.0.hive' },
281
+ install: { source: 'url', url: 'https://www.clearly.sh/apps/hue/hue-0.1.0.hive' },
282
282
  frontend: { title: 'Philips Hue', icon: '💡' },
283
283
  },
284
284
  {
@@ -289,7 +289,7 @@ const STATIC_FALLBACK_APPS = [
289
289
  tier: 'pro',
290
290
  tags: ['productivity', 'cloud', 'api'],
291
291
  kind: 'cloud',
292
- install: { source: 'url', url: 'https://clearly.sh/apps/jira/jira-0.1.0.hive' },
292
+ install: { source: 'url', url: 'https://www.clearly.sh/apps/jira/jira-0.1.0.hive' },
293
293
  frontend: { title: 'Jira', icon: '📋' },
294
294
  },
295
295
  {
@@ -707,7 +707,7 @@ let daemonAppManager = null;
707
707
  // curl-only distribution — the CLI is NOT published to npm. Updates fetch the
708
708
  // versioned tarball from downloads.clearly.sh via the public installer script.
709
709
  let announcedManagedUpdates = false;
710
- const INSTALL_URL = process.env.BEEHAVEN_INSTALL_URL || 'https://clearly.sh/install.sh';
710
+ const INSTALL_URL = process.env.BEEHAVEN_INSTALL_URL || 'https://www.clearly.sh/install.sh';
711
711
  /**
712
712
  * ⚠ THE SAME OVERRIDE ITS SIBLING ALREADY HAD. `min-version.ts` reads `BEEHAVEN_CLI_FEED` so the
713
713
  * force-update gate can be exercised against a scratch channel; the auto-updater — the half that
@@ -77,7 +77,7 @@ export async function linkProject(relay, folderPath, brandId) {
77
77
  }
78
78
  if (projects.length === 0) {
79
79
  console.error(' No projects found in your Clearly account.');
80
- console.error(' Create a project at https://clearly.sh first.');
80
+ console.error(' Create a project at https://www.clearly.sh first.');
81
81
  process.exit(1);
82
82
  }
83
83
  console.log('\n Available projects:\n');
@@ -97,7 +97,7 @@ export class LocalAppManager extends EventEmitter {
97
97
  */
98
98
  templateVars() {
99
99
  // `webBase` — dev Mac app sets this via env; defaults to production.
100
- const webBase = process.env.BEEHAVEN_WEB_BASE ?? process.env.CLEARLY_WEB_BASE ?? 'https://clearly.sh';
100
+ const webBase = process.env.BEEHAVEN_WEB_BASE ?? process.env.CLEARLY_WEB_BASE ?? 'https://www.clearly.sh';
101
101
  // User identity — the daemon stores these in config on sign-in
102
102
  // install(), so they're available without decoding the JWT here.
103
103
  const cfg = readDaemonConfigSafe();