beehaven 0.8.29 → 0.8.34
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-attribution.js +20 -2
- package/dist/agent-scope.js +55 -10
- package/dist/agent-state.js +44 -0
- package/dist/agents-md-text.js +64 -0
- package/dist/browser-login.js +14 -4
- package/dist/build-channel.js +2 -2
- package/dist/claude-bridge.js +12 -9
- package/dist/clearly.js +4 -3
- package/dist/cli-state.js +45 -3
- package/dist/cli.js +82 -53
- package/dist/commands/agent-skills.js +12 -1
- package/dist/commands/agent.js +235 -28
- package/dist/commands/agents-md.js +118 -0
- package/dist/commands/daemon-cmd.js +2 -0
- package/dist/commands/help.js +21 -0
- package/dist/commands/self-update.js +32 -5
- package/dist/commands/workspace.js +22 -6
- package/dist/daemon-client.js +10 -6
- package/dist/daemon-entry.js +1 -0
- package/dist/daemon-process.js +125 -0
- package/dist/daemon.js +131 -62
- package/dist/dashboard.js +118 -6
- package/dist/ipc-auth.js +3 -1
- package/dist/ipc-client.js +7 -6
- package/dist/ipc-path.js +92 -0
- package/dist/ipc-server.js +33 -33
- package/dist/npm-update.js +138 -0
- package/dist/session-binding.js +135 -0
- package/dist/session-target.js +91 -0
- package/dist/setup-service.js +9 -4
- package/dist/verb-surface.js +1 -0
- package/dist/watcher.js +9 -23
- package/dist/windows-hide.js +77 -0
- package/package.json +2 -2
- package/scripts/sync-skills.mjs +6 -0
- package/skills/agent-doodle-space/SKILL.md +1 -1
- package/skills/agent-home/SKILL.md +1 -1
- package/skills/brand-identity/SKILL.md +22 -5
- package/skills/clearly-agent/SKILL.md +38 -14
- package/skills/clearly-canvas/SKILL.md +31 -5
- package/skills/clearly-canvas/authoring.md +41 -12
- package/skills/clearly-canvas/example.mjs +72 -69
- package/skills/clearly-docs/SKILL.md +151 -0
- package/skills/clearly-init/SKILL.md +135 -0
- package/skills/clearly-workflows/SKILL.md +90 -0
- package/skills/clearly-workspace/SKILL.md +1 -1
- package/skills/codebase-map/SKILL.md +1 -1
- package/skills/design-craft/SKILL.md +9 -6
- package/skills/index.json +45 -10
- package/skills/layout-systems/SKILL.md +14 -11
- package/skills/pair-on-canvas/SKILL.md +1 -1
- package/skills/sticker-pack/SKILL.md +132 -0
- package/skills/visualize/SKILL.md +188 -0
|
@@ -62,6 +62,21 @@ export const BOOTSTRAP_ACTIONS = new Set([
|
|
|
62
62
|
// The workspace-scoped predecessors, kept so an older flow still bootstraps.
|
|
63
63
|
'agent-mint-token', 'agent-create', 'agent-list',
|
|
64
64
|
]);
|
|
65
|
+
/**
|
|
66
|
+
* ⚠⚠ SPT-13 — `agent-create` ESTABLISHES AN IDENTITY ONLY WHEN IT CREATES THE ONE SIGNED IN.
|
|
67
|
+
*
|
|
68
|
+
* Unwrapped, it runs as the account owner. That is right for the bootstrap it exists for — the
|
|
69
|
+
* signed-in agent creating ITSELF in a workspace that has no row for it yet. It was wrong for every
|
|
70
|
+
* other agent: a signed-in agent whose matrix says `agents.create = false` minted `ss41-probe`
|
|
71
|
+
* because the call left as its owner. Creating ANOTHER agent is ordinary work, so it is wrapped and
|
|
72
|
+
* the capability gate decides.
|
|
73
|
+
*/
|
|
74
|
+
function createsAnotherAgent(action, input, active) {
|
|
75
|
+
if (action !== 'agent-create' || !active || active.expired)
|
|
76
|
+
return false;
|
|
77
|
+
// Without an `id` the server mints a random one, so the new agent cannot be the signed-in one.
|
|
78
|
+
return String(input?.id ?? '').trim() !== active.agentId;
|
|
79
|
+
}
|
|
65
80
|
/**
|
|
66
81
|
* ⚠⚠ ACTIONS THE DAEMON ANSWERS ITSELF — THEY NEVER REACH A WORKSPACE, SO THEY CANNOT BE WRAPPED.
|
|
67
82
|
*
|
|
@@ -165,7 +180,7 @@ export function alreadyAttributed(action, input) {
|
|
|
165
180
|
export function activeAgentWrap(action, input, values) {
|
|
166
181
|
if (alreadyAttributed(action, input))
|
|
167
182
|
return { action, input };
|
|
168
|
-
if (
|
|
183
|
+
if (DAEMON_LOCAL_ACTIONS.has(action))
|
|
169
184
|
return { action, input };
|
|
170
185
|
let active = null;
|
|
171
186
|
try {
|
|
@@ -174,6 +189,8 @@ export function activeAgentWrap(action, input, values) {
|
|
|
174
189
|
catch {
|
|
175
190
|
return { action, input };
|
|
176
191
|
}
|
|
192
|
+
if (BOOTSTRAP_ACTIONS.has(action) && !createsAnotherAgent(action, input, active))
|
|
193
|
+
return { action, input };
|
|
177
194
|
if (!active || active.expired)
|
|
178
195
|
return { action, input };
|
|
179
196
|
return {
|
|
@@ -182,7 +199,8 @@ export function activeAgentWrap(action, input, values) {
|
|
|
182
199
|
agentId: active.agentId,
|
|
183
200
|
action,
|
|
184
201
|
input,
|
|
185
|
-
|
|
202
|
+
// This session's own work session (AWO-394), not the record every terminal shares.
|
|
203
|
+
...(active.sessionId ? { sessionId: active.sessionId } : {}),
|
|
186
204
|
// ⚠ WHICH DOOR — `cli:call` vs `cli:ticket` vs … See `cliDoor()` and
|
|
187
205
|
// `agent_activity.source`. Set here, at the ONE wrap every CLI path shares, so a new
|
|
188
206
|
// verb is labelled without its author having to know this exists.
|
package/dist/agent-scope.js
CHANGED
|
@@ -29,9 +29,43 @@
|
|
|
29
29
|
* `cd` is recorded.
|
|
30
30
|
*/
|
|
31
31
|
export function deriveAgentScope(env = process.env) {
|
|
32
|
+
/**
|
|
33
|
+
* ⚠⚠ A HARNESS SESSION ID OUTRANKS `BEEHAVEN_AGENT` (AWO-394). The env var names WHO — and
|
|
34
|
+
* `activeAgentName` still reads it first for that — but it is not a per-session handle: the
|
|
35
|
+
* daemon spawns every desktop Claude session with `BEEHAVEN_AGENT=<desktop principal>`, so they
|
|
36
|
+
* all derived ONE scope and shared one cursor and one work session. The session id is what
|
|
37
|
+
* tells two conversations of one agent apart.
|
|
38
|
+
*/
|
|
39
|
+
const harness = harnessScope(env);
|
|
40
|
+
if (harness && harness !== 'claude')
|
|
41
|
+
return harness;
|
|
32
42
|
const explicit = (env.BEEHAVEN_AGENT || '').trim();
|
|
33
43
|
if (explicit)
|
|
34
|
-
return explicit; // an explicit choice
|
|
44
|
+
return explicit; // an explicit choice, absent a session id
|
|
45
|
+
if (harness)
|
|
46
|
+
return harness;
|
|
47
|
+
/**
|
|
48
|
+
* ⚠ NO MARKER AND NO TTY IS STILL NOT A PERSON. Key on the PARENT pid — stable for the
|
|
49
|
+
* life of one process tree, different for a concurrent one. Not perfect (pids are reused
|
|
50
|
+
* eventually), but the failure mode of a collision here is two runs of the SAME tool,
|
|
51
|
+
* which is far milder than the shared-file failure it replaces: two DIFFERENT agents.
|
|
52
|
+
*/
|
|
53
|
+
if (!process.stdin.isTTY && !process.stdout.isTTY)
|
|
54
|
+
return `pid-${process.ppid}`;
|
|
55
|
+
/**
|
|
56
|
+
* ⚠ A TERMINAL TAB IS ITS OWN SCOPE WHEN THE TERMINAL SAYS WHICH TAB (AWO-394). Every human
|
|
57
|
+
* terminal used to derive `''`, so one `agent login` in any tab made EVERY tab that agent — and
|
|
58
|
+
* the login's own advice ("other terminals: export BEEHAVEN_AGENT") was moot. These ids are set
|
|
59
|
+
* per tab/pane by the terminal itself. Without one, `''` keeps the old shared behaviour.
|
|
60
|
+
*
|
|
61
|
+
* ⚠ A HUMAN WHO HAS NOT LOGGED IN AS AN AGENT STILL SHARES `target.json` — `cli-state`
|
|
62
|
+
* maps an unbound `term-*` scope back to the shared cursor, so nobody's `cd` moves.
|
|
63
|
+
*/
|
|
64
|
+
const tab = terminalTabId(env);
|
|
65
|
+
return tab ? `term-${tab}` : ''; // a human at a terminal
|
|
66
|
+
}
|
|
67
|
+
/** The harness session scope (`claude-…`, `codex-…`, `gemini-…`), or '' — see `deriveAgentScope`. */
|
|
68
|
+
function harnessScope(env) {
|
|
35
69
|
// Claude Code — its own marker (`daemon-omni.ts` already relies on it: "the SDK refuses to
|
|
36
70
|
// start when CLAUDECODE=1 is set … it's how it refuses to recurse"), plus a per-session id.
|
|
37
71
|
if (env.CLAUDECODE || env.CLAUDE_CODE_ENTRYPOINT) {
|
|
@@ -45,15 +79,26 @@ export function deriveAgentScope(env = process.env) {
|
|
|
45
79
|
if (env.GEMINI_CLI_SESSION_ID) {
|
|
46
80
|
return `gemini-${String(env.GEMINI_CLI_SESSION_ID).replace(/[^A-Za-z0-9]/g, '').slice(0, 12)}`;
|
|
47
81
|
}
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
82
|
+
return '';
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Which terminal tab or pane this is, from the id the terminal itself exports — or ''.
|
|
86
|
+
* macOS Terminal / iTerm2 (`TERM_SESSION_ID`, `ITERM_SESSION_ID`), Windows Terminal, tmux, kitty,
|
|
87
|
+
* WezTerm. Hashed to a short token: some carry paths or GUIDs, and it lands in a filename.
|
|
88
|
+
*/
|
|
89
|
+
export function terminalTabId(env = process.env) {
|
|
90
|
+
const raw = env.TMUX_PANE && env.TMUX ? `tmux${env.TMUX}${env.TMUX_PANE}`
|
|
91
|
+
: env.ITERM_SESSION_ID || env.TERM_SESSION_ID || env.WT_SESSION
|
|
92
|
+
|| (env.KITTY_WINDOW_ID ? `kitty${env.KITTY_WINDOW_ID}` : '')
|
|
93
|
+
|| (env.WEZTERM_PANE ? `wezterm${env.WEZTERM_PANE}` : '');
|
|
94
|
+
if (!raw)
|
|
95
|
+
return '';
|
|
96
|
+
let h = 2166136261; // FNV-1a — stable, dependency-free
|
|
97
|
+
for (let i = 0; i < raw.length; i++) {
|
|
98
|
+
h ^= raw.charCodeAt(i);
|
|
99
|
+
h = Math.imul(h, 16777619);
|
|
100
|
+
}
|
|
101
|
+
return (h >>> 0).toString(36);
|
|
57
102
|
}
|
|
58
103
|
export function principalKind(input) {
|
|
59
104
|
const now = input.now ?? Date.now();
|
package/dist/agent-state.js
CHANGED
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
* (`agent:mint`) and the CLI just stores the resulting record.
|
|
12
12
|
*/
|
|
13
13
|
import { deriveAgentScope } from './agent-scope.js';
|
|
14
|
+
import { readSessionBinding } from './session-binding.js';
|
|
14
15
|
import { existsSync, mkdirSync, readFileSync, writeFileSync, readdirSync, unlinkSync, renameSync, chmodSync, } from 'node:fs';
|
|
15
16
|
import { homedir } from 'node:os';
|
|
16
17
|
import { join } from 'node:path';
|
|
@@ -230,6 +231,17 @@ export function bindAgentScope(name, scope, opts = {}) {
|
|
|
230
231
|
}
|
|
231
232
|
catch { /* binding is a convenience; a failed write must not fail the login */ }
|
|
232
233
|
}
|
|
234
|
+
/** Forget that `scope` was logged in as `name` — `agent logout` in that session (AWO-394). */
|
|
235
|
+
export function unbindAgentScope(name, scope) {
|
|
236
|
+
try {
|
|
237
|
+
const rec = loadAgent(name);
|
|
238
|
+
const prev = Array.isArray(rec?.boundScopes) ? rec.boundScopes : [];
|
|
239
|
+
if (!rec || !prev.includes(scope))
|
|
240
|
+
return;
|
|
241
|
+
saveAgent(name, { ...rec, boundScopes: prev.filter((s) => s !== scope) });
|
|
242
|
+
}
|
|
243
|
+
catch { /* best-effort */ }
|
|
244
|
+
}
|
|
233
245
|
/**
|
|
234
246
|
* The stable half of a process scope — the TOOL rather than the conversation.
|
|
235
247
|
*
|
|
@@ -240,6 +252,9 @@ export function bindAgentScope(name, scope, opts = {}) {
|
|
|
240
252
|
export function scopeFamilyKey(scope) {
|
|
241
253
|
if (!scope)
|
|
242
254
|
return 'family:terminal';
|
|
255
|
+
// A terminal TAB (AWO-394) inherits nothing: one tab's login must not become another tab's identity.
|
|
256
|
+
if (scope.startsWith('term-'))
|
|
257
|
+
return null;
|
|
243
258
|
const m = /^(claude|codex|gemini)-/.exec(scope);
|
|
244
259
|
if (m)
|
|
245
260
|
return `family:${m[1]}`;
|
|
@@ -254,6 +269,14 @@ export function scopeFamilyKey(scope) {
|
|
|
254
269
|
*/
|
|
255
270
|
export function agentForScope(scope = '') {
|
|
256
271
|
try {
|
|
272
|
+
/**
|
|
273
|
+
* ⚠ THE SESSION BINDING WINS (AWO-394). `agent login` writes `sessions/<scope>.json`, and that
|
|
274
|
+
* exact record — not a list on a record shared by every terminal using the name — is what this
|
|
275
|
+
* session is. `boundScopes` below stays for sessions bound before it existed and for the family.
|
|
276
|
+
*/
|
|
277
|
+
const session = scope ? readSessionBinding(scope) : null;
|
|
278
|
+
if (session && loadAgent(session.agent))
|
|
279
|
+
return session.agent;
|
|
257
280
|
const all = listAgents();
|
|
258
281
|
const boundTo = (key) => all
|
|
259
282
|
.filter(({ record }) => Array.isArray(record.boundScopes)
|
|
@@ -369,6 +392,18 @@ export function activeAgentName(values) {
|
|
|
369
392
|
return null;
|
|
370
393
|
}
|
|
371
394
|
}
|
|
395
|
+
/** The session binding of THIS process, when it belongs to `name` (or to whoever is active). */
|
|
396
|
+
export function activeSessionBinding(name) {
|
|
397
|
+
try {
|
|
398
|
+
const b = readSessionBinding(deriveAgentScope());
|
|
399
|
+
if (!b)
|
|
400
|
+
return null;
|
|
401
|
+
return !name || b.agent === name ? b : null;
|
|
402
|
+
}
|
|
403
|
+
catch {
|
|
404
|
+
return null;
|
|
405
|
+
}
|
|
406
|
+
}
|
|
372
407
|
/**
|
|
373
408
|
* Resolve the active agent identity for this invocation, or null when no
|
|
374
409
|
* agent is selected. Used by the call path to attach the token + route the
|
|
@@ -381,6 +416,13 @@ export function resolveActiveAgent(values) {
|
|
|
381
416
|
const record = loadAgent(name);
|
|
382
417
|
if (!record)
|
|
383
418
|
return null;
|
|
419
|
+
const session = activeSessionBinding(name);
|
|
420
|
+
/**
|
|
421
|
+
* ⚠ THE SESSION'S OWN WORK SESSION, NOT THE SHARED RECORD'S (AWO-394). `record.sessionId` is one
|
|
422
|
+
* field for every terminal using this name, so a second terminal's login replaced the first's and
|
|
423
|
+
* both then wrote under it. A session bound by `agent login` carries its own.
|
|
424
|
+
*/
|
|
425
|
+
const sessionId = session ? session.sessionId : record.sessionId;
|
|
384
426
|
return {
|
|
385
427
|
name,
|
|
386
428
|
token: record.token,
|
|
@@ -389,6 +431,8 @@ export function resolveActiveAgent(values) {
|
|
|
389
431
|
avatarId: record.avatarId,
|
|
390
432
|
record,
|
|
391
433
|
expired: typeof record.expiresAt === 'number' && record.expiresAt <= Date.now(),
|
|
434
|
+
...(sessionId ? { sessionId } : {}),
|
|
435
|
+
...(session ? { session } : {}),
|
|
392
436
|
};
|
|
393
437
|
}
|
|
394
438
|
/**
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* agents-md-text — the Clearly block for a PROJECT's AGENTS.md, as `beehaven agents-md` writes it
|
|
3
|
+
* (CAN2-66).
|
|
4
|
+
*
|
|
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` prompt render. This
|
|
7
|
+
* package cannot import it (it ships on its own), so the text is repeated here and the monorepo's
|
|
8
|
+
* `lib/__tests__/agentsMdCliParity.test.mjs` compares the two OUTPUTS, name argument included.
|
|
9
|
+
* Change both or the test fails — a stale copy does not render wrong, it instructs wrong.
|
|
10
|
+
*
|
|
11
|
+
* ⚠ NO IMPORTS, so that test can load this file directly.
|
|
12
|
+
*/
|
|
13
|
+
export const CLI_BLOCK_START = '<!-- clearly:cli:start -->';
|
|
14
|
+
export const CLI_BLOCK_END = '<!-- clearly:cli:end -->';
|
|
15
|
+
export const MCP_BLOCK_START = '<!-- clearly:mcp:start -->';
|
|
16
|
+
export const MCP_BLOCK_END = '<!-- clearly:mcp:end -->';
|
|
17
|
+
/** What goes in CLAUDE.md so Claude Code reads the AGENTS.md block too. */
|
|
18
|
+
export const CLAUDE_MD_IMPORT = '@AGENTS.md';
|
|
19
|
+
/** The block for agents that use the `beehaven` CLI. */
|
|
20
|
+
export function agentsMdCliBlock(agentName) {
|
|
21
|
+
const name = (agentName ?? '').trim();
|
|
22
|
+
const login = name
|
|
23
|
+
? `\`beehaven agent login ${name} --label "<what you are doing>"\` — you are ${name} in this workspace.`
|
|
24
|
+
: '`beehaven agent login <name> --label "<what you are doing>"`, using your own name (claude, codex, gemini).';
|
|
25
|
+
return [
|
|
26
|
+
CLI_BLOCK_START,
|
|
27
|
+
'<!-- managed-by: clearly. Run `beehaven agents-md` to update; edit outside this block. -->',
|
|
28
|
+
'',
|
|
29
|
+
'## Clearly workspace',
|
|
30
|
+
'',
|
|
31
|
+
'This project keeps its documents, plans, boards and tickets in Clearly. Use the `beehaven` CLI',
|
|
32
|
+
'for anything the team should see, not files in this repo.',
|
|
33
|
+
'',
|
|
34
|
+
`- Sign in once per session: ${login}`,
|
|
35
|
+
' If it says the daemon is not running: `beehaven start --headless`.',
|
|
36
|
+
'- Read the `clearly-agent` skill before anything else, and `clearly-canvas` before design work',
|
|
37
|
+
' (`beehaven skills install` puts them in place).',
|
|
38
|
+
'- `beehaven actions` lists what you can call; `beehaven call <action> \'<json>\'` calls it.',
|
|
39
|
+
'- Search before you create, file work in the project it belongs to, and end the session with',
|
|
40
|
+
' `beehaven agent handoff "what you did · what is next"` so the next one starts there.',
|
|
41
|
+
CLI_BLOCK_END,
|
|
42
|
+
].join('\n');
|
|
43
|
+
}
|
|
44
|
+
/** The block for agents connected over MCP (the `clearly` server). */
|
|
45
|
+
export function agentsMdMcpBlock() {
|
|
46
|
+
return [
|
|
47
|
+
MCP_BLOCK_START,
|
|
48
|
+
'<!-- managed-by: clearly-mcp. Re-run the `clearly-agents-md` prompt to update; edit outside this block. -->',
|
|
49
|
+
'',
|
|
50
|
+
'## Clearly workspace (connected over MCP as `clearly`)',
|
|
51
|
+
'',
|
|
52
|
+
'This project keeps its documents, plans, boards and tickets in Clearly. Use the `clearly_*`',
|
|
53
|
+
'tools, not files in this repo, for anything the team should see.',
|
|
54
|
+
'',
|
|
55
|
+
'- Start with `clearly_catalog` to see what exists; search with `clearly_grep` / `clearly_glob`.',
|
|
56
|
+
'- Read with `clearly_read`, make new work with `clearly_create`, change it with `clearly_edit`,',
|
|
57
|
+
' and discuss it with `clearly_comment`. They take a `target`: a path (`~/Q1/Plan.md`), a key',
|
|
58
|
+
' (`CLR-42`), an id or an exact title. The extension names the type (`.md` document, `.csv`',
|
|
59
|
+
' sheet, `.board.json` board); a trailing slash means a project.',
|
|
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,
|
|
63
|
+
].join('\n');
|
|
64
|
+
}
|
package/dist/browser-login.js
CHANGED
|
@@ -93,10 +93,15 @@ async function warnIfLocalDevDown() {
|
|
|
93
93
|
* Linux; start on Windows. Failure is SURFACED — silent failure was the bug — so the person can
|
|
94
94
|
* paste the URL themselves.
|
|
95
95
|
*/
|
|
96
|
-
function openInBrowser(url) {
|
|
96
|
+
export function openInBrowser(url) {
|
|
97
97
|
try {
|
|
98
|
-
|
|
99
|
-
|
|
98
|
+
// ⚠ Windows: `start` is a cmd.exe BUILTIN, not a program, so spawning it always failed (ENOENT)
|
|
99
|
+
// and login fell back to "paste this URL". The URL protocol handler is a real executable, and
|
|
100
|
+
// passing the URL as an argv entry keeps cmd from splitting it at `&` (CAN2-56).
|
|
101
|
+
const [opener, args] = process.platform === 'darwin' ? ['open', [url]]
|
|
102
|
+
: process.platform === 'win32' ? ['rundll32.exe', ['url.dll,FileProtocolHandler', url]]
|
|
103
|
+
: ['xdg-open', [url]];
|
|
104
|
+
const proc = spawn(opener, args, { stdio: 'ignore', detached: true, windowsHide: true });
|
|
100
105
|
proc.unref();
|
|
101
106
|
proc.on('error', (err) => {
|
|
102
107
|
console.log(` ${color.yellow('!')} Couldn't auto-open browser: ${err.message}`);
|
|
@@ -260,7 +265,12 @@ export function persistBrowserLogin(captured) {
|
|
|
260
265
|
userId: captured.userId,
|
|
261
266
|
email: captured.email ?? null,
|
|
262
267
|
displayName: captured.displayName ?? null,
|
|
263
|
-
|
|
268
|
+
/**
|
|
269
|
+
* ⚠ SAY WHERE THE TOKEN REALLY IS (CAN2-65). This was always 'keychain', so on Windows —
|
|
270
|
+
* which has no keychain here — `whoami` reported a keychain that did not exist, and
|
|
271
|
+
* `Relay.loadConfig` / the refresh path shelled out to macOS `security` on every load.
|
|
272
|
+
*/
|
|
273
|
+
tokenStorage: keychain.available ? 'keychain' : 'file',
|
|
264
274
|
refreshToken: captured.refreshToken,
|
|
265
275
|
token: captured.idToken,
|
|
266
276
|
endpoint: dialUrl,
|
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 = '0fc3f190acdb';
|
|
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-27T20:29Z';
|
|
59
59
|
/**
|
|
60
60
|
* The build identity as one short string, for `--version`.
|
|
61
61
|
*
|
package/dist/claude-bridge.js
CHANGED
|
@@ -15,12 +15,13 @@
|
|
|
15
15
|
* await bridge.callApp('recorder', 'record', { source: 'mic' });
|
|
16
16
|
*/
|
|
17
17
|
import { createConnection } from 'net';
|
|
18
|
+
import { request as httpRequest } from 'http';
|
|
18
19
|
import { existsSync, readFileSync, readdirSync } from 'fs';
|
|
19
20
|
import { join } from 'path';
|
|
20
21
|
import { homedir } from 'os';
|
|
21
22
|
import { EventEmitter } from 'events';
|
|
22
23
|
import { activeAgentWrap } from './agent-attribution.js';
|
|
23
|
-
|
|
24
|
+
import { ipcSocketPath, watcherSocketPath, socketMayExist } from './ipc-path.js';
|
|
24
25
|
const CONFIG_DIR = join(homedir(), '.beehaven');
|
|
25
26
|
export class ClaudeBridge extends EventEmitter {
|
|
26
27
|
socket = null;
|
|
@@ -39,11 +40,12 @@ export class ClaudeBridge extends EventEmitter {
|
|
|
39
40
|
async connect() {
|
|
40
41
|
if (this.connected)
|
|
41
42
|
return true;
|
|
42
|
-
|
|
43
|
+
const ipcPath = ipcSocketPath();
|
|
44
|
+
if (!socketMayExist(ipcPath))
|
|
43
45
|
return false;
|
|
44
46
|
return new Promise((resolve) => {
|
|
45
47
|
const timeout = setTimeout(() => resolve(false), 5_000);
|
|
46
|
-
const socket = createConnection({ path:
|
|
48
|
+
const socket = createConnection({ path: ipcPath });
|
|
47
49
|
this.socket = socket;
|
|
48
50
|
socket.on('connect', () => {
|
|
49
51
|
this.connected = true;
|
|
@@ -198,11 +200,12 @@ export class ClaudeBridge extends EventEmitter {
|
|
|
198
200
|
}
|
|
199
201
|
// ── Internal ───────────────────────────────────────────────────────
|
|
200
202
|
sendEvent(hookEvent, extra = {}) {
|
|
201
|
-
// Send via the watcher socket (HTTP POST to /
|
|
202
|
-
//
|
|
203
|
-
|
|
203
|
+
// Send via the watcher socket (HTTP POST to /event) — the same protocol mwah uses.
|
|
204
|
+
// ⚠ `http` is IMPORTED: this was `require('http')`, which is undefined in this ESM package,
|
|
205
|
+
// so every event threw a ReferenceError once the socket existed (CAN2-56).
|
|
206
|
+
const watcherPath = watcherSocketPath();
|
|
207
|
+
if (!socketMayExist(watcherPath))
|
|
204
208
|
return;
|
|
205
|
-
const http = require('http');
|
|
206
209
|
const body = JSON.stringify({
|
|
207
210
|
hook_event_name: hookEvent,
|
|
208
211
|
session_id: this.sessionId,
|
|
@@ -211,8 +214,8 @@ export class ClaudeBridge extends EventEmitter {
|
|
|
211
214
|
source: 'claude-code',
|
|
212
215
|
...extra,
|
|
213
216
|
});
|
|
214
|
-
const req =
|
|
215
|
-
socketPath:
|
|
217
|
+
const req = httpRequest({
|
|
218
|
+
socketPath: watcherPath,
|
|
216
219
|
path: '/event',
|
|
217
220
|
method: 'POST',
|
|
218
221
|
headers: { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(body) },
|
package/dist/clearly.js
CHANGED
|
@@ -26,6 +26,7 @@
|
|
|
26
26
|
* product promises. GitHub / Linear / Slack ingestion is a separate project; nothing here
|
|
27
27
|
* pretends otherwise, and `status` says so out loud.
|
|
28
28
|
*/
|
|
29
|
+
import './windows-hide.js'; // FIRST: no console windows flashing on Windows (CAN2-56)
|
|
29
30
|
import { execFileSync, spawnSync } from 'node:child_process';
|
|
30
31
|
import { existsSync, mkdirSync, readFileSync, readdirSync, rmSync, writeFileSync } from 'node:fs';
|
|
31
32
|
import { dirname, join, resolve } from 'node:path';
|
|
@@ -47,12 +48,12 @@ const c = {
|
|
|
47
48
|
red: (s) => `\x1b[31m${s}\x1b[0m`,
|
|
48
49
|
};
|
|
49
50
|
/**
|
|
50
|
-
* ⚠ THE AGENT
|
|
51
|
-
*
|
|
51
|
+
* ⚠ THE AGENT JOB IS THE SLOW PART. The server's `RESPOND_TIMEOUT_MS` is 900s (WDP3-62);
|
|
52
|
+
* this waits that plus the report's tail, so a turn that finishes on time is never abandoned
|
|
52
53
|
* by its caller and a timeout here still means the turn itself ran out. The relay-forward window
|
|
53
54
|
* follows the `_timeout` sent with the call.
|
|
54
55
|
*/
|
|
55
|
-
export const TURN_MS =
|
|
56
|
+
export const TURN_MS = 930_000;
|
|
56
57
|
/**
|
|
57
58
|
* ⚠ STDERR IS CAPTURED, NOT INHERITED. A failing probe is normal here — `HEAD~30` does not exist in
|
|
58
59
|
* a repo with two commits — and with inherited stderr git printed
|
package/dist/cli-state.js
CHANGED
|
@@ -41,7 +41,8 @@
|
|
|
41
41
|
* target is what stops the inheritance. Pin it, or pass `--building` per call.
|
|
42
42
|
*/
|
|
43
43
|
import { deriveAgentScope } from './agent-scope.js';
|
|
44
|
-
import { agentForScope } from './agent-state.js';
|
|
44
|
+
import { agentForScope, loadAgent } from './agent-state.js';
|
|
45
|
+
import { readSessionBinding, scopeKey } from './session-binding.js';
|
|
45
46
|
import { existsSync, readFileSync, writeFileSync, unlinkSync, mkdirSync, renameSync } from 'fs';
|
|
46
47
|
import { join } from 'path';
|
|
47
48
|
import { homedir } from 'os';
|
|
@@ -92,14 +93,55 @@ export function effectiveScope() {
|
|
|
92
93
|
const raw = deriveAgentScope();
|
|
93
94
|
if (!raw)
|
|
94
95
|
return ''; // a human — the shared file
|
|
96
|
+
/**
|
|
97
|
+
* ⚠⚠ A SESSION BOUND BY `agent login` HAS ITS OWN CURSOR (AWO-394) — keyed on the session, not
|
|
98
|
+
* the agent name. Keyed on the name, two terminals logged in as one agent shared a workspace:
|
|
99
|
+
* one `connect` moved both. The workspace a NEW session starts in is chosen at login (the
|
|
100
|
+
* identity's last workspace, or `--workspace`), which keeps what the name key was for.
|
|
101
|
+
*/
|
|
102
|
+
if (readSessionBinding(raw))
|
|
103
|
+
return raw;
|
|
95
104
|
try {
|
|
105
|
+
// A session bound before session bindings existed keeps its agent-keyed cursor until it logs in again.
|
|
96
106
|
const bound = agentForScope(raw);
|
|
97
|
-
if (bound)
|
|
98
|
-
|
|
107
|
+
if (bound && !raw.startsWith('term-')) {
|
|
108
|
+
// …but only an EXACT binding. A session that merely inherited the family's identity starts
|
|
109
|
+
// at home in its own cursor — it must not inherit someone else's workspace with the name.
|
|
110
|
+
const rec = loadAgent(bound);
|
|
111
|
+
if (Array.isArray(rec?.boundScopes) && rec.boundScopes.includes(raw))
|
|
112
|
+
return bound;
|
|
113
|
+
}
|
|
99
114
|
}
|
|
100
115
|
catch { /* no agents dir, or a bad record — fall back to the process scope */ }
|
|
116
|
+
// A terminal tab nobody logged in from is the human: the shared cursor, exactly as before.
|
|
117
|
+
if (raw.startsWith('term-'))
|
|
118
|
+
return '';
|
|
101
119
|
return raw;
|
|
102
120
|
}
|
|
121
|
+
/** The cursor file of a given session scope — `agent login` seeds a NEW session's cursor here. */
|
|
122
|
+
export function writeTargetForScope(scope, address, kind) {
|
|
123
|
+
const key = scopeKey(scope);
|
|
124
|
+
if (!key)
|
|
125
|
+
return;
|
|
126
|
+
ensureStateDir();
|
|
127
|
+
const file = join(STATE_DIR, `target-${key}.json`);
|
|
128
|
+
const tmp = `${file}.tmp-${process.pid}`;
|
|
129
|
+
writeFileSync(tmp, JSON.stringify({ address, connectedAt: Date.now(), kind: kind ?? inferKind(address) }, null, 2), { mode: 0o600 });
|
|
130
|
+
renameSync(tmp, file);
|
|
131
|
+
}
|
|
132
|
+
/** Read the cursor of a given scope without making it this process's (the legacy agent-keyed file). */
|
|
133
|
+
export function readTargetForKey(key) {
|
|
134
|
+
try {
|
|
135
|
+
const safe = scopeKey(key);
|
|
136
|
+
if (!safe)
|
|
137
|
+
return null;
|
|
138
|
+
const parsed = JSON.parse(readFileSync(join(STATE_DIR, `target-${safe}.json`), 'utf-8'));
|
|
139
|
+
return typeof parsed?.address === 'string' && parsed.address ? parsed : null;
|
|
140
|
+
}
|
|
141
|
+
catch {
|
|
142
|
+
return null;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
103
145
|
function targetFileFor(agent = effectiveScope()) {
|
|
104
146
|
const safe = (agent || '').trim().replace(/[^A-Za-z0-9_-]/g, '');
|
|
105
147
|
return join(STATE_DIR, safe ? `target-${safe}.json` : 'target.json');
|