beehaven 0.8.37 → 0.8.39
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-limit-refusal.js +43 -0
- package/dist/agent-mint-response.js +10 -0
- package/dist/agent-state.js +8 -1
- package/dist/build-channel.js +2 -2
- package/dist/call-brief.js +22 -7
- package/dist/clearly.js +0 -0
- package/dist/cli.js +14 -3
- package/dist/commands/agent.js +27 -3
- package/dist/commands/daemon-cmd.js +17 -1
- package/dist/commands/workspace.js +19 -9
- package/dist/connection-ready.js +25 -0
- package/dist/daemon-process.js +26 -11
- package/dist/daemon.js +4 -3
- package/dist/limit-advice.js +32 -0
- package/dist/relay.js +10 -1
- package/package.json +1 -1
- package/scripts/sync-skills.mjs +12 -0
- package/skills/clearly-agent/SKILL.md +13 -1
- package/skills/clearly-artifacts/SKILL.md +6 -5
- package/skills/clearly-docs/SKILL.md +39 -2
- package/skills/clearly-init/SKILL.md +19 -10
- package/skills/clearly-workflows/SKILL.md +3 -3
- package/skills/index.json +21 -6
- package/skills/print-production/SKILL.md +96 -0
- package/skills/print-production/formats.md +90 -0
- package/skills/social-posts/SKILL.md +46 -0
- package/skills/visualize/SKILL.md +5 -6
- package/dist/daemon-codex.js +0 -349
- package/dist/daemon-write-sandbox.js +0 -191
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP2-65 — what `beehaven agent login <new name>` says when the workspace already has all the
|
|
3
|
+
* agents its plan includes (Free includes one).
|
|
4
|
+
*
|
|
5
|
+
* The identity mints at the account; joining the workspace is where the cap refuses
|
|
6
|
+
* (`agent-session-start` → automatic admission → `agent-create`, code `agent-limit-reached`). The
|
|
7
|
+
* reason alone left the person at a wall. The three choices, in this order:
|
|
8
|
+
* 1. log in as an agent they already have — one agent works from several apps;
|
|
9
|
+
* 2. upgrade for more agents (a pricing link: this is the terminal, not an MCP tool result);
|
|
10
|
+
* 3. archive one in Agents settings, then log in again.
|
|
11
|
+
*
|
|
12
|
+
* Pure: returns plain lines, the command colours them.
|
|
13
|
+
*/
|
|
14
|
+
export const AGENT_LIMIT_CODE = 'agent-limit-reached';
|
|
15
|
+
export function isAgentLimitRefusal(code) {
|
|
16
|
+
return String(code ?? '') === AGENT_LIMIT_CODE;
|
|
17
|
+
}
|
|
18
|
+
/** An absolute pricing link from the refusal's relative `upgradeUrl` and the web origin for this env. */
|
|
19
|
+
export function upgradeLink(origin, upgradeUrl) {
|
|
20
|
+
const base = String(origin || 'https://www.clearly.sh').replace(/\/+$/, '');
|
|
21
|
+
const raw = typeof upgradeUrl === 'string' ? upgradeUrl.trim() : '';
|
|
22
|
+
// Only a site-relative path is joined; anything else falls back to the pricing page.
|
|
23
|
+
const path = /^\/(?!\/)[A-Za-z0-9/_?=&.-]*$/.test(raw) ? raw : '/pricing';
|
|
24
|
+
return `${base}${path}`;
|
|
25
|
+
}
|
|
26
|
+
export function agentLimitLoginLines(opts) {
|
|
27
|
+
const agents = (Array.isArray(opts.details?.activeAgents) ? opts.details.activeAgents : [])
|
|
28
|
+
.map((a) => ({ id: String(a?.id ?? '').trim(), name: String(a?.name ?? '').trim() }))
|
|
29
|
+
.filter((a) => a.id && a.id !== opts.name);
|
|
30
|
+
const first = agents[0];
|
|
31
|
+
const listed = agents.map((a) => (a.name && a.name !== a.id ? `${a.id} ("${a.name}")` : a.id)).join(', ');
|
|
32
|
+
const reason = String(opts.reason || '').trim();
|
|
33
|
+
return {
|
|
34
|
+
headline: `Signed in, but ${opts.workspace || 'this workspace'} is at its agent limit${reason ? `: ${reason}` : '.'}`,
|
|
35
|
+
choices: [
|
|
36
|
+
first
|
|
37
|
+
? `Use the agent you already have (one agent works from several apps): beehaven agent login ${first.id}${agents.length > 1 ? ` · also yours here: ${listed}` : ''}`
|
|
38
|
+
: 'Use an agent you already have (one agent works from several apps): beehaven agent login <its name>',
|
|
39
|
+
`Upgrade for more agents: ${upgradeLink(opts.origin, opts.details?.upgradeUrl)}`,
|
|
40
|
+
`Or archive one in Agents settings, then run: beehaven agent login ${opts.name}`,
|
|
41
|
+
],
|
|
42
|
+
};
|
|
43
|
+
}
|
|
@@ -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/agent-state.js
CHANGED
|
@@ -365,11 +365,18 @@ export function loginSessionOutcome(started, thrown) {
|
|
|
365
365
|
return { kind: 'started', sessionId: id, brief: payload?.brief };
|
|
366
366
|
const err = payload?.error ?? started?.error;
|
|
367
367
|
if (err) {
|
|
368
|
+
const code = String(payload?.code ?? started?.code ?? '');
|
|
368
369
|
return {
|
|
369
370
|
kind: 'refused',
|
|
370
371
|
reason: typeof err === 'string' ? err : String(err?.message ?? JSON.stringify(err)),
|
|
371
|
-
code
|
|
372
|
+
code,
|
|
372
373
|
scope: String(payload?.scope ?? started?.scope ?? ''),
|
|
374
|
+
// MCP2-65: the agent-cap refusal names the caller's live agents and a pricing path, so the
|
|
375
|
+
// login can print the three choices instead of a bare reason.
|
|
376
|
+
...(code === 'agent-limit-reached' ? { details: {
|
|
377
|
+
activeAgents: Array.isArray(payload?.activeAgents) ? payload.activeAgents : [],
|
|
378
|
+
...(typeof payload?.upgradeUrl === 'string' ? { upgradeUrl: payload.upgradeUrl } : {}),
|
|
379
|
+
} } : {}),
|
|
373
380
|
};
|
|
374
381
|
}
|
|
375
382
|
return { kind: 'refused', reason: 'the workspace returned no session', code: '', scope: '' };
|
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 = '3026e5a147f9';
|
|
57
57
|
/** ISO-8601 minute precision, UTC. Rewritten in `dist/` by `scripts/stamp-build-channel.mjs`. */
|
|
58
|
-
export const BUILD_TIME = '2026-
|
|
58
|
+
export const BUILD_TIME = '2026-11-12T22:52Z';
|
|
59
59
|
/**
|
|
60
60
|
* The build identity as one short string, for `--version`.
|
|
61
61
|
*
|
package/dist/call-brief.js
CHANGED
|
@@ -58,14 +58,29 @@ function batchReceipt(action, r) {
|
|
|
58
58
|
for (const s of steps)
|
|
59
59
|
counts.set(verb(s?.action ?? '?'), (counts.get(verb(s?.action ?? '?')) ?? 0) + 1);
|
|
60
60
|
const mix = [...counts].sort((a, b) => b[1] - a[1]).slice(0, 3).map(([a, n]) => (n > 1 ? `${a} ×${n}` : a)).join(', ');
|
|
61
|
+
/**
|
|
62
|
+
* CDS-163 — THE BATCH'S OWN DELTA WINS. Only update steps carry a per-step `canvasChanges`, so
|
|
63
|
+
* summing steps printed "+0 ~69 −0 layers" for a 343-step build that created 211 layers. The worker
|
|
64
|
+
* returns one batch-level `canvasChanges`, diffed over every node the batch addressed (creates
|
|
65
|
+
* included); the per-step sum is kept for an older worker that does not send it.
|
|
66
|
+
*/
|
|
61
67
|
let added = 0, changed = 0, removed = 0, sawDelta = false;
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
68
|
+
const isDelta = (d) => !!d && typeof d === 'object' && !Array.isArray(d);
|
|
69
|
+
if (isDelta(r.canvasChanges)) {
|
|
70
|
+
sawDelta = true;
|
|
71
|
+
added = Number(r.canvasChanges.added || 0);
|
|
72
|
+
changed = Number(r.canvasChanges.changed || 0);
|
|
73
|
+
removed = Number(r.canvasChanges.removed || 0);
|
|
74
|
+
}
|
|
75
|
+
else {
|
|
76
|
+
for (const s of steps) {
|
|
77
|
+
const d = s?.value?.canvasChanges;
|
|
78
|
+
if (isDelta(d)) {
|
|
79
|
+
sawDelta = true;
|
|
80
|
+
added += Number(d.added || 0);
|
|
81
|
+
changed += Number(d.changed || 0);
|
|
82
|
+
removed += Number(d.removed || 0);
|
|
83
|
+
}
|
|
69
84
|
}
|
|
70
85
|
}
|
|
71
86
|
const rev = r.canvasNodesRev ?? [...steps].reverse().find((s) => s?.value?.canvasNodesRev != null)?.value?.canvasNodesRev;
|
package/dist/clearly.js
CHANGED
|
File without changes
|
package/dist/cli.js
CHANGED
|
@@ -1881,6 +1881,7 @@ async function run() {
|
|
|
1881
1881
|
* would be refused while `conversations --json` worked.
|
|
1882
1882
|
*/
|
|
1883
1883
|
const JSON_CAPABLE = new Set([
|
|
1884
|
+
'doc', 'search',
|
|
1884
1885
|
'admin', 'apps', 'd1', 'describe', 'explain', 'marketplace', 'secrets',
|
|
1885
1886
|
'shop', 'user', 'webhook', 'webhooks', 'workspaces', 'ws',
|
|
1886
1887
|
// Honour what the modules already implement.
|
|
@@ -3064,13 +3065,13 @@ async function run() {
|
|
|
3064
3065
|
}
|
|
3065
3066
|
console.log('');
|
|
3066
3067
|
console.log(` ${color.green('✓')} Signed in as ${color.bold(captured.email ?? captured.userId)}`);
|
|
3067
|
-
console.log(`
|
|
3068
|
+
console.log(` Sign-in saved on this device`);
|
|
3068
3069
|
// Auto-restart the daemon. New tokens won't take effect until
|
|
3069
3070
|
// the daemon restarts (it reads creds at startup, keeps them
|
|
3070
3071
|
// in memory, has no hot-reload IPC verb). Doing the restart
|
|
3071
3072
|
// here matches gh / firebase-tools UX — login-then-go.
|
|
3072
3073
|
const { findLiveDaemons, stopDaemonProcess } = await import('./daemon.js');
|
|
3073
|
-
const { spawnStandaloneDaemon } = await import('./commands/daemon-cmd.js');
|
|
3074
|
+
const { spawnStandaloneDaemon, waitForLoginConnection } = await import('./commands/daemon-cmd.js');
|
|
3074
3075
|
/**
|
|
3075
3076
|
* ⚠⚠ THE RESTART IS PLUMBING AND WAS NARRATED AS THOUGH IT WERE THE POINT. What a person saw
|
|
3076
3077
|
* after signing in was:
|
|
@@ -3115,6 +3116,7 @@ async function run() {
|
|
|
3115
3116
|
* started with developer scope.
|
|
3116
3117
|
*/
|
|
3117
3118
|
const pid = spawnStandaloneDaemon(values);
|
|
3119
|
+
const connection = await waitForLoginConnection();
|
|
3118
3120
|
/**
|
|
3119
3121
|
* ⚠ ONE OUTCOME LINE, IN THE SAME SHAPE AS THE ONE ABOVE IT. `✓ (PID 73532)` was a tick, a
|
|
3120
3122
|
* bracket and a number — grammatically nothing, and unreadable as a result. This says what
|
|
@@ -3125,7 +3127,16 @@ async function run() {
|
|
|
3125
3127
|
* `beehaven status` labels the same thing `Relay:`. Naming the outcome rather than the
|
|
3126
3128
|
* process is what stops a sign-in ending on the word "stopped".
|
|
3127
3129
|
*/
|
|
3128
|
-
|
|
3130
|
+
if (connection.ready) {
|
|
3131
|
+
console.log(` ${color.green('✓')} Connected — your new sign-in is live`);
|
|
3132
|
+
}
|
|
3133
|
+
else {
|
|
3134
|
+
console.error(` ${color.yellow('!')} Signed in, but the workspace connection is not ready.`);
|
|
3135
|
+
console.error(connection.reason === 'environment-mismatch'
|
|
3136
|
+
? ' The running service is on a different environment. Run `beehaven status` before retrying.'
|
|
3137
|
+
: ' Run `beehaven status` to check the connection, then retry your workspace command.');
|
|
3138
|
+
process.exitCode = 2;
|
|
3139
|
+
}
|
|
3129
3140
|
console.log(` Background service ${wasRunning ? 'restarted' : 'started'} (PID ${pid})`);
|
|
3130
3141
|
/**
|
|
3131
3142
|
* ⚠⚠ 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,8 @@ 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';
|
|
29
|
+
import { agentLimitLoginLines, isAgentLimitRefusal } from '../agent-limit-refusal.js';
|
|
28
30
|
import { saveAgent, loadAgent, listAgents, deleteAgent, findAgentByAvatar, isValidAgentName, resolveActiveAgent, activeAgentName, saveAgentSession, clearAgentSession, clearAgentSessionId, decodeAgentTokenClaims, } from '../agent-state.js';
|
|
29
31
|
const VALID_KINDS = ['claude-code', 'daemon-agent', 'clearly-ai'];
|
|
30
32
|
export const FREE_SKINS = [
|
|
@@ -643,7 +645,7 @@ async function agentLogin(args, values) {
|
|
|
643
645
|
}
|
|
644
646
|
const avatarId = requestedAvatarId || existingRecord?.avatarId || chosenSkin || name;
|
|
645
647
|
// Local-first avatar-collision guard (durable guard is the cloud pool).
|
|
646
|
-
const collision = findAgentByAvatar(
|
|
648
|
+
const collision = requestedAvatarId ? findAgentByAvatar(requestedAvatarId, name) : null;
|
|
647
649
|
if (collision) {
|
|
648
650
|
console.error(` Avatar "${avatarId}" is already claimed by agent "${collision}".`);
|
|
649
651
|
console.error(` Pick another: beehaven agent login ${name} --avatar <other-id>`);
|
|
@@ -670,7 +672,7 @@ async function agentLogin(args, values) {
|
|
|
670
672
|
* credential.
|
|
671
673
|
*/
|
|
672
674
|
try {
|
|
673
|
-
const cloud = await rpcOnce('agent-account-mint',
|
|
675
|
+
const cloud = accountMintResponse(await rpcOnce('agent-account-mint', accountMintRequest(name, requestedAvatarId), 15_000));
|
|
674
676
|
const tok = cloud?.token ?? cloud?.result?.token;
|
|
675
677
|
if (tok) {
|
|
676
678
|
minted = {
|
|
@@ -698,7 +700,14 @@ async function agentLogin(args, values) {
|
|
|
698
700
|
}
|
|
699
701
|
catch (err) {
|
|
700
702
|
console.error(` Could not mint identity: ${err?.message ?? err}`);
|
|
701
|
-
|
|
703
|
+
if (String(err?.code) === 'identity-limit-reached') {
|
|
704
|
+
// MCP2-71: the account's safety ceiling — no plan lifts it; signing in as one you have always works.
|
|
705
|
+
console.error(' Sign in as an agent you already have: beehaven agent list, then beehaven agent login <name>');
|
|
706
|
+
console.error(' Or delete one you no longer use in Agents, then run this again.');
|
|
707
|
+
process.exit(1);
|
|
708
|
+
}
|
|
709
|
+
if (!err?.code || /^(ECONN|ENOENT|ETIMEDOUT)/.test(String(err.code)))
|
|
710
|
+
console.error(' Is the daemon connected? Check with: beehaven status');
|
|
702
711
|
process.exit(1);
|
|
703
712
|
}
|
|
704
713
|
/**
|
|
@@ -962,6 +971,21 @@ async function agentLogin(args, values) {
|
|
|
962
971
|
console.log(` ${green('◷')} Workspace work session ${cyan(outcome.sessionId.slice(0, 8))} started.`);
|
|
963
972
|
printBrief(outcome.brief);
|
|
964
973
|
}
|
|
974
|
+
else if (outcome.kind === 'refused' && isAgentLimitRefusal(outcome.code)) {
|
|
975
|
+
// MCP2-65: at the plan's agent limit, say what to do — not just that it was refused.
|
|
976
|
+
const { resolveAuthOrigin } = await import('../browser-login.js');
|
|
977
|
+
const lines = agentLimitLoginLines({
|
|
978
|
+
name, workspace: (await workspaceLabel(chosenWorkspace)).split(/ {2}\(| — /)[0], reason: outcome.reason,
|
|
979
|
+
details: outcome.details, origin: resolveAuthOrigin().authOrigin,
|
|
980
|
+
});
|
|
981
|
+
console.log('');
|
|
982
|
+
console.error(` ${yellow('⚠')} ${lines.headline}`);
|
|
983
|
+
console.error(' Your choices:');
|
|
984
|
+
for (const line of lines.choices)
|
|
985
|
+
console.error(` · ${line}`);
|
|
986
|
+
console.error(dim(` Calls from this session run as ${name} and will be refused — they will NOT fall back to another agent.`));
|
|
987
|
+
process.exitCode = 1;
|
|
988
|
+
}
|
|
965
989
|
else if (outcome.kind === 'refused') {
|
|
966
990
|
console.log('');
|
|
967
991
|
console.error(` ${yellow('⚠')} Signed in, but this workspace refused ${cyan(name)}: ${outcome.reason}`);
|
|
@@ -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 */ }
|
|
@@ -26,6 +26,8 @@ import { resolveActiveAgent } from '../agent-state.js';
|
|
|
26
26
|
import { isAgentRuntime } from '../agent-scope.js';
|
|
27
27
|
import { getTarget } from '../cli-state.js';
|
|
28
28
|
import { attachShellCwd, absorbShellCwd, shellTargetKey } from '../shell-session.js';
|
|
29
|
+
import { planLimitAdvice } from '../limit-advice.js';
|
|
30
|
+
import { resolveAuthOrigin } from '../browser-login.js';
|
|
29
31
|
/** Workspace-agent jobs wait for the server's ceiling (900s) plus the report's tail. */
|
|
30
32
|
export const AGENT_TURN_TIMEOUT_MS = 930_000;
|
|
31
33
|
const AGENT_TURN_ACTIONS = new Set(['agent-prompt', 'agent-turn']);
|
|
@@ -1287,6 +1289,16 @@ function printKnownActionAdvice(action, input, result) {
|
|
|
1287
1289
|
if (action === 'ticket-get' && result?.code === 'not-found') {
|
|
1288
1290
|
console.error(' If this key belongs to a document, read it with document-get; ticket-get reads board cards only.');
|
|
1289
1291
|
}
|
|
1292
|
+
// MCP2-70 / MCP2-71: a plan or account limit says what is true and what to do, in words.
|
|
1293
|
+
if (/-limit-reached$/.test(String(result?.code ?? ''))) {
|
|
1294
|
+
let origin = 'https://www.clearly.sh';
|
|
1295
|
+
try {
|
|
1296
|
+
origin = resolveAuthOrigin().authOrigin;
|
|
1297
|
+
}
|
|
1298
|
+
catch { /* the default web origin */ }
|
|
1299
|
+
for (const line of planLimitAdvice(result, origin))
|
|
1300
|
+
console.error(line);
|
|
1301
|
+
}
|
|
1290
1302
|
}
|
|
1291
1303
|
/**
|
|
1292
1304
|
* Emit navigation hints for common call failures. Agents parse these to
|
|
@@ -2463,8 +2475,8 @@ export async function cmdTask(title, values) {
|
|
|
2463
2475
|
}
|
|
2464
2476
|
// ── beehaven doc add <file> — local file → searchable project document ───
|
|
2465
2477
|
//
|
|
2466
|
-
// One-shot: reads a local file
|
|
2467
|
-
//
|
|
2478
|
+
// One-shot: reads a local file and creates it through document-create.
|
|
2479
|
+
// Content lands in the workspace SQLite note-content
|
|
2468
2480
|
// table + FTS5 index, so `search-notes` and the cloud agent can retrieve it.
|
|
2469
2481
|
// This is the DOCUMENT path; `teleport` is the raw-blob (R2) path. Markdown is
|
|
2470
2482
|
// stored verbatim; code is wrapped in a fenced block (with the language) so it
|
|
@@ -2525,16 +2537,14 @@ export async function cmdDocAdd(filePath, values) {
|
|
|
2525
2537
|
const title = values.title ||
|
|
2526
2538
|
(isMarkdown ? raw.match(/^#\s+(.+)$/m)?.[1]?.trim() || fileName : fileName);
|
|
2527
2539
|
try {
|
|
2528
|
-
const created = await rpcOnce('
|
|
2529
|
-
|
|
2530
|
-
fileType: 'document',
|
|
2540
|
+
const created = await rpcOnce('document-create', {
|
|
2541
|
+
projectId: values.project || undefined, title, markdown: data,
|
|
2531
2542
|
}, getTimeout(values));
|
|
2532
|
-
|
|
2543
|
+
assertNoRpcError(created, 'document-create');
|
|
2544
|
+
const noteId = created?.id;
|
|
2533
2545
|
if (!noteId) {
|
|
2534
|
-
|
|
2535
|
-
process.exit(1);
|
|
2546
|
+
throw new Error(`document-create failed: ${JSON.stringify(created)}`);
|
|
2536
2547
|
}
|
|
2537
|
-
await rpcOnce('note-content-seed', { noteId, data, title }, getTimeout(values));
|
|
2538
2548
|
if (values.json) {
|
|
2539
2549
|
console.log(JSON.stringify({
|
|
2540
2550
|
noteId,
|
|
@@ -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-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();
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Plain-language advice printed under a plan or account limit refusal from `beehaven call`
|
|
3
|
+
* (MCP2-70 workspaces, MCP2-71 identities, MCP2-65 per-workspace agents). The refusal JSON is
|
|
4
|
+
* printed first; these lines say what is true and the one thing to do next. Pure: returns lines.
|
|
5
|
+
*/
|
|
6
|
+
import { upgradeLink } from './agent-limit-refusal.js';
|
|
7
|
+
export function planLimitAdvice(result, origin) {
|
|
8
|
+
const code = String(result?.code ?? '');
|
|
9
|
+
if (code === 'workspace-limit-reached') {
|
|
10
|
+
const owned = Number(result?.current) || 1;
|
|
11
|
+
return [
|
|
12
|
+
owned > 1
|
|
13
|
+
? ` You own ${owned} workspaces and Free includes 1. Nothing has been removed.`
|
|
14
|
+
: ' Free includes 1 workspace you own, and you have it. Workspaces you were invited to don\'t count.',
|
|
15
|
+
` Upgrade your workspace to create more: ${upgradeLink(origin, result?.upgradeUrl)}`,
|
|
16
|
+
];
|
|
17
|
+
}
|
|
18
|
+
if (code === 'identity-limit-reached') {
|
|
19
|
+
return [
|
|
20
|
+
` Your account has ${Number(result?.current) || 'the most'} agents, the most one account can have (the same on every plan).`,
|
|
21
|
+
' Delete one you no longer use in Agents, or sign in as one you have: beehaven agent list',
|
|
22
|
+
];
|
|
23
|
+
}
|
|
24
|
+
if (code === 'agent-limit-reached') {
|
|
25
|
+
return [
|
|
26
|
+
' This workspace has all the agents its plan includes. One agent can work from several apps.',
|
|
27
|
+
' Use one you have (beehaven agent login <name>), archive one in Agents settings,',
|
|
28
|
+
` or upgrade for more agents: ${upgradeLink(origin, result?.upgradeUrl)}`,
|
|
29
|
+
];
|
|
30
|
+
}
|
|
31
|
+
return [];
|
|
32
|
+
}
|
package/dist/relay.js
CHANGED
|
@@ -518,7 +518,16 @@ export class Relay {
|
|
|
518
518
|
expiredNoRefresh = true;
|
|
519
519
|
}
|
|
520
520
|
}
|
|
521
|
-
catch {
|
|
521
|
+
catch {
|
|
522
|
+
// A damaged cached ID token has no decodable exp, but a valid refresh session can still
|
|
523
|
+
// recover it. Do not strand that account in an unauthenticated reconnect loop. Opaque
|
|
524
|
+
// self-hosted tokens without a refresh session retain their existing dial behavior.
|
|
525
|
+
if (this.config.refreshToken) {
|
|
526
|
+
const refreshed = await this.refreshIdToken();
|
|
527
|
+
if (refreshed && this.config)
|
|
528
|
+
token = this.config.token;
|
|
529
|
+
}
|
|
530
|
+
}
|
|
522
531
|
// If the token is already expired AND refresh failed, dialing would just
|
|
523
532
|
// get rejected — spinning the reconnect loop forever with a dead
|
|
524
533
|
// credential (the classic "daemon stuck reconnecting" state). Back off
|
package/package.json
CHANGED
package/scripts/sync-skills.mjs
CHANGED
|
@@ -47,12 +47,24 @@ const SOURCES = {
|
|
|
47
47
|
'clearly-init': 'apps/mcp-server/plugin/skills/clearly-init/SKILL.md',
|
|
48
48
|
'clearly-workflows': 'apps/mcp-server/plugin/skills/clearly-workflows/SKILL.md',
|
|
49
49
|
'clearly-artifacts': 'apps/mcp-server/plugin/skills/clearly-artifacts/SKILL.md',
|
|
50
|
+
// CC2-245 — print and social design, so the CLI teaches the same set as the app.
|
|
51
|
+
'print-production': 'apps/mcp-server/plugin/skills/print-production/SKILL.md',
|
|
52
|
+
'social-posts': 'apps/mcp-server/plugin/skills/social-posts/SKILL.md',
|
|
50
53
|
};
|
|
51
54
|
|
|
52
55
|
let missing = 0;
|
|
53
56
|
const out = join(PKG, 'skills');
|
|
54
57
|
mkdirSync(out, { recursive: true });
|
|
55
58
|
for (const [name, rel] of Object.entries(SOURCES)) {
|
|
59
|
+
// Standalone package CI has no Clearly monorepo. Its explicitly merged bundle is the input;
|
|
60
|
+
// still validate every shipped skill and regenerate the same content-hashed manifest below.
|
|
61
|
+
if (process.env.BEEHAVEN_SKILLS_SNAPSHOT === '1') {
|
|
62
|
+
if (!existsSync(join(out, name, 'SKILL.md'))) {
|
|
63
|
+
console.error(` ✗ ${name}: packaged snapshot missing`);
|
|
64
|
+
missing++;
|
|
65
|
+
}
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
56
68
|
const src = join(REPO, rel);
|
|
57
69
|
if (!existsSync(src)) {
|
|
58
70
|
/**
|
|
@@ -190,11 +190,23 @@ Anything you create is stamped `created_by: agent:<your-id>` and born **private*
|
|
|
190
190
|
- **a SIBLING agent does not**, unless it is shared;
|
|
191
191
|
- **you can see and manage your own**, and you can see your owner's work.
|
|
192
192
|
|
|
193
|
-
⚠⚠ **FILING INTO A SHARED PROJECT IS
|
|
193
|
+
⚠⚠ **FILING INTO A SHARED PROJECT IS HOW YOU SHARE.** There is no per-item ACL. If a
|
|
194
194
|
teammate or another agent needs your work, move it into a project you both belong to
|
|
195
195
|
(`project-member-add` takes an `agentId`). A project is **open** (all workspace members) or
|
|
196
196
|
**invite-only**; the workspace default is set once in Settings → Workspace → Project access.
|
|
197
197
|
|
|
198
|
+
Change it on something you made (document, sheet, deck or canvas) with one call, CLI or MCP:
|
|
199
|
+
|
|
200
|
+
```bash
|
|
201
|
+
beehaven call sharing-set '{"kind":"document","id":"PLA-5420","project":"Agent Work OS — Seed Roadmap"}'
|
|
202
|
+
beehaven call sharing-set '{"kind":"document","id":"PLA-5420","visibility":"workspace"}' # unfiled, everyone
|
|
203
|
+
beehaven call sharing-set '{"kind":"composition","id":"<canvas id>","project":null}' # back to Drafts, private
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
Over MCP: `clearly_edit { target, changes: { sharing: { project?, visibility? } } }`. The reply says
|
|
207
|
+
who can see it now. ⚠ Only on your own work: another agent's (or your owner's) is refused `not-yours`.
|
|
208
|
+
Don't copy a document to share it; the copy splits its history and comments.
|
|
209
|
+
|
|
198
210
|
⚠ Creating something does **not** put it in your owner's Recents — that is deliberate. They find it
|
|
199
211
|
in Drafts. Opening it is what makes it recent *for them*.
|
|
200
212
|
|
|
@@ -5,7 +5,7 @@ description: >-
|
|
|
5
5
|
Put a self-contained interactive HTML page into a Clearly workspace, the way a
|
|
6
6
|
Claude artifact works: an explainer, an animation, a small tool, a calculator, a
|
|
7
7
|
chart that runs. Covers which of the three HTML homes runs scripts (a SITE does,
|
|
8
|
-
a canvas ARTIFACT card does
|
|
8
|
+
a canvas ARTIFACT card does and is clicked only in Interact mode, a NOTE never does), the exact
|
|
9
9
|
CLI calls, the sandbox limits, updating, filing in a project, how a person opens
|
|
10
10
|
it, and QA. Use when asked to publish, host or add an HTML page, demo or animation
|
|
11
11
|
to the workspace. Triggers: "make an artifact", "publish this HTML", "add this page /
|
|
@@ -23,13 +23,14 @@ production hold different workspaces.
|
|
|
23
23
|
| Home | Make it with | Scripts and CSS animation | Clickable | Search, history | Opens at |
|
|
24
24
|
|---|---|---|---|---|---|
|
|
25
25
|
| **Site** | `compose-upload-init` → `file-teleport-inline` → `compose-upload-finalize` | run | yes | not in `document-search`; no version history; a re-upload overwrites | Recents or project card → `/compositions/detail?id=<id>` |
|
|
26
|
-
| **Canvas artifact** | `canvas-act` `canvas.add-artifact` | run | **
|
|
26
|
+
| **Canvas artifact** | `canvas-act` `canvas.add-artifact` | run | **only in Interact mode**: by default the card takes no pointer events and pans like a picture; the person selects it and presses **Interact** (Done or Esc hands input back to the canvas) | no; it is part of the canvas and is deleted with it | the canvas, `/c/<compositionId>` |
|
|
27
27
|
| **Note** | `document-create {html}`; `page-write` (in-app chat only) | **never**: it opens in the document editor, which drops `<script>`, `<style>` and inline `<svg>` | n/a | yes, both | `/documents/<id>` |
|
|
28
28
|
|
|
29
29
|
- An interactive page someone opens, clicks through or comes back to is a **site**. This is the
|
|
30
30
|
Claude-artifact equivalent and the default.
|
|
31
31
|
- A page that only needs to be **seen** beside other cards (a looping animation, a live chart in a
|
|
32
|
-
review board) is a **canvas artifact**.
|
|
32
|
+
review board) is a **canvas artifact**. Its buttons respond only after the person selects the
|
|
33
|
+
card and presses Interact, so never make a control the only way to see the content.
|
|
33
34
|
- Prose the team will read, search and edit is a **note**. Never put a script in a note: it
|
|
34
35
|
is stored, and then nothing ever runs it. Editing the note in the app rewrites its body.
|
|
35
36
|
|
|
@@ -99,8 +100,8 @@ artifact card. It copies the entry HTML at that moment: later re-uploads do not
|
|
|
99
100
|
and the site's other files do not load.
|
|
100
101
|
|
|
101
102
|
**Connector only (MCP):** no tool makes a site or an artifact. `clearly_file_upload` cannot choose
|
|
102
|
-
a site path, and `clearly_create` stores a document body as markdown. `
|
|
103
|
-
{target, kind:"site", id}` can place a site that already exists. Otherwise, tell the person the
|
|
103
|
+
a site path, and `clearly_create` stores a document body as markdown. `clearly_canvas_add
|
|
104
|
+
{target, ops:[{op:"place", kind:"site", id}]}` can place a site that already exists. Otherwise, tell the person the
|
|
104
105
|
running page needs the CLI.
|
|
105
106
|
|
|
106
107
|
## 4. Update, file, link
|