@clearlysh/beehaven 0.8.36 → 0.8.38
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-mint-response.js +10 -0
- package/dist/build-channel.js +2 -2
- package/dist/call-brief.js +157 -0
- package/dist/claude-bridge.js +4 -1
- package/dist/clearly.js +0 -0
- package/dist/cli.js +18 -4
- package/dist/commands/agent.js +5 -3
- package/dist/commands/daemon-cmd.js +17 -1
- package/dist/commands/help.js +3 -1
- package/dist/commands/workspace.js +31 -2
- package/dist/connection-ready.js +25 -0
- package/dist/daemon-client.js +7 -2
- package/dist/daemon-process.js +26 -11
- package/dist/daemon.js +4 -3
- package/dist/ipc-client.js +4 -1
- package/dist/ipc-server.js +4 -1
- package/dist/relay.js +19 -6
- package/dist/utf8-stream.js +23 -0
- package/package.json +1 -1
- package/skills/clearly-canvas/SKILL.md +16 -8
- package/skills/clearly-canvas/authoring.md +40 -17
- package/skills/clearly-canvas/review.mjs +25 -4
- package/skills/index.json +1 -1
- package/dist/daemon-codex.js +0 -349
- package/dist/daemon-write-sandbox.js +0 -191
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/** Only an explicit appearance choice may rebind a portable identity on another device. */
|
|
2
|
+
export function accountMintRequest(agentId, requestedAvatarId) {
|
|
3
|
+
return { agentId, ...(requestedAvatarId ? { avatarId: requestedAvatarId } : {}) };
|
|
4
|
+
}
|
|
5
|
+
export function accountMintResponse(response) {
|
|
6
|
+
const value = response?.result ?? response;
|
|
7
|
+
if (!value?.token)
|
|
8
|
+
throw Object.assign(new Error(`${String(value?.error || 'The server returned no agent token')}${value?.code ? ` (${value.code})` : ''}`), { code: value?.code || 'mint-refused' });
|
|
9
|
+
return value;
|
|
10
|
+
}
|
package/dist/build-channel.js
CHANGED
|
@@ -53,9 +53,9 @@ export const PUBLIC_BUILD = true;
|
|
|
53
53
|
* ⚠ REWRITTEN IN `dist/` AT BUILD TIME, like `PUBLIC_BUILD`. `unstamped` is what a source checkout
|
|
54
54
|
* carries and what `tsx`/`vitest` see, and it is honest: nothing has stamped them.
|
|
55
55
|
*/
|
|
56
|
-
export const BUILD_SHA = '
|
|
56
|
+
export const BUILD_SHA = '6f5ce18b17c5';
|
|
57
57
|
/** ISO-8601 minute precision, UTC. Rewritten in `dist/` by `scripts/stamp-build-channel.mjs`. */
|
|
58
|
-
export const BUILD_TIME = '2026-10-
|
|
58
|
+
export const BUILD_TIME = '2026-10-30T07:25Z';
|
|
59
59
|
/**
|
|
60
60
|
* The build identity as one short string, for `--version`.
|
|
61
61
|
*
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `beehaven call <action> … --brief` — a receipt a person (or an agent's context window) can read.
|
|
3
|
+
*
|
|
4
|
+
* ⚠⚠ WHY THIS EXISTS. A `canvas-act` batch answers with every step's full value plus the lint of
|
|
5
|
+
* every touched artboard — routinely tens of kilobytes. Agents learned to pipe it into a summariser
|
|
6
|
+
* they wrote on the spot (`… 2>&1 | node summ.mjs`), and in the first recorded canvas take (MCP2-20)
|
|
7
|
+
* that improvisation cost four failed tool calls: shell pipes and `/tmp` redirects need approval in
|
|
8
|
+
* a headless agent, and the summariser crashed on the stderr breadcrumb it was fed. The receipt the
|
|
9
|
+
* agent wanted is printed here instead, from the same result, with the exit code unchanged.
|
|
10
|
+
*
|
|
11
|
+
* ⚠ IT DROPS NOTHING THAT DECIDES THE NEXT STEP: failed steps (with their error), rollbacks and
|
|
12
|
+
* skips, lint findings that need fixing, a review's verdict and blocking count. What it leaves out
|
|
13
|
+
* is bulk — per-step echoes, previews, font metadata. `--json` still prints the whole payload.
|
|
14
|
+
*/
|
|
15
|
+
const clip = (s, n) => {
|
|
16
|
+
const t = String(s ?? '').replace(/\s+/g, ' ').trim();
|
|
17
|
+
return t.length > n ? `${t.slice(0, n - 1)}…` : t;
|
|
18
|
+
};
|
|
19
|
+
const plural = (n, one, many = `${one}s`) => `${n} ${n === 1 ? one : many}`;
|
|
20
|
+
/** `canvas.create-node` → `create-node`; `frame.create` stays as written. */
|
|
21
|
+
const verb = (a) => (a.startsWith('canvas.') ? a.slice(7) : a);
|
|
22
|
+
export function briefReceipt(action, result) {
|
|
23
|
+
const r = (result && typeof result === 'object' ? result : { value: result });
|
|
24
|
+
const error = typeof r.error === 'string' && r.error ? r.error : null;
|
|
25
|
+
/**
|
|
26
|
+
* MCP2-28 (codex review) — A FAILED BATCH IS STILL A BATCH. This returned the one-line refusal for
|
|
27
|
+
* any `ok:false`, before looking at `results`, so a rolled-back canvas-act (step 2 of 2 refused)
|
|
28
|
+
* printed a truncated error with no step number and no "rolled back". The batch receipt is read
|
|
29
|
+
* first; the top-level failure is kept on it, so the exit code still says it failed.
|
|
30
|
+
*/
|
|
31
|
+
if (Array.isArray(r.results) && r.results.length) {
|
|
32
|
+
const b = batchReceipt(action, r);
|
|
33
|
+
if (error || r.ok === false) {
|
|
34
|
+
b.ok = false;
|
|
35
|
+
b.lines[0] = b.lines[0].replace(/^✓/, '✗');
|
|
36
|
+
const why = clip(error ?? r.message ?? r.code ?? '', 200);
|
|
37
|
+
// Not repeated when a failed step already says it (the step line clips at 140, this at 200).
|
|
38
|
+
if (why && !b.lines.some((l) => l.includes(why.slice(0, 60))))
|
|
39
|
+
b.lines.splice(1, 0, ` ${why}`);
|
|
40
|
+
}
|
|
41
|
+
return b;
|
|
42
|
+
}
|
|
43
|
+
if (error || r.ok === false) {
|
|
44
|
+
return { ok: false, lines: [`✗ ${action} · ${clip(error ?? r.message ?? r.code ?? 'refused', 160)}`] };
|
|
45
|
+
}
|
|
46
|
+
if (Array.isArray(r.results))
|
|
47
|
+
return batchReceipt(action, r);
|
|
48
|
+
return { ok: true, lines: [`✓ ${action}${describeValue(r)}`] };
|
|
49
|
+
}
|
|
50
|
+
function batchReceipt(action, r) {
|
|
51
|
+
const steps = r.results;
|
|
52
|
+
const failed = steps.map((s, i) => ({ s, i })).filter(({ s }) => s?.ok === false);
|
|
53
|
+
const lines = [];
|
|
54
|
+
// What ran: a review or a set of measurements reads better as its result than as a step count.
|
|
55
|
+
const reviews = steps.filter((s) => /canvas\.review/.test(s?.action) && s?.ok !== false);
|
|
56
|
+
const measures = steps.filter((s) => s?.action === 'canvas.measure-text' && s?.ok !== false);
|
|
57
|
+
const counts = new Map();
|
|
58
|
+
for (const s of steps)
|
|
59
|
+
counts.set(verb(s?.action ?? '?'), (counts.get(verb(s?.action ?? '?')) ?? 0) + 1);
|
|
60
|
+
const mix = [...counts].sort((a, b) => b[1] - a[1]).slice(0, 3).map(([a, n]) => (n > 1 ? `${a} ×${n}` : a)).join(', ');
|
|
61
|
+
let added = 0, changed = 0, removed = 0, sawDelta = false;
|
|
62
|
+
for (const s of steps) {
|
|
63
|
+
const d = s?.value?.canvasChanges;
|
|
64
|
+
if (d && typeof d === 'object' && !Array.isArray(d)) {
|
|
65
|
+
sawDelta = true;
|
|
66
|
+
added += Number(d.added || 0);
|
|
67
|
+
changed += Number(d.changed || 0);
|
|
68
|
+
removed += Number(d.removed || 0);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
const rev = r.canvasNodesRev ?? [...steps].reverse().find((s) => s?.value?.canvasNodesRev != null)?.value?.canvasNodesRev;
|
|
72
|
+
const head = [`${failed.length ? '✗' : '✓'} ${action}`, `${plural(steps.length, 'step')}${mix ? ` (${mix})` : ''}`];
|
|
73
|
+
if (sawDelta && added + changed + removed)
|
|
74
|
+
head.push(`+${added} ~${changed} −${removed} layers`);
|
|
75
|
+
if (rev != null)
|
|
76
|
+
head.push(`rev ${rev}`);
|
|
77
|
+
if (r.rolledBack)
|
|
78
|
+
head.push('rolled back');
|
|
79
|
+
if (r.partial)
|
|
80
|
+
head.push('partly applied');
|
|
81
|
+
if (Array.isArray(r.skipped) ? r.skipped.length : r.skipped)
|
|
82
|
+
head.push(`${Array.isArray(r.skipped) ? r.skipped.length : r.skipped} skipped`);
|
|
83
|
+
lines.push(head.join(' · '));
|
|
84
|
+
for (const { s, i } of failed.slice(0, 3)) {
|
|
85
|
+
lines.push(` ✗ step ${i + 1} ${verb(s.action ?? '?')}: ${clip(s.error ?? s.value?.error ?? 'failed', 140)}`);
|
|
86
|
+
}
|
|
87
|
+
if (failed.length > 3)
|
|
88
|
+
lines.push(` … ${failed.length - 3} more failed steps`);
|
|
89
|
+
for (const m of measures.slice(0, 6)) {
|
|
90
|
+
const v = m.value ?? {};
|
|
91
|
+
lines.push(` ${Math.round(v.width ?? 0)}×${Math.round(v.height ?? 0)} · ${plural(v.lineCount ?? 1, 'line')}${v.exact === false ? ' · estimated' : ''} — ${v.family ?? ''} ${v.size ?? ''}/${v.fontWeight ?? ''}`.trimEnd());
|
|
92
|
+
}
|
|
93
|
+
for (const rv of reviews) {
|
|
94
|
+
const v = rv.value ?? {};
|
|
95
|
+
const reports = Array.isArray(v.reviews) ? v.reviews : [v];
|
|
96
|
+
for (const rep of reports) {
|
|
97
|
+
const blocking = rep.blocking?.craft ?? 0;
|
|
98
|
+
const where = rep.scope?.name ?? rep.scope?.ids?.[0] ?? '';
|
|
99
|
+
lines.push(` review${where ? ` ${clip(where, 40)}` : ''}: ${rep.verdict ?? 'unknown'}${blocking ? ` · ${plural(blocking, 'blocking finding')}` : ''}`);
|
|
100
|
+
for (const f of (rep.craft?.findings ?? []).filter((x) => x.severity === 'warn').slice(0, 3)) {
|
|
101
|
+
lines.push(` ${f.check}: ${clip(f.detail, 120)}`);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
// Lint of the touched artboards: what must be fixed next, never the whole report.
|
|
106
|
+
for (const d of (Array.isArray(r.diagnostics) ? r.diagnostics : [])) {
|
|
107
|
+
const errors = d.errors ?? [];
|
|
108
|
+
if (!errors.length && d.verdict !== 'fix')
|
|
109
|
+
continue;
|
|
110
|
+
lines.push(` lint ${clip(d.artboard ?? d.id, 40)}: ${d.verdict ?? 'fix'}`);
|
|
111
|
+
for (const e of errors.slice(0, 3))
|
|
112
|
+
lines.push(` ${clip(e, 140)}`);
|
|
113
|
+
if (errors.length > 3 || d.moreErrors)
|
|
114
|
+
lines.push(` … ${(errors.length - 3 > 0 ? errors.length - 3 : 0) + (d.moreErrors ?? 0)} more`);
|
|
115
|
+
}
|
|
116
|
+
const clean = (Array.isArray(r.diagnostics) ? r.diagnostics : []).filter((d) => d.verdict && d.verdict !== 'fix');
|
|
117
|
+
if (clean.length)
|
|
118
|
+
lines.push(` lint ${[...new Set(clean.map((d) => d.verdict))].join('/')} on ${plural(clean.length, 'artboard')}`);
|
|
119
|
+
/**
|
|
120
|
+
* MCP2-25 — THE CARRIED TICKET'S OUTCOME DECIDES THE NEXT STEP TOO. A batch carrying `ticket` moves
|
|
121
|
+
* it only when its review passes; the receipt said nothing either way, so an agent could not tell a
|
|
122
|
+
* done commit from one the review held back.
|
|
123
|
+
*/
|
|
124
|
+
const t = r.ticket;
|
|
125
|
+
if (t && typeof t === 'object') {
|
|
126
|
+
if (t.applied)
|
|
127
|
+
lines.push(` ticket ${t.key ?? t.id} → ${t.status ?? 'updated'}${t.frame?.url ? ` · frame ${t.frame.url}` : ''}`);
|
|
128
|
+
else
|
|
129
|
+
lines.push(` ✗ ticket ${t.id ?? ''} not moved: ${clip(t.reason ?? t.error ?? 'refused', 200)}`);
|
|
130
|
+
}
|
|
131
|
+
return { ok: failed.length === 0, lines };
|
|
132
|
+
}
|
|
133
|
+
/** A one-line description of an ordinary result: its identity and what it holds. */
|
|
134
|
+
function describeValue(r) {
|
|
135
|
+
const bits = [];
|
|
136
|
+
const id = r.key ?? r.id ?? r.compositionId ?? r.documentId;
|
|
137
|
+
if (id)
|
|
138
|
+
bits.push(String(id).length > 24 ? String(id).slice(0, 8) : String(id));
|
|
139
|
+
const title = r.title ?? r.name;
|
|
140
|
+
if (typeof title === 'string' && title)
|
|
141
|
+
bits.push(`"${clip(title, 60)}"`);
|
|
142
|
+
if (typeof r.status === 'string')
|
|
143
|
+
bits.push(r.status);
|
|
144
|
+
if (r.verdict)
|
|
145
|
+
bits.push(`verdict ${r.verdict}`);
|
|
146
|
+
for (const [k, v] of Object.entries(r)) {
|
|
147
|
+
if (Array.isArray(v) && v.length && bits.length < 4 && !['results', 'diagnostics'].includes(k))
|
|
148
|
+
bits.push(`${v.length} ${k}`);
|
|
149
|
+
}
|
|
150
|
+
if (typeof r.count === 'number' && !bits.some((b) => b.endsWith(' items')))
|
|
151
|
+
bits.push(`count ${r.count}`);
|
|
152
|
+
if (typeof r.url === 'string')
|
|
153
|
+
bits.push(r.url);
|
|
154
|
+
if (r.value !== undefined && typeof r.value !== 'object')
|
|
155
|
+
bits.push(clip(r.value, 120));
|
|
156
|
+
return bits.length ? ` · ${bits.join(' · ')}` : '';
|
|
157
|
+
}
|
package/dist/claude-bridge.js
CHANGED
|
@@ -22,6 +22,7 @@ import { homedir } from 'os';
|
|
|
22
22
|
import { EventEmitter } from 'events';
|
|
23
23
|
import { activeAgentWrap } from './agent-attribution.js';
|
|
24
24
|
import { ipcSocketPath, watcherSocketPath, socketMayExist } from './ipc-path.js';
|
|
25
|
+
import { utf8Stream } from './utf8-stream.js';
|
|
25
26
|
const CONFIG_DIR = join(homedir(), '.beehaven');
|
|
26
27
|
export class ClaudeBridge extends EventEmitter {
|
|
27
28
|
socket = null;
|
|
@@ -56,8 +57,10 @@ export class ClaudeBridge extends EventEmitter {
|
|
|
56
57
|
this.send({ type: 'session:announce', label: 'claude-code', app: 'bridge' });
|
|
57
58
|
resolve(true);
|
|
58
59
|
});
|
|
60
|
+
// CA-63 — decode the stream, not each chunk: a character split across two reads stays whole.
|
|
61
|
+
const decode = utf8Stream();
|
|
59
62
|
socket.on('data', (data) => {
|
|
60
|
-
this.buffer += data
|
|
63
|
+
this.buffer += decode(data);
|
|
61
64
|
const lines = this.buffer.split('\n');
|
|
62
65
|
this.buffer = lines.pop() || '';
|
|
63
66
|
for (const line of lines) {
|
package/dist/clearly.js
CHANGED
|
File without changes
|
package/dist/cli.js
CHANGED
|
@@ -408,7 +408,7 @@ ${DEV_ONLY_HELP ? `
|
|
|
408
408
|
beehaven actions --all …including ones this caller cannot invoke
|
|
409
409
|
beehaven actions --search <q> Find one by substring
|
|
410
410
|
beehaven explain <action> Its exact arguments, from the live schema
|
|
411
|
-
beehaven call <action> [json] Invoke it; returns the raw JSON result
|
|
411
|
+
beehaven call <action> [json] Invoke it; returns the raw JSON result (--brief: a short receipt; --out <file>: the JSON to a file)
|
|
412
412
|
|
|
413
413
|
# discover, then run
|
|
414
414
|
beehaven actions --search ticket
|
|
@@ -1443,6 +1443,8 @@ async function run() {
|
|
|
1443
1443
|
address: { type: 'string' },
|
|
1444
1444
|
protocol: { type: 'string' },
|
|
1445
1445
|
json: { type: 'boolean', default: false },
|
|
1446
|
+
// `beehaven call … --brief`: a short receipt instead of the full JSON (call-brief.ts).
|
|
1447
|
+
brief: { type: 'boolean', default: false },
|
|
1446
1448
|
// `--conversation <id>` threads `beehaven agent "<msg>"` onto an existing
|
|
1447
1449
|
// Omni thread instead of minting a new one per invocation.
|
|
1448
1450
|
conversation: { type: 'string' },
|
|
@@ -2090,6 +2092,8 @@ async function run() {
|
|
|
2090
2092
|
'slides', 'deck',
|
|
2091
2093
|
// `brand tokens -o tokens.css` · `component render -o button.svg`
|
|
2092
2094
|
'brand', 'brands', 'component', 'components',
|
|
2095
|
+
// MCP2-25 — `call <action> … --out result.json`: the whole result to a file, the receipt to the console.
|
|
2096
|
+
'call',
|
|
2093
2097
|
]);
|
|
2094
2098
|
/**
|
|
2095
2099
|
* ⚠ ONE REFUSAL, because there is now one flag. Both spellings are named in the message so a
|
|
@@ -3060,13 +3064,13 @@ async function run() {
|
|
|
3060
3064
|
}
|
|
3061
3065
|
console.log('');
|
|
3062
3066
|
console.log(` ${color.green('✓')} Signed in as ${color.bold(captured.email ?? captured.userId)}`);
|
|
3063
|
-
console.log(`
|
|
3067
|
+
console.log(` Sign-in saved on this device`);
|
|
3064
3068
|
// Auto-restart the daemon. New tokens won't take effect until
|
|
3065
3069
|
// the daemon restarts (it reads creds at startup, keeps them
|
|
3066
3070
|
// in memory, has no hot-reload IPC verb). Doing the restart
|
|
3067
3071
|
// here matches gh / firebase-tools UX — login-then-go.
|
|
3068
3072
|
const { findLiveDaemons, stopDaemonProcess } = await import('./daemon.js');
|
|
3069
|
-
const { spawnStandaloneDaemon } = await import('./commands/daemon-cmd.js');
|
|
3073
|
+
const { spawnStandaloneDaemon, waitForLoginConnection } = await import('./commands/daemon-cmd.js');
|
|
3070
3074
|
/**
|
|
3071
3075
|
* ⚠⚠ THE RESTART IS PLUMBING AND WAS NARRATED AS THOUGH IT WERE THE POINT. What a person saw
|
|
3072
3076
|
* after signing in was:
|
|
@@ -3111,6 +3115,7 @@ async function run() {
|
|
|
3111
3115
|
* started with developer scope.
|
|
3112
3116
|
*/
|
|
3113
3117
|
const pid = spawnStandaloneDaemon(values);
|
|
3118
|
+
const connection = await waitForLoginConnection();
|
|
3114
3119
|
/**
|
|
3115
3120
|
* ⚠ ONE OUTCOME LINE, IN THE SAME SHAPE AS THE ONE ABOVE IT. `✓ (PID 73532)` was a tick, a
|
|
3116
3121
|
* bracket and a number — grammatically nothing, and unreadable as a result. This says what
|
|
@@ -3121,7 +3126,16 @@ async function run() {
|
|
|
3121
3126
|
* `beehaven status` labels the same thing `Relay:`. Naming the outcome rather than the
|
|
3122
3127
|
* process is what stops a sign-in ending on the word "stopped".
|
|
3123
3128
|
*/
|
|
3124
|
-
|
|
3129
|
+
if (connection.ready) {
|
|
3130
|
+
console.log(` ${color.green('✓')} Connected — your new sign-in is live`);
|
|
3131
|
+
}
|
|
3132
|
+
else {
|
|
3133
|
+
console.error(` ${color.yellow('!')} Signed in, but the workspace connection is not ready.`);
|
|
3134
|
+
console.error(connection.reason === 'environment-mismatch'
|
|
3135
|
+
? ' The running service is on a different environment. Run `beehaven status` before retrying.'
|
|
3136
|
+
: ' Run `beehaven status` to check the connection, then retry your workspace command.');
|
|
3137
|
+
process.exitCode = 2;
|
|
3138
|
+
}
|
|
3125
3139
|
console.log(` Background service ${wasRunning ? 'restarted' : 'started'} (PID ${pid})`);
|
|
3126
3140
|
/**
|
|
3127
3141
|
* ⚠⚠ TEACH THE AGENTS AS PART OF SIGNING IN (PPO-190). "Install the CLI, log in, and your
|
package/dist/commands/agent.js
CHANGED
|
@@ -25,6 +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 { accountMintRequest, accountMintResponse } from '../agent-mint-response.js';
|
|
28
29
|
import { saveAgent, loadAgent, listAgents, deleteAgent, findAgentByAvatar, isValidAgentName, resolveActiveAgent, activeAgentName, saveAgentSession, clearAgentSession, clearAgentSessionId, decodeAgentTokenClaims, } from '../agent-state.js';
|
|
29
30
|
const VALID_KINDS = ['claude-code', 'daemon-agent', 'clearly-ai'];
|
|
30
31
|
export const FREE_SKINS = [
|
|
@@ -643,7 +644,7 @@ async function agentLogin(args, values) {
|
|
|
643
644
|
}
|
|
644
645
|
const avatarId = requestedAvatarId || existingRecord?.avatarId || chosenSkin || name;
|
|
645
646
|
// Local-first avatar-collision guard (durable guard is the cloud pool).
|
|
646
|
-
const collision = findAgentByAvatar(
|
|
647
|
+
const collision = requestedAvatarId ? findAgentByAvatar(requestedAvatarId, name) : null;
|
|
647
648
|
if (collision) {
|
|
648
649
|
console.error(` Avatar "${avatarId}" is already claimed by agent "${collision}".`);
|
|
649
650
|
console.error(` Pick another: beehaven agent login ${name} --avatar <other-id>`);
|
|
@@ -670,7 +671,7 @@ async function agentLogin(args, values) {
|
|
|
670
671
|
* credential.
|
|
671
672
|
*/
|
|
672
673
|
try {
|
|
673
|
-
const cloud = await rpcOnce('agent-account-mint',
|
|
674
|
+
const cloud = accountMintResponse(await rpcOnce('agent-account-mint', accountMintRequest(name, requestedAvatarId), 15_000));
|
|
674
675
|
const tok = cloud?.token ?? cloud?.result?.token;
|
|
675
676
|
if (tok) {
|
|
676
677
|
minted = {
|
|
@@ -698,7 +699,8 @@ async function agentLogin(args, values) {
|
|
|
698
699
|
}
|
|
699
700
|
catch (err) {
|
|
700
701
|
console.error(` Could not mint identity: ${err?.message ?? err}`);
|
|
701
|
-
|
|
702
|
+
if (!err?.code || /^(ECONN|ENOENT|ETIMEDOUT)/.test(String(err.code)))
|
|
703
|
+
console.error(' Is the daemon connected? Check with: beehaven status');
|
|
702
704
|
process.exit(1);
|
|
703
705
|
}
|
|
704
706
|
/**
|
|
@@ -16,6 +16,22 @@ import { fileURLToPath } from 'node:url';
|
|
|
16
16
|
import { existsSync } from 'node:fs';
|
|
17
17
|
import { dirname, join } from 'node:path';
|
|
18
18
|
import { findLiveDaemons, resolveRelayUrlForDaemon } from '../daemon.js';
|
|
19
|
+
import { waitForConnection } from '../connection-ready.js';
|
|
20
|
+
/** Each attempt authenticates over IPC; a PID or greeting alone cannot prove a usable sign-in. */
|
|
21
|
+
export async function waitForLoginConnection() {
|
|
22
|
+
const { IpcClient } = await import('../ipc-client.js');
|
|
23
|
+
return waitForConnection(async () => {
|
|
24
|
+
const client = new IpcClient({ timeout: 1000 });
|
|
25
|
+
try {
|
|
26
|
+
await client.connect();
|
|
27
|
+
const state = await client.call('daemon:session-status', {}, 1000);
|
|
28
|
+
return { connected: state?.relay?.connected === true, endpoint: state?.relay?.endpoint };
|
|
29
|
+
}
|
|
30
|
+
finally {
|
|
31
|
+
client.disconnect();
|
|
32
|
+
}
|
|
33
|
+
}, resolveRelayUrlForDaemon());
|
|
34
|
+
}
|
|
19
35
|
/** dist/daemon-entry.js — sibling of dist/cli.js (this file is dist/commands/). */
|
|
20
36
|
function daemonEntryPath() {
|
|
21
37
|
return join(dirname(fileURLToPath(import.meta.url)), '..', 'daemon-entry.js');
|
|
@@ -82,7 +98,7 @@ export async function startHeadlessDaemon(values) {
|
|
|
82
98
|
const { daemonRpc } = await import('../daemon-client.js');
|
|
83
99
|
let activeRelay = null;
|
|
84
100
|
try {
|
|
85
|
-
const status = await daemonRpc('daemon:status', {});
|
|
101
|
+
const status = await daemonRpc('daemon:session-status', {});
|
|
86
102
|
activeRelay = status?.relay?.endpoint ?? null;
|
|
87
103
|
}
|
|
88
104
|
catch { /* status is advisory; the socket probe already succeeded */ }
|
package/dist/commands/help.js
CHANGED
|
@@ -715,7 +715,7 @@ const PAGES = {
|
|
|
715
715
|
related: ['call', 'pwd', 'workspace'],
|
|
716
716
|
},
|
|
717
717
|
call: {
|
|
718
|
-
synopsis: 'beehaven call <action> [json-input] [--building <type>:<id>] [--json]',
|
|
718
|
+
synopsis: 'beehaven call <action> [json-input | @file | -] [--building <type>:<id>] [--json | --brief]',
|
|
719
719
|
description: `Invoke an RPC on the current target. Input is a JSON string.
|
|
720
720
|
Schema comes from the action's manifest — use \`beehaven actions\` first
|
|
721
721
|
to see what's available, or \`beehaven explain <action>\` for the exact
|
|
@@ -725,10 +725,12 @@ const PAGES = {
|
|
|
725
725
|
flags: [
|
|
726
726
|
{ name: '--building <type>:<id>', desc: 'Explicit target (e.g. shopifyStore:clearly-demo)' },
|
|
727
727
|
{ name: '--json', desc: 'Suppress breadcrumb + hint blocks; raw stdout only' },
|
|
728
|
+
{ name: '--brief', desc: 'A short receipt instead of the JSON: steps, layer changes, failures, lint to fix, review verdicts. Same exit code.' },
|
|
728
729
|
{ name: '--timeout <ms>', desc: 'Override the 30s default' },
|
|
729
730
|
],
|
|
730
731
|
examples: [
|
|
731
732
|
{ cmd: 'beehaven call note-create \'{"title":"Hello"}\'' },
|
|
733
|
+
{ cmd: 'beehaven call canvas-act @build.json --brief', note: 'a canvas batch, read as a one-line receipt' },
|
|
732
734
|
{ cmd: "beehaven call project-list '{}' --building home" },
|
|
733
735
|
{
|
|
734
736
|
cmd: 'beehaven call admin-agent-chat \'{"conversationId":"c1","message":"top sellers?","scope":"read"}\'',
|
|
@@ -1132,7 +1132,36 @@ export async function cmdCall(action, rawInput, values) {
|
|
|
1132
1132
|
printCallErrorHints(raw.message, action, values);
|
|
1133
1133
|
process.exit(1);
|
|
1134
1134
|
}
|
|
1135
|
-
|
|
1135
|
+
/**
|
|
1136
|
+
* MCP2-25 — `call … --out <file>` writes the whole JSON result to a file. The canvas skill tells
|
|
1137
|
+
* agents to save a large response once and read a summary, and the only way was `> file`, which a
|
|
1138
|
+
* headless agent needs permission for: a recorded Claude Code take lost a call to "`--out` is not
|
|
1139
|
+
* supported by `beehaven call`". The receipt (or a one-line note) still prints; the exit code is
|
|
1140
|
+
* the result's, exactly as without the flag.
|
|
1141
|
+
*/
|
|
1142
|
+
const outPath = values.out ?? values.output;
|
|
1143
|
+
if (typeof outPath === 'string' && outPath) {
|
|
1144
|
+
const { mkdirSync, writeFileSync } = await import('node:fs');
|
|
1145
|
+
const { dirname, resolve } = await import('node:path');
|
|
1146
|
+
const file = resolve(outPath);
|
|
1147
|
+
mkdirSync(dirname(file), { recursive: true });
|
|
1148
|
+
const text = result && typeof result === 'object' ? JSON.stringify(result, null, 2) : String(result ?? '');
|
|
1149
|
+
writeFileSync(file, text + '\n');
|
|
1150
|
+
if (!values.brief)
|
|
1151
|
+
console.log(`wrote ${text.length.toLocaleString()} bytes to ${outPath}`);
|
|
1152
|
+
}
|
|
1153
|
+
let briefFailed = false;
|
|
1154
|
+
if (values.brief && !values.json) {
|
|
1155
|
+
// ⚠ THE SAME RESULT, READ FOR A PERSON — see call-brief.ts for what is kept and why.
|
|
1156
|
+
const { briefReceipt } = await import('../call-brief.js');
|
|
1157
|
+
const receipt = briefReceipt(action, result);
|
|
1158
|
+
console.log(receipt.lines.join('\n'));
|
|
1159
|
+
briefFailed = !receipt.ok;
|
|
1160
|
+
}
|
|
1161
|
+
else if (typeof outPath === 'string' && outPath) {
|
|
1162
|
+
// Written above; the console keeps only the note, so a large result stays out of the context.
|
|
1163
|
+
}
|
|
1164
|
+
else if (result && typeof result === 'object') {
|
|
1136
1165
|
console.log(JSON.stringify(result, null, 2));
|
|
1137
1166
|
}
|
|
1138
1167
|
else {
|
|
@@ -1234,7 +1263,7 @@ export async function cmdCall(action, rawInput, values) {
|
|
|
1234
1263
|
}
|
|
1235
1264
|
printKnownActionAdvice(action, input, result);
|
|
1236
1265
|
}
|
|
1237
|
-
if (isFailureResult(result))
|
|
1266
|
+
if (briefFailed || isFailureResult(result))
|
|
1238
1267
|
process.exit(1);
|
|
1239
1268
|
}
|
|
1240
1269
|
catch (err) {
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export async function waitForConnection(probe, expectedEndpoint, options = {}) {
|
|
2
|
+
const now = options.now ?? Date.now;
|
|
3
|
+
const sleep = options.sleep ?? ((ms) => new Promise(resolve => setTimeout(resolve, ms)));
|
|
4
|
+
const deadline = now() + (options.timeoutMs ?? 30_000);
|
|
5
|
+
const normalize = (value) => value.replace(/\/+$/, '');
|
|
6
|
+
let reason = 'starting';
|
|
7
|
+
do {
|
|
8
|
+
try {
|
|
9
|
+
const state = await probe();
|
|
10
|
+
if (state.endpoint && normalize(state.endpoint) !== normalize(expectedEndpoint))
|
|
11
|
+
return { ready: false, reason: 'environment-mismatch' };
|
|
12
|
+
if (state.connected && state.endpoint && normalize(state.endpoint) === normalize(expectedEndpoint))
|
|
13
|
+
return { ready: true, reason: 'connected' };
|
|
14
|
+
reason = 'relay-unavailable';
|
|
15
|
+
}
|
|
16
|
+
catch {
|
|
17
|
+
reason = 'starting';
|
|
18
|
+
}
|
|
19
|
+
const remaining = deadline - now();
|
|
20
|
+
if (remaining <= 0)
|
|
21
|
+
break;
|
|
22
|
+
await sleep(Math.min(options.pollMs ?? 250, remaining));
|
|
23
|
+
} while (now() < deadline);
|
|
24
|
+
return { ready: false, reason };
|
|
25
|
+
}
|
package/dist/daemon-client.js
CHANGED
|
@@ -18,6 +18,7 @@ import net from 'net';
|
|
|
18
18
|
import { resolveClientAuthToken } from './ipc-auth.js';
|
|
19
19
|
import { activeAgentWrap } from './agent-attribution.js';
|
|
20
20
|
import { ipcSocketPath, socketMayExist } from './ipc-path.js';
|
|
21
|
+
import { utf8Stream } from './utf8-stream.js';
|
|
21
22
|
const TIMEOUT_MS = 60_000;
|
|
22
23
|
const AUTH_TIMEOUT_MS = 3_000;
|
|
23
24
|
/** One attempt. Resolves, never rejects. */
|
|
@@ -112,8 +113,10 @@ function handshake(socket) {
|
|
|
112
113
|
socket.removeListener('data', onData);
|
|
113
114
|
reject(new Error('IPC auth handshake timeout'));
|
|
114
115
|
}, AUTH_TIMEOUT_MS);
|
|
116
|
+
// CA-63 — decode the stream, not each chunk: a character split across two reads stays whole.
|
|
117
|
+
const decode = utf8Stream();
|
|
115
118
|
const onData = (chunk) => {
|
|
116
|
-
buffer += chunk
|
|
119
|
+
buffer += decode(chunk);
|
|
117
120
|
const lines = buffer.split('\n');
|
|
118
121
|
buffer = lines.pop() || '';
|
|
119
122
|
for (const line of lines) {
|
|
@@ -218,8 +221,10 @@ function sendIpc(socket, msg) {
|
|
|
218
221
|
socket.removeListener('data', onData);
|
|
219
222
|
reject(new Error(`Daemon RPC timeout after ${TIMEOUT_MS}ms`));
|
|
220
223
|
}, TIMEOUT_MS);
|
|
224
|
+
// CA-63 — decode the stream, not each chunk: a character split across two reads stays whole.
|
|
225
|
+
const decode = utf8Stream();
|
|
221
226
|
const onData = (chunk) => {
|
|
222
|
-
buffer += chunk
|
|
227
|
+
buffer += decode(chunk);
|
|
223
228
|
const lines = buffer.split('\n');
|
|
224
229
|
buffer = lines.pop() || '';
|
|
225
230
|
for (const line of lines) {
|
package/dist/daemon-process.js
CHANGED
|
@@ -8,8 +8,15 @@
|
|
|
8
8
|
* daemon. Windows reads command lines from one CIM query instead.
|
|
9
9
|
*/
|
|
10
10
|
import { execFileSync } from 'child_process';
|
|
11
|
+
import { fileURLToPath } from 'node:url';
|
|
12
|
+
export const CURRENT_DAEMON_ENTRY = fileURLToPath(new URL('./daemon-entry.js', import.meta.url));
|
|
13
|
+
/** pgrep receives a regex argument, so every metacharacter in a user-chosen path is literal. */
|
|
14
|
+
export function daemonEntryPattern(entry = CURRENT_DAEMON_ENTRY) {
|
|
15
|
+
const special = '\\.^$*+?()[]{}|';
|
|
16
|
+
return Array.from(entry, character => special.includes(character) ? '\\' + character : character).join('');
|
|
17
|
+
}
|
|
11
18
|
/**
|
|
12
|
-
* Whether a full command line is a beehaven daemon or CLI
|
|
19
|
+
* Whether a full command line is a beehaven daemon or a legacy CLI-start daemon.
|
|
13
20
|
*
|
|
14
21
|
* Strict two-condition test (unchanged on macOS/Linux):
|
|
15
22
|
* 1. argv[0] is a node/tsx/beehaven binary — not a shell, an editor, a grep, etc.;
|
|
@@ -20,14 +27,17 @@ import { execFileSync } from 'child_process';
|
|
|
20
27
|
* an extension (`"C:\Program Files\nodejs\node.exe" C:\…\daemon-entry.js`). It is reduced to the
|
|
21
28
|
* Unix shape (`C:/Program_Files/nodejs/node …`) so the same two conditions apply, unquoted.
|
|
22
29
|
*/
|
|
23
|
-
export function isBeehavenCommandLine(raw, platform = process.platform) {
|
|
30
|
+
export function isBeehavenCommandLine(raw, platform = process.platform, expectedEntry = CURRENT_DAEMON_ENTRY, executable = process.execPath) {
|
|
24
31
|
let cmd = (raw ?? '').trim();
|
|
25
32
|
if (!cmd)
|
|
26
33
|
return false;
|
|
34
|
+
const ownExecutable = platform !== 'win32' && (cmd.startsWith(executable + ' ') || cmd.startsWith(`"${executable}" `));
|
|
35
|
+
let tail = ownExecutable ? cmd.slice(cmd.startsWith('"') ? executable.length + 3 : executable.length + 1).trim() : cmd.replace(/^\S+\s*/, '');
|
|
27
36
|
if (platform === 'win32') {
|
|
28
37
|
const m = /^"([^"]*)"\s*(.*)$/s.exec(cmd) ?? /^(\S+)\s*(.*)$/s.exec(cmd);
|
|
29
38
|
if (!m)
|
|
30
39
|
return false;
|
|
40
|
+
tail = m[2].trim();
|
|
31
41
|
const argv0 = m[1].replace(/\\/g, '/').replace(/\.(exe|cmd)$/i, '').replace(/\s+/g, '_').toLowerCase();
|
|
32
42
|
cmd = `${argv0} ${m[2].replace(/"/g, '')}`.trim();
|
|
33
43
|
}
|
|
@@ -35,15 +45,20 @@ export function isBeehavenCommandLine(raw, platform = process.platform) {
|
|
|
35
45
|
// running a heredoc that contains "node .../cli.js" deeper in its argv doesn't false-positive.
|
|
36
46
|
const argv0Ok = /^(\S*\/)?node(\s|$)/.test(cmd) ||
|
|
37
47
|
/^(\S*\/)?tsx(\s|$)/.test(cmd) ||
|
|
38
|
-
/^(\S*\/)?beehaven(\s|$)/.test(cmd);
|
|
48
|
+
/^(\S*\/)?beehaven(\s|$)/.test(cmd) || ownExecutable;
|
|
39
49
|
if (!argv0Ok)
|
|
40
50
|
return false;
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
//
|
|
45
|
-
//
|
|
46
|
-
|
|
51
|
+
const firstArgument = tail.startsWith('"') ? tail.slice(1).split('"')[0] : tail.split(/\s/)[0];
|
|
52
|
+
if (/^-(?:e|p|ep|pe)$|^--(?:eval|print)(?:=|$)/.test(firstArgument))
|
|
53
|
+
return false;
|
|
54
|
+
// A custom installer prefix need not contain the word "beehaven". Trust only this package's
|
|
55
|
+
// exact entry as the direct script argument; a quoted mention inside `node -e` is not a daemon.
|
|
56
|
+
const normalize = (s) => platform === 'win32' ? s.replace(/\\/g, '/').toLowerCase() : s;
|
|
57
|
+
const rest = normalize(tail), entry = normalize(expectedEntry);
|
|
58
|
+
if ([entry, `"${entry}"`].some(value => rest === value || rest.startsWith(value + ' ')))
|
|
59
|
+
return true;
|
|
60
|
+
// Windows has no legacy CLI-start daemons. Unix retains that old start shape, but ordinary
|
|
61
|
+
// CLI calls and eval/print processes are never daemon candidates on any platform.
|
|
47
62
|
if (platform === 'win32')
|
|
48
63
|
return /[/\\]\.?beehaven([-_]sdk)?[/\\].*daemon-entry\.(js|ts)\b/.test(cmd);
|
|
49
64
|
// Condition 2: a subsequent argument must reference a beehaven script.
|
|
@@ -59,8 +74,8 @@ export function isBeehavenCommandLine(raw, platform = process.platform) {
|
|
|
59
74
|
/[/\\]\.?beehaven([-_]sdk)?[/\\].*daemon-entry\.(js|ts)\b/.test(cmd) ||
|
|
60
75
|
/Clearly[^/\\]*\.app[/\\]Contents[/\\]Resources[/\\]beehaven[/\\]daemon-entry\.js\b/.test(cmd) ||
|
|
61
76
|
// Legacy `cli.js start` — for old bundles pre-dating daemon-entry.js.
|
|
62
|
-
/[/\\]\.?beehaven([-_]sdk)?[/\\].*cli\.(js|ts)
|
|
63
|
-
/Clearly[^/\\]*\.app[/\\]Contents[/\\]Resources[/\\]beehaven[/\\]cli\.js
|
|
77
|
+
/[/\\]\.?beehaven([-_]sdk)?[/\\].*cli\.(js|ts)["']?\s+start(?:\s|$)/.test(cmd) ||
|
|
78
|
+
/Clearly[^/\\]*\.app[/\\]Contents[/\\]Resources[/\\]beehaven[/\\]cli\.js["']?\s+start(?:\s|$)/.test(cmd) ||
|
|
64
79
|
// Bare `beehaven start` — wrapper already matched argv[0].
|
|
65
80
|
/^(\S*\/)?beehaven\s+start\b/.test(cmd));
|
|
66
81
|
}
|
package/dist/daemon.js
CHANGED
|
@@ -14,7 +14,7 @@ import { browsePath } from './file-browser.js';
|
|
|
14
14
|
import { CloudBeeProxy } from './bee/cloud-proxy.js';
|
|
15
15
|
import { IpcServer, probeIpcSocket } from './ipc-server.js';
|
|
16
16
|
import { ipcSocketPath } from './ipc-path.js';
|
|
17
|
-
import { isBeehavenCommandLine, windowsNodeProcesses, selectWindowsDaemons } from './daemon-process.js';
|
|
17
|
+
import { isBeehavenCommandLine, windowsNodeProcesses, selectWindowsDaemons, daemonEntryPattern } from './daemon-process.js';
|
|
18
18
|
import { mintIpcAuthTokens, clearIpcAuthToken } from './ipc-auth.js';
|
|
19
19
|
/**
|
|
20
20
|
* What THIS daemon minted, so shutdown can avoid scrubbing a successor's token.
|
|
@@ -41,7 +41,7 @@ import { AgentSessions } from './agent-sessions.js';
|
|
|
41
41
|
import { RELAY_PRESETS, relayUrlFromConfig, resolveRelaySelection } from './relay-presets.js';
|
|
42
42
|
import { readFileSync, existsSync, mkdirSync, mkdtempSync, writeFileSync, readdirSync, chmodSync, unlinkSync, rmSync, } from 'fs';
|
|
43
43
|
import { createHash } from 'crypto';
|
|
44
|
-
import { execFile, execSync, spawn } from 'child_process';
|
|
44
|
+
import { execFile, execFileSync, execSync, spawn } from 'child_process';
|
|
45
45
|
import { homedir, tmpdir } from 'os';
|
|
46
46
|
import { join, dirname, relative } from 'path';
|
|
47
47
|
import { fileURLToPath } from 'url';
|
|
@@ -1228,13 +1228,14 @@ export function findLiveDaemons() {
|
|
|
1228
1228
|
// re-validated by isBeehavenProcess so a broad pgrep hit still has
|
|
1229
1229
|
// to pass the strict regex check before being considered ours.
|
|
1230
1230
|
const patterns = [
|
|
1231
|
+
daemonEntryPattern(),
|
|
1231
1232
|
'beehaven.*daemon-entry\\.(js|ts)',
|
|
1232
1233
|
'beehaven.*cli\\.(js|ts)',
|
|
1233
1234
|
'Clearly.*Resources/beehaven',
|
|
1234
1235
|
];
|
|
1235
1236
|
for (const pattern of patterns) {
|
|
1236
1237
|
try {
|
|
1237
|
-
const out =
|
|
1238
|
+
const out = execFileSync('pgrep', ['-f', pattern], {
|
|
1238
1239
|
encoding: 'utf8',
|
|
1239
1240
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
1240
1241
|
}).trim();
|
package/dist/ipc-client.js
CHANGED
|
@@ -11,6 +11,7 @@ import { EventEmitter } from 'events';
|
|
|
11
11
|
import { resolveClientAuthToken } from './ipc-auth.js';
|
|
12
12
|
import { ipcSocketPath, socketMayExist } from './ipc-path.js';
|
|
13
13
|
import { activeAgentWrap } from './agent-attribution.js';
|
|
14
|
+
import { utf8Stream } from './utf8-stream.js';
|
|
14
15
|
/** The default address (Unix socket, or this user's named pipe on Windows) — see `ipc-path.ts`. */
|
|
15
16
|
const IPC_SOCKET_PATH = ipcSocketPath();
|
|
16
17
|
/** Default timeout for RPC calls (ms) */
|
|
@@ -77,8 +78,10 @@ export class IpcClient extends EventEmitter {
|
|
|
77
78
|
socket.on('connect', () => {
|
|
78
79
|
this._connected = true;
|
|
79
80
|
});
|
|
81
|
+
// CA-63 — decode the stream, not each chunk: a character split across two reads stays whole.
|
|
82
|
+
const decode = utf8Stream();
|
|
80
83
|
socket.on('data', (data) => {
|
|
81
|
-
this.buffer += data
|
|
84
|
+
this.buffer += decode(data);
|
|
82
85
|
const lines = this.buffer.split('\n');
|
|
83
86
|
this.buffer = lines.pop() || '';
|
|
84
87
|
for (const line of lines) {
|
package/dist/ipc-server.js
CHANGED
|
@@ -27,6 +27,7 @@ import { createConnection, createServer } from 'net';
|
|
|
27
27
|
import { EventEmitter } from 'events';
|
|
28
28
|
import { resolveAuthScope } from './ipc-auth.js';
|
|
29
29
|
import { ipcSocketPath, socketMayExist, clearStaleSocket, restrictSocketToOwner } from './ipc-path.js';
|
|
30
|
+
import { utf8Stream } from './utf8-stream.js';
|
|
30
31
|
/** The default address (Unix socket, or this user's named pipe on Windows) — see `ipc-path.ts`. */
|
|
31
32
|
const IPC_SOCKET_PATH = ipcSocketPath();
|
|
32
33
|
/**
|
|
@@ -194,8 +195,10 @@ export class IpcServer extends EventEmitter {
|
|
|
194
195
|
daemonPid: process.pid,
|
|
195
196
|
requireAuth: true,
|
|
196
197
|
});
|
|
198
|
+
// CA-63 — decode the stream, not each chunk: a character split across two reads stays whole.
|
|
199
|
+
const decode = utf8Stream();
|
|
197
200
|
socket.on('data', (data) => {
|
|
198
|
-
client.buffer += data
|
|
201
|
+
client.buffer += decode(data);
|
|
199
202
|
const lines = client.buffer.split('\n');
|
|
200
203
|
client.buffer = lines.pop() || '';
|
|
201
204
|
for (const line of lines) {
|