@clearlysh/beehaven 0.8.34 → 0.8.36
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-sessions.js +9 -6
- package/dist/agents-md-text.js +79 -43
- package/dist/browser-login.js +94 -0
- package/dist/build-channel.js +2 -2
- package/dist/cli.js +96 -50
- package/dist/commands/agent-instructions.js +38 -30
- package/dist/commands/agents-md.js +104 -12
- package/dist/commands/help.js +49 -14
- package/dist/commands/mcp-token.js +113 -0
- package/dist/reconnect-backoff.js +78 -0
- package/dist/relay.js +33 -22
- package/package.json +1 -1
- package/scripts/sync-skills.mjs +1 -0
- package/skills/clearly-artifacts/SKILL.md +143 -0
- package/skills/index.json +7 -0
package/dist/agent-sessions.js
CHANGED
|
@@ -14,6 +14,7 @@ import { writeFileSync, readFileSync, renameSync, chmodSync, mkdirSync, existsSy
|
|
|
14
14
|
import { createHash } from 'node:crypto';
|
|
15
15
|
import { join } from 'node:path';
|
|
16
16
|
import { homedir } from 'node:os';
|
|
17
|
+
import { ReconnectBackoff } from './reconnect-backoff.js';
|
|
17
18
|
const CONFIG_DIR = join(homedir(), '.beehaven');
|
|
18
19
|
function scopedPresenceFile(scope) {
|
|
19
20
|
const digest = createHash('sha256').update(String(scope || 'local')).digest('hex').slice(0, 24);
|
|
@@ -31,8 +32,6 @@ export class AgentSessions {
|
|
|
31
32
|
presenceBroadcast = null;
|
|
32
33
|
/** A desktop identity is presence, not a permanent login badge. */
|
|
33
34
|
static IDLE_TIMEOUT_MS = 30 * 60 * 1000;
|
|
34
|
-
static RECONNECT_MIN_MS = 1_000;
|
|
35
|
-
static RECONNECT_MAX_MS = 30_000;
|
|
36
35
|
/** @param wsBase returns the relay ws base, e.g. wss://…/ (no trailing path) */
|
|
37
36
|
constructor(wsBase, presenceScope = 'local') {
|
|
38
37
|
this.wsBase = wsBase;
|
|
@@ -188,8 +187,7 @@ export class AgentSessions {
|
|
|
188
187
|
scheduleReconnect(agentId, spec) {
|
|
189
188
|
if (this.specs.get(agentId) !== spec || spec.reconnectTimer)
|
|
190
189
|
return;
|
|
191
|
-
const delay = spec.
|
|
192
|
-
spec.reconnectDelay = Math.min(spec.reconnectDelay * 2, AgentSessions.RECONNECT_MAX_MS);
|
|
190
|
+
const delay = spec.backoff.next();
|
|
193
191
|
spec.reconnectTimer = setTimeout(() => {
|
|
194
192
|
spec.reconnectTimer = null;
|
|
195
193
|
this.openSocket(agentId, spec);
|
|
@@ -213,6 +211,7 @@ export class AgentSessions {
|
|
|
213
211
|
const url = `${base}/my/workspace/ws?token=${encodeURIComponent(spec.token)}&clientType=agent&client=agent&deviceId=agent-${encodeURIComponent(agentId)}`;
|
|
214
212
|
let ws;
|
|
215
213
|
let opened = false;
|
|
214
|
+
let openedAt = null;
|
|
216
215
|
let settled = false;
|
|
217
216
|
let timer = null;
|
|
218
217
|
const settleFirst = (result) => {
|
|
@@ -253,7 +252,10 @@ export class AgentSessions {
|
|
|
253
252
|
// Reconnecting transport is not agent activity. A new socket stays inactive until a
|
|
254
253
|
// verified call touches the identity; a persisted interaction keeps its existing clock.
|
|
255
254
|
this.scheduleIdleExpiry(agentId);
|
|
256
|
-
|
|
255
|
+
// ⚠ PPO-532: opening is not recovering — this reset-on-open made a workspace that drops
|
|
256
|
+
// every new socket while hydrating (1006) see a reconnect every ~1 s, in step with the
|
|
257
|
+
// daemon's own socket. The backoff now resets only after the socket stays open 60 s.
|
|
258
|
+
openedAt = spec.backoff.opened();
|
|
257
259
|
this.syncPersistedInteractions();
|
|
258
260
|
this.syncActiveSessionsFile();
|
|
259
261
|
settleFirst({ ok: true });
|
|
@@ -290,6 +292,7 @@ export class AgentSessions {
|
|
|
290
292
|
return;
|
|
291
293
|
}
|
|
292
294
|
this.syncActiveSessionsFile();
|
|
295
|
+
spec.backoff.closed(openedAt);
|
|
293
296
|
this.scheduleReconnect(agentId, spec);
|
|
294
297
|
};
|
|
295
298
|
}
|
|
@@ -300,7 +303,7 @@ export class AgentSessions {
|
|
|
300
303
|
const spec = {
|
|
301
304
|
token,
|
|
302
305
|
reconnectTimer: null,
|
|
303
|
-
|
|
306
|
+
backoff: new ReconnectBackoff(),
|
|
304
307
|
};
|
|
305
308
|
this.specs.set(agentId, spec);
|
|
306
309
|
return new Promise((resolve) => this.openSocket(agentId, spec, resolve));
|
package/dist/agents-md-text.js
CHANGED
|
@@ -1,64 +1,100 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* agents-md-text — the Clearly block for a PROJECT's AGENTS.md, as `beehaven agents-md` writes it
|
|
3
|
-
* (CAN2-66).
|
|
3
|
+
* (CAN2-66, AWO-401).
|
|
4
4
|
*
|
|
5
5
|
* ⚠⚠ A COPY, PINNED. The source of truth is the monorepo's `lib/connections/agentsMd.ts`, which the
|
|
6
|
-
* Integrations page, each agent's details and the MCP `clearly-agents-md`
|
|
7
|
-
* package cannot import it (it ships on its own), so the text is repeated here
|
|
8
|
-
* `lib/__tests__/agentsMdCliParity.test.mjs` compares the two OUTPUTS, name
|
|
9
|
-
* Change both or the test fails — a stale copy does not render wrong, it
|
|
6
|
+
* Integrations page, each agent's details, the /cli and /mcp guides and the MCP `clearly-agents-md`
|
|
7
|
+
* prompt render. This package cannot import it (it ships on its own), so the text is repeated here
|
|
8
|
+
* and the monorepo's `lib/__tests__/agentsMdCliParity.test.mjs` compares the two OUTPUTS, name
|
|
9
|
+
* argument included. Change both or the test fails — a stale copy does not render wrong, it
|
|
10
|
+
* instructs wrong.
|
|
11
|
+
*
|
|
12
|
+
* ⚠ ONE TRACKED BLOCK FOR MCP AND THE CLI (founder 2026-10-28): every task is tracked in Clearly,
|
|
13
|
+
* and each step names the MCP tool and the CLI command. The per-route blocks before it are removed
|
|
14
|
+
* when this one is written (`LEGACY_BLOCK_MARKERS`).
|
|
10
15
|
*
|
|
11
16
|
* ⚠ NO IMPORTS, so that test can load this file directly.
|
|
12
17
|
*/
|
|
13
|
-
export const
|
|
14
|
-
export const
|
|
15
|
-
|
|
16
|
-
export const
|
|
18
|
+
export const AGENTS_MD_START = '<!-- clearly:start -->';
|
|
19
|
+
export const AGENTS_MD_END = '<!-- clearly:end -->';
|
|
20
|
+
/** The per-route markers used before AWO-401. Writers remove these blocks; nothing writes them. */
|
|
21
|
+
export const LEGACY_BLOCK_MARKERS = [
|
|
22
|
+
['<!-- clearly:cli:start -->', '<!-- clearly:cli:end -->'],
|
|
23
|
+
['<!-- clearly:mcp:start -->', '<!-- clearly:mcp:end -->'],
|
|
24
|
+
];
|
|
25
|
+
/**
|
|
26
|
+
* The opt-out (AWO-403): a project whose AGENTS.md or CLAUDE.md carries this exact line is not tracked in Clearly.
|
|
27
|
+
* The global block (agentsGlobal.ts) and the MCP standing instructions name it, so the opt-out is a literal an agent
|
|
28
|
+
* matches, not a sentence it interprets. `beehaven agents-md --not-tracked` writes it between Clearly's markers.
|
|
29
|
+
*/
|
|
30
|
+
export const AGENTS_MD_OPT_OUT_LINE = 'Clearly: not tracked';
|
|
31
|
+
/** The opted-out project's block: the line, inside the same markers, so re-running the setup turns tracking back on. */
|
|
32
|
+
export function agentsMdOptOutBlock() {
|
|
33
|
+
return [
|
|
34
|
+
AGENTS_MD_START,
|
|
35
|
+
'<!-- managed-by: clearly. Run `beehaven agents-md` to track this project in Clearly again. -->',
|
|
36
|
+
'',
|
|
37
|
+
AGENTS_MD_OPT_OUT_LINE,
|
|
38
|
+
AGENTS_MD_END,
|
|
39
|
+
].join('\n');
|
|
40
|
+
}
|
|
41
|
+
/** How Claude Code runs the MCP server's `clearly-agents-md` prompt, which writes the block. */
|
|
42
|
+
export const MCP_AGENTS_MD_SLASH = '/mcp__clearly__clearly-agents-md';
|
|
17
43
|
/** What goes in CLAUDE.md so Claude Code reads the AGENTS.md block too. */
|
|
18
44
|
export const CLAUDE_MD_IMPORT = '@AGENTS.md';
|
|
19
|
-
/**
|
|
20
|
-
export
|
|
45
|
+
/** Where the block goes, said once for every surface. */
|
|
46
|
+
export const AGENTS_MD_HOW = 'Every agent that opens the repository reads AGENTS.md, so the block makes each session track its work in Clearly. Claude Code reads CLAUDE.md — give it one line.';
|
|
47
|
+
/** The MCP tools the block names. Each must be on the server's `tools/list` (a Worker test pins it). */
|
|
48
|
+
export const MCP_BLOCK_TOOLS = ['clearly_catalog', 'clearly_read', 'clearly_grep', 'clearly_create', 'clearly_edit', 'clearly_comment'];
|
|
49
|
+
/**
|
|
50
|
+
* The block. `agentName` personalises the CLI sign-in line for one agent (its details view);
|
|
51
|
+
* without it the agent is told to use its own name.
|
|
52
|
+
*/
|
|
53
|
+
export function agentsMdBlock(agentName) {
|
|
21
54
|
const name = (agentName ?? '').trim();
|
|
22
55
|
const login = name
|
|
23
|
-
? `\`beehaven agent login ${name} --label "<
|
|
24
|
-
: '`beehaven agent login <name> --label "<
|
|
56
|
+
? `\`beehaven agent login ${name} --label "<the task>"\` — you are ${name} in this workspace —`
|
|
57
|
+
: '`beehaven agent login <your-name> --label "<the task>"` (claude, codex, gemini…)';
|
|
25
58
|
return [
|
|
26
|
-
|
|
27
|
-
'<!-- managed-by: clearly. Run `beehaven agents-md` to update; edit outside this block. -->',
|
|
59
|
+
AGENTS_MD_START,
|
|
60
|
+
'<!-- managed-by: clearly. Run `beehaven agents-md` or the `clearly-agents-md` MCP prompt to update; edit outside this block. -->',
|
|
61
|
+
'',
|
|
62
|
+
'## Clearly workspace: track every task',
|
|
28
63
|
'',
|
|
29
|
-
'
|
|
64
|
+
"This project's plans, tickets, documents and designs live in Clearly. Track every task there,",
|
|
65
|
+
'even when the person does not ask. Use whichever route this session has: the `clearly_*` MCP tools,',
|
|
66
|
+
"or the `beehaven` CLI (`beehaven actions` lists actions; `beehaven call <action> '<json>'` runs one).",
|
|
30
67
|
'',
|
|
31
|
-
'
|
|
32
|
-
'for anything the team should see, not files in this repo.',
|
|
68
|
+
'### Start of every session',
|
|
33
69
|
'',
|
|
34
|
-
`-
|
|
70
|
+
`- CLI: sign in first with ${login} and read the brief it prints.`,
|
|
35
71
|
' If it says the daemon is not running: `beehaven start --headless`.',
|
|
36
|
-
'
|
|
37
|
-
'
|
|
38
|
-
'
|
|
39
|
-
'
|
|
40
|
-
'
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
'
|
|
72
|
+
' MCP: you are already signed in as the agent chosen when Clearly was connected.',
|
|
73
|
+
"- Read this project's open tickets and its latest handoff before starting (`clearly_grep` /",
|
|
74
|
+
' `clearly_read`, or the brief).',
|
|
75
|
+
'',
|
|
76
|
+
'### Every task has a ticket',
|
|
77
|
+
'',
|
|
78
|
+
'1. Search first (`clearly_grep`, `clearly_catalog`, or `beehaven call ticket-list`). Reuse the project,',
|
|
79
|
+
' board and ticket that already own the work; create a board only if the project has none.',
|
|
80
|
+
'2. Create the ticket before changing code or files (`clearly_create` with `type: "ticket"` and',
|
|
81
|
+
' `fields.boardId`, or `beehaven call ticket-create`). Say in its body what done means.',
|
|
82
|
+
'3. Move it to in-progress when work begins (`clearly_edit`, or `beehaven call ticket-update`).',
|
|
83
|
+
'4. File what you make (documents, designs, notes) into the project and link it from the ticket.',
|
|
84
|
+
' Record decisions, open questions and progress as comments (`clearly_comment`).',
|
|
85
|
+
'5. Mark it done only after verifying the result, with the resolution and any remaining risk.',
|
|
86
|
+
'',
|
|
87
|
+
'Questions and reads need no ticket.',
|
|
49
88
|
'',
|
|
50
|
-
'
|
|
89
|
+
'### End of every session',
|
|
51
90
|
'',
|
|
52
|
-
'
|
|
53
|
-
'
|
|
91
|
+
'- Leave a handoff: what you did and what is next. CLI: `beehaven agent handoff "<done · next>"`.',
|
|
92
|
+
' MCP: a comment on the work ticket.',
|
|
54
93
|
'',
|
|
55
|
-
'
|
|
56
|
-
'
|
|
57
|
-
'
|
|
58
|
-
'
|
|
59
|
-
|
|
60
|
-
'- Search before you write, and write decisions back so the next session inherits them.',
|
|
61
|
-
'- If a job is not covered by a listed tool, say so rather than guessing an action name.',
|
|
62
|
-
MCP_BLOCK_END,
|
|
94
|
+
'If this session has neither the `clearly_*` tools nor `beehaven`, stop and ask the person to connect',
|
|
95
|
+
'Clearly (clearly.sh/docs/mcp or clearly.sh/docs/cli). Do not skip the tracking. In CI or a cloud',
|
|
96
|
+
'agent, where nobody can sign in, connect over MCP with an agent token from the agent\'s details in',
|
|
97
|
+
'Clearly, kept as a secret.',
|
|
98
|
+
AGENTS_MD_END,
|
|
63
99
|
].join('\n');
|
|
64
100
|
}
|
package/dist/browser-login.js
CHANGED
|
@@ -291,3 +291,97 @@ export async function browserLogin() {
|
|
|
291
291
|
persistBrowserLogin(captured);
|
|
292
292
|
return captured;
|
|
293
293
|
}
|
|
294
|
+
/** The relay's HTTPS origin for the configured env (prod when unconfigured). */
|
|
295
|
+
export function relayHttpFromConfig(cfg = readConfig(), env = process.env) {
|
|
296
|
+
const override = env.BEEHAVEN_RELAY_HTTP_URL;
|
|
297
|
+
if (override)
|
|
298
|
+
return override.replace(/\/+$/, '');
|
|
299
|
+
return resolveRelaySelection(cfg).url.replace(/^wss:\/\//, 'https://').replace(/^ws:\/\//, 'http://').replace(/\/+$/, '');
|
|
300
|
+
}
|
|
301
|
+
/**
|
|
302
|
+
* True where a browser on this machine is unlikely: an SSH session, CI, or Linux with no display.
|
|
303
|
+
* `beehaven login --browser` overrides it; `--device-code` forces the device flow anywhere.
|
|
304
|
+
*/
|
|
305
|
+
export function preferDeviceLogin(env = process.env, platform = process.platform) {
|
|
306
|
+
if (env.SSH_CONNECTION || env.SSH_TTY || env.CI)
|
|
307
|
+
return true;
|
|
308
|
+
return platform === 'linux' && !env.DISPLAY && !env.WAYLAND_DISPLAY;
|
|
309
|
+
}
|
|
310
|
+
/** The approval page, optionally with the code filled in. */
|
|
311
|
+
export function deviceAuthUrl(authOrigin, userCode) {
|
|
312
|
+
return withUtm(`${authOrigin}/cli/device${userCode ? `?code=${encodeURIComponent(userCode)}` : ''}`);
|
|
313
|
+
}
|
|
314
|
+
/** Email and name from the ID token's payload, for the "Signed in as" line. Display only. */
|
|
315
|
+
function idTokenClaims(idToken) {
|
|
316
|
+
try {
|
|
317
|
+
const payload = JSON.parse(Buffer.from(idToken.split('.')[1] ?? '', 'base64url').toString('utf8'));
|
|
318
|
+
return { email: typeof payload.email === 'string' ? payload.email : undefined, name: typeof payload.name === 'string' ? payload.name : undefined };
|
|
319
|
+
}
|
|
320
|
+
catch {
|
|
321
|
+
return {};
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
export function captureDeviceLogin(opts) {
|
|
325
|
+
const { relayOrigin, authOrigin, client, fetchImpl = fetch, sleep = (ms) => new Promise((r) => setTimeout(r, ms)), now = Date.now, log = (line) => console.log(line), } = opts;
|
|
326
|
+
const post = async (path, body) => {
|
|
327
|
+
const res = await fetchImpl(`${relayOrigin}${path}`, {
|
|
328
|
+
method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body),
|
|
329
|
+
});
|
|
330
|
+
const json = await res.json().catch(() => ({}));
|
|
331
|
+
return { res, json };
|
|
332
|
+
};
|
|
333
|
+
return (async () => {
|
|
334
|
+
let start;
|
|
335
|
+
try {
|
|
336
|
+
const { res, json } = await post('/cli/device/start', { client });
|
|
337
|
+
if (!res.ok || !json?.ok)
|
|
338
|
+
throw new Error(json?.error === 'slow_down' ? 'Too many sign-in attempts from here. Wait a while and try again.' : (json?.error || `relay answered ${res.status}`));
|
|
339
|
+
start = json;
|
|
340
|
+
}
|
|
341
|
+
catch (err) {
|
|
342
|
+
throw new Error(`Couldn't start a device sign-in: ${err?.message ?? err}`);
|
|
343
|
+
}
|
|
344
|
+
log('');
|
|
345
|
+
log(` On any device with a browser, open ${color.cyan(deviceAuthUrl(authOrigin))}`);
|
|
346
|
+
log(` and enter this code: ${color.bold(start.userCode)}`);
|
|
347
|
+
log('');
|
|
348
|
+
log(color.dim(` Direct link: ${deviceAuthUrl(authOrigin, start.userCode)}`));
|
|
349
|
+
log(color.dim(` Waiting for approval (the code expires in ${Math.round(start.expiresIn / 60)} minutes)…`));
|
|
350
|
+
let interval = Math.max(1, start.interval) * 1000;
|
|
351
|
+
const deadline = now() + start.expiresIn * 1000;
|
|
352
|
+
while (now() < deadline) {
|
|
353
|
+
await sleep(interval);
|
|
354
|
+
let res;
|
|
355
|
+
let json;
|
|
356
|
+
try {
|
|
357
|
+
({ res, json } = await post('/cli/device/token', { deviceCode: start.deviceCode }));
|
|
358
|
+
}
|
|
359
|
+
catch {
|
|
360
|
+
continue; // a dropped poll is retried at the next tick
|
|
361
|
+
}
|
|
362
|
+
if (res.ok && json?.ok && json.idToken && json.refreshToken && json.userId) {
|
|
363
|
+
const { email, name } = idTokenClaims(json.idToken);
|
|
364
|
+
return { idToken: json.idToken, refreshToken: json.refreshToken, userId: json.userId, email, displayName: name };
|
|
365
|
+
}
|
|
366
|
+
switch (json?.error) {
|
|
367
|
+
case 'authorization_pending': continue;
|
|
368
|
+
case 'slow_down':
|
|
369
|
+
interval += 5000;
|
|
370
|
+
continue;
|
|
371
|
+
case 'access_denied': throw new Error('The sign-in was denied in the browser.');
|
|
372
|
+
case 'expired_token': throw new Error('The code expired. Run `beehaven login --device-code` again.');
|
|
373
|
+
default:
|
|
374
|
+
if (res.status >= 500)
|
|
375
|
+
throw new Error(json?.error || `relay answered ${res.status}`);
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
throw new Error('The code expired. Run `beehaven login --device-code` again.');
|
|
379
|
+
})();
|
|
380
|
+
}
|
|
381
|
+
/** Device-code sign-in end to end: start, show the code, poll, persist like the browser flow. */
|
|
382
|
+
export async function deviceLogin(opts = {}) {
|
|
383
|
+
const { authOrigin } = resolveAuthOrigin();
|
|
384
|
+
const captured = await captureDeviceLogin({ relayOrigin: relayHttpFromConfig(), authOrigin, client: opts.client });
|
|
385
|
+
persistBrowserLogin(captured);
|
|
386
|
+
return captured;
|
|
387
|
+
}
|
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 = '31746b91dfbf';
|
|
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-29T05:50Z';
|
|
59
59
|
/**
|
|
60
60
|
* The build identity as one short string, for `--version`.
|
|
61
61
|
*
|
package/dist/cli.js
CHANGED
|
@@ -284,6 +284,7 @@ ${DEV_ONLY_HELP ? ` PROJECT — build your own Durable Object backend (HIVE_DE
|
|
|
284
284
|
mcp status Which clients are configured
|
|
285
285
|
mcp remove Remove the entries + revoke the token
|
|
286
286
|
mcp-config [--name N] Print the block instead of writing it
|
|
287
|
+
mcp-config --token-only --expires <days> Print ONLY the token, for a secret store
|
|
287
288
|
|
|
288
289
|
Anywhere else — claude.ai, Claude Desktop, another machine — point the client
|
|
289
290
|
at the endpoint and let it log you in; nothing to install and no token to paste:
|
|
@@ -1340,7 +1341,7 @@ async function run() {
|
|
|
1340
1341
|
state: { type: 'string' },
|
|
1341
1342
|
goal: { type: 'string' },
|
|
1342
1343
|
remove: { type: 'boolean', default: false },
|
|
1343
|
-
// `agents-md`:
|
|
1344
|
+
// `agents-md`: `--mcp` is accepted for older instructions (one block since AWO-401); `--print` prints it.
|
|
1344
1345
|
mcp: { type: 'boolean', default: false },
|
|
1345
1346
|
print: { type: 'boolean', default: false },
|
|
1346
1347
|
/**
|
|
@@ -1417,6 +1418,10 @@ async function run() {
|
|
|
1417
1418
|
// (a live client) when none is connected; `--dev` → localhost.
|
|
1418
1419
|
launch: { type: 'boolean', default: false },
|
|
1419
1420
|
browser: { type: 'boolean', default: false },
|
|
1421
|
+
// `beehaven login --device-code` — sign in by approving a code on another device (AWO-402).
|
|
1422
|
+
'device-code': { type: 'boolean', default: false },
|
|
1423
|
+
// `beehaven agents-md --not-tracked` — write the opt-out line instead of the tracked block (AWO-403).
|
|
1424
|
+
'not-tracked': { type: 'boolean', default: false },
|
|
1420
1425
|
dev: { type: 'boolean', default: false },
|
|
1421
1426
|
// `beehaven canvas perceive --pixels --max 80`, `canvas verify --keep`
|
|
1422
1427
|
pixels: { type: 'boolean', default: false },
|
|
@@ -1461,6 +1466,8 @@ async function run() {
|
|
|
1461
1466
|
client: { type: 'string' },
|
|
1462
1467
|
/** `beehaven mcp install --rotate` — mint a new token instead of reusing the installed one. */
|
|
1463
1468
|
rotate: { type: 'boolean' },
|
|
1469
|
+
/** `beehaven mcp-config --token-only --expires <days>` — the raw token alone on stdout, for piping. */
|
|
1470
|
+
'token-only': { type: 'boolean' },
|
|
1464
1471
|
password: { type: 'string' },
|
|
1465
1472
|
email: { type: 'string' },
|
|
1466
1473
|
expires: { type: 'string' },
|
|
@@ -3033,10 +3040,19 @@ async function run() {
|
|
|
3033
3040
|
// own copy aimed at /auth/beehaven, a route the web app no longer serves.
|
|
3034
3041
|
// What follows the sign-in — restarting the daemon, teaching the agents —
|
|
3035
3042
|
// stays here, because the daemon fallback must not restart itself.
|
|
3043
|
+
// AWO-402 — the device-code flow for a machine with no browser of its own: forced with
|
|
3044
|
+
// `--device-code`, chosen automatically over SSH / CI / a display-less Linux, and never when
|
|
3045
|
+
// the person asked for `--browser`.
|
|
3036
3046
|
let captured;
|
|
3037
3047
|
try {
|
|
3038
|
-
const { browserLogin } = await import('./browser-login.js');
|
|
3039
|
-
|
|
3048
|
+
const { browserLogin, deviceLogin, preferDeviceLogin } = await import('./browser-login.js');
|
|
3049
|
+
const useDevice = Boolean(values['device-code']) || (!values.browser && preferDeviceLogin());
|
|
3050
|
+
if (useDevice && !values['device-code']) {
|
|
3051
|
+
console.log(color.dim(' No browser on this machine, so signing in with a code (`beehaven login --browser` to open one here).'));
|
|
3052
|
+
}
|
|
3053
|
+
captured = useDevice
|
|
3054
|
+
? await deviceLogin({ client: `beehaven ${getVersion()} (${process.platform})` })
|
|
3055
|
+
: await browserLogin();
|
|
3040
3056
|
}
|
|
3041
3057
|
catch (err) {
|
|
3042
3058
|
console.error(` ${color.red('✗')} ${err?.message ?? err}`);
|
|
@@ -3170,6 +3186,7 @@ async function run() {
|
|
|
3170
3186
|
mcp: !!values.mcp,
|
|
3171
3187
|
print: !!values.print,
|
|
3172
3188
|
remove: !!values.remove,
|
|
3189
|
+
notTracked: Boolean(values['not-tracked']),
|
|
3173
3190
|
});
|
|
3174
3191
|
break;
|
|
3175
3192
|
}
|
|
@@ -4073,10 +4090,82 @@ complete -F _beehaven_completions beehaven`);
|
|
|
4073
4090
|
const tgtAddress = tgt?.address ?? 'home';
|
|
4074
4091
|
const mcpEntry = entryNameFor(tgtAddress);
|
|
4075
4092
|
const wsId = isHomeAddress(tgtAddress) ? '' : tgtAddress.split('/').slice(1).join('/');
|
|
4076
|
-
const
|
|
4093
|
+
const defaultMcpLabel = `${mcpEntry} · ${new Date().toISOString().slice(0, 10)}`;
|
|
4077
4094
|
const { rpcOnce: _rpc } = await import('./ipc-client.js');
|
|
4078
4095
|
const { resolveActiveAgent } = await import('./agent-state.js');
|
|
4096
|
+
const { mcpMintInput, runMcpTokenOnly } = await import('./commands/mcp-token.js');
|
|
4079
4097
|
const activeMcpAgent = resolveActiveAgent(values);
|
|
4098
|
+
/**
|
|
4099
|
+
* ⚠⚠ DO NOT ECHO THE RAW ERROR — IT SENT PEOPLE TO THE WRONG FIX.
|
|
4100
|
+
*
|
|
4101
|
+
* Observed: this printed "Cannot connect to daemon. Run `beehaven start --headless`" in the
|
|
4102
|
+
* same second that `beehaven status` reported "Daemon: running". The daemon was up and the
|
|
4103
|
+
* RELAY had dropped. Following the advice runs into the single-instance guard, which
|
|
4104
|
+
* refuses — so the message costs a cycle and teaches the wrong model of the failure.
|
|
4105
|
+
*
|
|
4106
|
+
* The CLI already distinguishes these three states carefully (see `status`), precisely
|
|
4107
|
+
* because the fixes are mutually exclusive: a restart will NOT refresh an expired token,
|
|
4108
|
+
* and a re-login will NOT revive a dropped socket. This path bypassed all of it by
|
|
4109
|
+
* forwarding whatever string the transport happened to raise.
|
|
4110
|
+
*
|
|
4111
|
+
* Re-probe and say what is actually true. Stderr only, and it returns the exit code, so the
|
|
4112
|
+
* `--token-only` path can use it without touching stdout.
|
|
4113
|
+
*/
|
|
4114
|
+
const explainMintFailure = async (err) => {
|
|
4115
|
+
const { isDaemonRunning } = await import('./daemon-client.js');
|
|
4116
|
+
let up = false;
|
|
4117
|
+
try {
|
|
4118
|
+
up = await isDaemonRunning();
|
|
4119
|
+
}
|
|
4120
|
+
catch {
|
|
4121
|
+
up = false;
|
|
4122
|
+
}
|
|
4123
|
+
let expired = false;
|
|
4124
|
+
try {
|
|
4125
|
+
const cfg = JSON.parse(readFileSync(join(homedir(), '.beehaven', 'config.json'), 'utf8'));
|
|
4126
|
+
const payload = JSON.parse(Buffer.from(String(cfg.token).split('.')[1], 'base64').toString());
|
|
4127
|
+
expired = !payload.exp || Date.now() / 1000 > payload.exp - 60;
|
|
4128
|
+
}
|
|
4129
|
+
catch {
|
|
4130
|
+
expired = true;
|
|
4131
|
+
}
|
|
4132
|
+
console.error(` ${color.red('✗')} Could not mint a token.`);
|
|
4133
|
+
console.error(!up
|
|
4134
|
+
? ' The daemon is not running — `beehaven start --headless`'
|
|
4135
|
+
: expired
|
|
4136
|
+
? ' The daemon is up but your token has EXPIRED — `beehaven login` (a restart will not refresh it)'
|
|
4137
|
+
: ' The daemon is up and your token is valid; the relay socket dropped — `beehaven restart`');
|
|
4138
|
+
console.error(color.dim(` (${err?.message ?? err})`));
|
|
4139
|
+
return 1;
|
|
4140
|
+
};
|
|
4141
|
+
/**
|
|
4142
|
+
* ⚠⚠ `--token-only`: THE RAW TOKEN ON STDOUT AND NOTHING ELSE, for
|
|
4143
|
+
* `… | gh secret set CLEARLY_MCP_TOKEN`. The rules (stderr for every message, `--expires`
|
|
4144
|
+
* required, the caller's own agent) live in commands/mcp-token.ts, where the tests call them.
|
|
4145
|
+
* It branches off HERE, before the reuse check (print never reuses) and before anything below
|
|
4146
|
+
* writes to stdout.
|
|
4147
|
+
*/
|
|
4148
|
+
if (values['token-only']) {
|
|
4149
|
+
process.exitCode = await runMcpTokenOnly({
|
|
4150
|
+
sub: mcpSub,
|
|
4151
|
+
flags: values,
|
|
4152
|
+
defaultLabel: defaultMcpLabel,
|
|
4153
|
+
agentId: activeMcpAgent?.agentId,
|
|
4154
|
+
mint: (input) => _rpc('create-mcp-token', input, 30_000),
|
|
4155
|
+
out: (s) => { process.stdout.write(s); },
|
|
4156
|
+
err: (s) => { process.stderr.write(s); },
|
|
4157
|
+
stdoutIsTTY: Boolean(process.stdout.isTTY),
|
|
4158
|
+
onMintError: explainMintFailure,
|
|
4159
|
+
});
|
|
4160
|
+
break;
|
|
4161
|
+
}
|
|
4162
|
+
// `--label` (else `--name`), `--expires <days>` and the caller's agent, for every mint below.
|
|
4163
|
+
const mintInput = mcpMintInput(values, { label: defaultMcpLabel, agentId: activeMcpAgent?.agentId });
|
|
4164
|
+
if ('error' in mintInput) {
|
|
4165
|
+
console.error(` ${color.red('✗')} ${mintInput.error}`);
|
|
4166
|
+
process.exit(2);
|
|
4167
|
+
}
|
|
4168
|
+
const label = mintInput.input.label;
|
|
4080
4169
|
let res;
|
|
4081
4170
|
/**
|
|
4082
4171
|
* ⚠⚠ REUSE THE TOKEN ALREADY INSTALLED RATHER THAN MINTING ANOTHER. Minting
|
|
@@ -4121,54 +4210,11 @@ complete -F _beehaven_completions beehaven`);
|
|
|
4121
4210
|
}
|
|
4122
4211
|
}
|
|
4123
4212
|
try {
|
|
4124
|
-
if (!res)
|
|
4125
|
-
res = await _rpc('create-mcp-token',
|
|
4126
|
-
label,
|
|
4127
|
-
...(activeMcpAgent?.agentId ? { agentId: activeMcpAgent.agentId } : {}),
|
|
4128
|
-
}, 30_000);
|
|
4129
|
-
}
|
|
4213
|
+
if (!res)
|
|
4214
|
+
res = await _rpc('create-mcp-token', mintInput.input, 30_000);
|
|
4130
4215
|
}
|
|
4131
4216
|
catch (err) {
|
|
4132
|
-
|
|
4133
|
-
* ⚠⚠ DO NOT ECHO THE RAW ERROR — IT SENT PEOPLE TO THE WRONG FIX.
|
|
4134
|
-
*
|
|
4135
|
-
* Observed: this printed "Cannot connect to daemon. Run `beehaven start --headless`" in the
|
|
4136
|
-
* same second that `beehaven status` reported "Daemon: running". The daemon was up and the
|
|
4137
|
-
* RELAY had dropped. Following the advice runs into the single-instance guard, which
|
|
4138
|
-
* refuses — so the message costs a cycle and teaches the wrong model of the failure.
|
|
4139
|
-
*
|
|
4140
|
-
* The CLI already distinguishes these three states carefully (see `status`), precisely
|
|
4141
|
-
* because the fixes are mutually exclusive: a restart will NOT refresh an expired token,
|
|
4142
|
-
* and a re-login will NOT revive a dropped socket. This path bypassed all of it by
|
|
4143
|
-
* forwarding whatever string the transport happened to raise.
|
|
4144
|
-
*
|
|
4145
|
-
* Re-probe and say what is actually true.
|
|
4146
|
-
*/
|
|
4147
|
-
const { isDaemonRunning } = await import('./daemon-client.js');
|
|
4148
|
-
let up = false;
|
|
4149
|
-
try {
|
|
4150
|
-
up = await isDaemonRunning();
|
|
4151
|
-
}
|
|
4152
|
-
catch {
|
|
4153
|
-
up = false;
|
|
4154
|
-
}
|
|
4155
|
-
let expired = false;
|
|
4156
|
-
try {
|
|
4157
|
-
const cfg = JSON.parse(readFileSync(join(homedir(), '.beehaven', 'config.json'), 'utf8'));
|
|
4158
|
-
const payload = JSON.parse(Buffer.from(String(cfg.token).split('.')[1], 'base64').toString());
|
|
4159
|
-
expired = !payload.exp || Date.now() / 1000 > payload.exp - 60;
|
|
4160
|
-
}
|
|
4161
|
-
catch {
|
|
4162
|
-
expired = true;
|
|
4163
|
-
}
|
|
4164
|
-
console.error(` ${color.red('✗')} Could not mint a token.`);
|
|
4165
|
-
console.error(!up
|
|
4166
|
-
? ' The daemon is not running — `beehaven start --headless`'
|
|
4167
|
-
: expired
|
|
4168
|
-
? ' The daemon is up but your token has EXPIRED — `beehaven login` (a restart will not refresh it)'
|
|
4169
|
-
: ' The daemon is up and your token is valid; the relay socket dropped — `beehaven restart`');
|
|
4170
|
-
console.error(color.dim(` (${err?.message ?? err})`));
|
|
4171
|
-
process.exit(1);
|
|
4217
|
+
process.exit(await explainMintFailure(err));
|
|
4172
4218
|
}
|
|
4173
4219
|
if (!res?.key) {
|
|
4174
4220
|
console.error(` ${color.red('✗')} ${res?.error ?? 'No token returned.'}`);
|
|
@@ -14,9 +14,12 @@
|
|
|
14
14
|
* means both converge and either uninstaller removes it. `agentInstructionParity.test.mjs` in the
|
|
15
15
|
* root repo pins the two copies together.
|
|
16
16
|
*
|
|
17
|
-
* ⚠
|
|
18
|
-
*
|
|
19
|
-
*
|
|
17
|
+
* ⚠ ONE TRACKED BLOCK FOR EVERY AGENT, MCP OR CLI (founder 2026-10-28, AWO-401 follow-up: anyone
|
|
18
|
+
* using Clearly gets the mandatory block). It is short (~15 lines) because `~/.claude/CLAUDE.md` is
|
|
19
|
+
* prepended to every Claude Code session in every repository; the exact verbs stay in the skills.
|
|
20
|
+
* It has an escape hatch — a project's AGENTS.md or the person can say a project is not tracked —
|
|
21
|
+
* and a project's own Clearly block takes precedence. The web copy for pasting (Cursor's user
|
|
22
|
+
* rules) is the monorepo's lib/connections/agentsGlobal.ts, pinned to this one by a test.
|
|
20
23
|
*
|
|
21
24
|
* ⚠ ONLY OUR BLOCK IS EVER TOUCHED. Text around it is preserved byte for byte; a file with no
|
|
22
25
|
* block gets one appended; removal deletes the block and nothing else.
|
|
@@ -41,34 +44,24 @@ export function instructionBlock() {
|
|
|
41
44
|
BLOCK_START,
|
|
42
45
|
`<!-- ${MANAGED_MARKER}. Edits inside this block are overwritten on app launch; edit outside it. -->`,
|
|
43
46
|
'',
|
|
44
|
-
'## Clearly
|
|
47
|
+
'## Clearly workspace: track your work there',
|
|
45
48
|
'',
|
|
46
|
-
'
|
|
47
|
-
'
|
|
49
|
+
'This person keeps their plans, tickets, documents and designs in Clearly (clearly.sh). Track every',
|
|
50
|
+
'task there, even unasked and in any project, unless the project\'s AGENTS.md or CLAUDE.md has the line',
|
|
51
|
+
'`Clearly: not tracked`, or the person says not to track it. A project\'s own Clearly block in AGENTS.md',
|
|
52
|
+
'takes precedence over this one.',
|
|
48
53
|
'',
|
|
49
|
-
'
|
|
50
|
-
'refuses. `login` MINTS the name if it does not exist yet, and binds this terminal:',
|
|
54
|
+
'Use whichever route this session has: the `clearly_*` MCP tools, or the `beehaven` CLI.',
|
|
51
55
|
'',
|
|
52
|
-
'
|
|
53
|
-
'
|
|
54
|
-
'
|
|
55
|
-
'
|
|
56
|
-
'
|
|
57
|
-
'
|
|
56
|
+
'- Start. CLI: `beehaven agent login <your-name> --label "<the task>"` (claude, codex, gemini…), then',
|
|
57
|
+
' read the brief it prints; if the daemon is not running, `beehaven start --headless`. MCP: you are',
|
|
58
|
+
' already signed in.',
|
|
59
|
+
'- Each task: search first, then create a ticket before changing code or files and move it to',
|
|
60
|
+
' in-progress. File what you make into the project, record decisions as comments, and mark it done',
|
|
61
|
+
' only after verifying. Questions and reads need no ticket.',
|
|
62
|
+
'- End: a handoff, `beehaven agent handoff "<done · next>"` on the CLI or a comment on the work ticket.',
|
|
58
63
|
'',
|
|
59
|
-
'
|
|
60
|
-
'',
|
|
61
|
-
'- `~/.<agent>/skills/clearly-agent/SKILL.md` — **read this first**: identity, sessions, your brief',
|
|
62
|
-
'- `~/.<agent>/skills/beehaven-cli/SKILL.md` — connect / call / sql / logs, the daemon',
|
|
63
|
-
'- `~/.<agent>/skills/clearly-workspace/SKILL.md` — audit what a workspace holds and who made it',
|
|
64
|
-
'- `~/.<agent>/skills/clearly-canvas/SKILL.md` — build + perceive a canvas, comm blocks, code diffs',
|
|
65
|
-
'- `~/.<agent>/skills/design-craft/SKILL.md` — **load before any visual work**: grid, type, colour, the studio pass',
|
|
66
|
-
'- `~/.<agent>/skills/layout-systems/SKILL.md` — grids and systems across many artboards',
|
|
67
|
-
'- `~/.<agent>/skills/pair-on-canvas/SKILL.md` — drive a coding task from a spec on the canvas',
|
|
68
|
-
'- `~/.<agent>/skills/codebase-map/SKILL.md` — draw a repo’s architecture on the canvas',
|
|
69
|
-
'',
|
|
70
|
-
'If a command reports the daemon is unreachable, `beehaven login` re-auths and',
|
|
71
|
-
'`beehaven start --headless` starts a daemon when the Mac app is not running.',
|
|
64
|
+
'Detail is in the `clearly-agent` skill (`beehaven skills install` puts the skills in place).',
|
|
72
65
|
BLOCK_END,
|
|
73
66
|
].join('\n');
|
|
74
67
|
}
|
|
@@ -78,9 +71,24 @@ export function claudeBlock() {
|
|
|
78
71
|
BLOCK_START,
|
|
79
72
|
`<!-- ${MANAGED_MARKER}. Edits inside this block are overwritten on app launch; edit outside it. -->`,
|
|
80
73
|
'',
|
|
81
|
-
'
|
|
82
|
-
'
|
|
83
|
-
'
|
|
74
|
+
'## Clearly workspace: track your work there',
|
|
75
|
+
'',
|
|
76
|
+
'This person keeps their plans, tickets, documents and designs in Clearly (clearly.sh). Track every',
|
|
77
|
+
'task there, even unasked and in any project, unless the project\'s AGENTS.md or CLAUDE.md has the line',
|
|
78
|
+
'`Clearly: not tracked`, or the person says not to track it. A project\'s own Clearly block in AGENTS.md',
|
|
79
|
+
'takes precedence over this one.',
|
|
80
|
+
'',
|
|
81
|
+
'Use whichever route this session has: the `clearly_*` MCP tools, or the `beehaven` CLI.',
|
|
82
|
+
'',
|
|
83
|
+
'- Start. CLI: `beehaven agent login <your-name> --label "<the task>"` (claude, codex, gemini…), then',
|
|
84
|
+
' read the brief it prints; if the daemon is not running, `beehaven start --headless`. MCP: you are',
|
|
85
|
+
' already signed in.',
|
|
86
|
+
'- Each task: search first, then create a ticket before changing code or files and move it to',
|
|
87
|
+
' in-progress. File what you make into the project, record decisions as comments, and mark it done',
|
|
88
|
+
' only after verifying. Questions and reads need no ticket.',
|
|
89
|
+
'- End: a handoff, `beehaven agent handoff "<done · next>"` on the CLI or a comment on the work ticket.',
|
|
90
|
+
'',
|
|
91
|
+
'Detail is in the `clearly-agent` skill (`beehaven skills install` puts the skills in place).',
|
|
84
92
|
BLOCK_END,
|
|
85
93
|
].join('\n');
|
|
86
94
|
}
|