beehaven 0.8.24 → 0.8.27
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agent-state.js +19 -0
- package/dist/browser-login.js +283 -0
- package/dist/build-channel.js +2 -2
- package/dist/cli.js +13 -265
- package/dist/commands/agent.js +186 -59
- package/dist/commands/canvas.js +1 -1
- package/dist/commands/compose.js +2 -2
- package/dist/commands/help.js +2 -0
- package/dist/commands/self-update.js +2 -2
- package/dist/commands/workspace.js +8 -3
- package/dist/daemon.js +9 -9
- package/dist/folder-sync.js +1 -1
- package/dist/local-app-manager.js +1 -1
- package/dist/setup-relay.js +20 -235
- package/package.json +1 -1
- package/skills/clearly-agent/SKILL.md +14 -3
- package/skills/clearly-canvas/SKILL.md +9 -0
- package/skills/clearly-canvas/authoring.md +4 -4
- package/skills/index.json +2 -2
package/dist/setup-relay.js
CHANGED
|
@@ -1,148 +1,28 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
// ============================================================================
|
|
3
3
|
// Beehaven Office - Relay Setup (Login)
|
|
4
|
-
//
|
|
4
|
+
// The daemon's interactive sign-in fallback, and logout.
|
|
5
5
|
// ============================================================================
|
|
6
|
-
import
|
|
7
|
-
import crypto from 'crypto';
|
|
8
|
-
import { createInterface } from 'readline';
|
|
9
|
-
import { existsSync, readFileSync, writeFileSync, mkdirSync, chmodSync } from 'fs';
|
|
6
|
+
import { existsSync, readFileSync, writeFileSync, chmodSync } from 'fs';
|
|
10
7
|
import { homedir } from 'os';
|
|
11
8
|
import { join } from 'path';
|
|
12
9
|
import { spawnSync } from 'child_process';
|
|
13
10
|
import { Relay, CLEARLY_RELAY_URL } from './relay.js';
|
|
14
|
-
import { resolveRelaySelection } from './relay-presets.js';
|
|
15
11
|
import { SESSION_KEYCHAIN_ACCOUNTS, KEYCHAIN_SERVICE } from './keychain.js';
|
|
16
|
-
import { withUtm } from './utm.js';
|
|
17
12
|
const CONFIG_DIR = join(homedir(), '.beehaven');
|
|
18
13
|
const CONFIG_FILE = join(CONFIG_DIR, 'config.json');
|
|
19
|
-
const AUTH_BASE_URL = 'https://clearly.sh/auth/beehaven';
|
|
20
|
-
const LOGIN_TIMEOUT_MS = 120_000; // 2 minutes
|
|
21
14
|
/**
|
|
22
|
-
* The
|
|
23
|
-
*
|
|
15
|
+
* The daemon calls this when it starts interactively with no token, or when its session has
|
|
16
|
+
* expired (daemon.ts). It then re-reads the config, so all it needs is credentials on disk.
|
|
17
|
+
*
|
|
18
|
+
* ⚠⚠ AWO-224 — THIS USED TO BE A SECOND, DIFFERENT SIGN-IN. It opened
|
|
19
|
+
* `https://www.clearly.sh/auth/beehaven?callback=…`, waited for a single `token` and wrote its own
|
|
20
|
+
* config. The web app no longer serves that route (404 on prod and, via the apex redirect, the
|
|
21
|
+
* same 404), so the fallback opened a dead page and timed out after two minutes. It now runs the
|
|
22
|
+
* exact flow `beehaven login` runs (`browser-login.ts`: `/cli/auth`, the keychain idToken slot
|
|
23
|
+
* the daemon reads, both dial-target keys) and nothing else — it must NOT restart the daemon,
|
|
24
|
+
* because it runs inside it.
|
|
24
25
|
*/
|
|
25
|
-
function readConfigFile() {
|
|
26
|
-
if (!existsSync(CONFIG_FILE))
|
|
27
|
-
return {};
|
|
28
|
-
try {
|
|
29
|
-
return JSON.parse(readFileSync(CONFIG_FILE, 'utf8'));
|
|
30
|
-
}
|
|
31
|
-
catch {
|
|
32
|
-
return {};
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
function saveRelayConfig(token, endpoint) {
|
|
36
|
-
mkdirSync(CONFIG_DIR, { recursive: true, mode: 0o700 });
|
|
37
|
-
let existing = {};
|
|
38
|
-
if (existsSync(CONFIG_FILE)) {
|
|
39
|
-
try {
|
|
40
|
-
existing = JSON.parse(readFileSync(CONFIG_FILE, 'utf8'));
|
|
41
|
-
}
|
|
42
|
-
catch (err) {
|
|
43
|
-
console.warn(`[config] Failed to parse ${CONFIG_FILE}:`, err.message);
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
existing.token = token;
|
|
47
|
-
// ⚠ BOTH KEYS, as `beehaven env` writes them — `endpoint` is dialled, `relayUrl` is the alias
|
|
48
|
-
// other surfaces read. Updating one leaves the other naming a different environment.
|
|
49
|
-
existing.endpoint = endpoint;
|
|
50
|
-
existing.relayUrl = endpoint;
|
|
51
|
-
existing.tier = 'connected';
|
|
52
|
-
writeFileSync(CONFIG_FILE, JSON.stringify(existing, null, 2));
|
|
53
|
-
chmodSync(CONFIG_FILE, 0o600);
|
|
54
|
-
}
|
|
55
|
-
function prompt(question) {
|
|
56
|
-
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
57
|
-
return new Promise((resolve) => {
|
|
58
|
-
rl.question(question, (answer) => {
|
|
59
|
-
rl.close();
|
|
60
|
-
resolve(answer.trim());
|
|
61
|
-
});
|
|
62
|
-
});
|
|
63
|
-
}
|
|
64
|
-
/**
|
|
65
|
-
* Start a local HTTP server on a random port to receive the OAuth callback.
|
|
66
|
-
* Returns a promise for the token, plus helpers to get the port and close.
|
|
67
|
-
*/
|
|
68
|
-
function startCallbackServer() {
|
|
69
|
-
let resolveToken;
|
|
70
|
-
let rejectToken;
|
|
71
|
-
let serverRef;
|
|
72
|
-
let settled = false;
|
|
73
|
-
const oauthState = crypto.randomBytes(32).toString('hex');
|
|
74
|
-
const promise = new Promise((resolve, reject) => {
|
|
75
|
-
resolveToken = resolve;
|
|
76
|
-
rejectToken = reject;
|
|
77
|
-
serverRef = http.createServer((req, res) => {
|
|
78
|
-
if (settled)
|
|
79
|
-
return;
|
|
80
|
-
const url = new URL(req.url, `http://localhost`);
|
|
81
|
-
if (url.pathname === '/callback') {
|
|
82
|
-
// Validate OAuth state parameter to prevent CSRF (skip if server doesn't echo it back yet)
|
|
83
|
-
const returnedState = url.searchParams.get('state');
|
|
84
|
-
if (returnedState && returnedState !== oauthState) {
|
|
85
|
-
res.writeHead(403, { 'Content-Type': 'text/html' });
|
|
86
|
-
res.end('<!DOCTYPE html><html><body><h1>Invalid state parameter</h1></body></html>');
|
|
87
|
-
return;
|
|
88
|
-
}
|
|
89
|
-
const token = url.searchParams.get('token');
|
|
90
|
-
if (token) {
|
|
91
|
-
settled = true;
|
|
92
|
-
res.writeHead(200, { 'Content-Type': 'text/html' });
|
|
93
|
-
res.end([
|
|
94
|
-
'<!DOCTYPE html><html><head>',
|
|
95
|
-
'<meta charset="utf-8">',
|
|
96
|
-
'<title>Beehaven - Logged In</title>',
|
|
97
|
-
'<style>',
|
|
98
|
-
'body{font-family:system-ui,-apple-system,sans-serif;display:flex;align-items:center;justify-content:center;min-height:100vh;margin:0;background:#fef9ef}',
|
|
99
|
-
'.card{text-align:center;padding:48px;border-radius:16px;background:white;box-shadow:0 2px 12px rgba(0,0,0,0.08)}',
|
|
100
|
-
'h1{margin:0 0 8px;font-size:24px}',
|
|
101
|
-
'p{margin:0;color:#666;font-size:15px}',
|
|
102
|
-
'.bee{font-size:48px;margin-bottom:16px}',
|
|
103
|
-
'</style>',
|
|
104
|
-
'</head><body>',
|
|
105
|
-
'<div class="card">',
|
|
106
|
-
'<div class="bee">🐝</div>',
|
|
107
|
-
'<h1>Logged in!</h1>',
|
|
108
|
-
'<p>You can close this tab and return to your terminal.</p>',
|
|
109
|
-
'</div>',
|
|
110
|
-
'</body></html>',
|
|
111
|
-
].join(''));
|
|
112
|
-
serverRef.close();
|
|
113
|
-
resolve(token);
|
|
114
|
-
}
|
|
115
|
-
else {
|
|
116
|
-
res.writeHead(400, { 'Content-Type': 'text/html' });
|
|
117
|
-
res.end('<!DOCTYPE html><html><body><h1>Missing token</h1></body></html>');
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
else {
|
|
121
|
-
res.writeHead(404);
|
|
122
|
-
res.end();
|
|
123
|
-
}
|
|
124
|
-
});
|
|
125
|
-
serverRef.listen(0, '127.0.0.1');
|
|
126
|
-
// Timeout after 2 minutes
|
|
127
|
-
const timer = setTimeout(() => {
|
|
128
|
-
if (!settled) {
|
|
129
|
-
settled = true;
|
|
130
|
-
serverRef.close();
|
|
131
|
-
reject(new Error('Login timed out after 2 minutes.'));
|
|
132
|
-
}
|
|
133
|
-
}, LOGIN_TIMEOUT_MS);
|
|
134
|
-
serverRef.on('close', () => clearTimeout(timer));
|
|
135
|
-
});
|
|
136
|
-
return {
|
|
137
|
-
promise,
|
|
138
|
-
getPort: () => serverRef.address()?.port || 0,
|
|
139
|
-
close: () => {
|
|
140
|
-
settled = true;
|
|
141
|
-
serverRef?.close();
|
|
142
|
-
},
|
|
143
|
-
state: oauthState,
|
|
144
|
-
};
|
|
145
|
-
}
|
|
146
26
|
export async function login() {
|
|
147
27
|
console.log('');
|
|
148
28
|
console.log(' Clearly account required to use Beehaven.');
|
|
@@ -165,114 +45,19 @@ export async function login() {
|
|
|
165
45
|
console.log(' Session expired. Please log in again.');
|
|
166
46
|
console.log('');
|
|
167
47
|
}
|
|
168
|
-
// Start local callback server
|
|
169
|
-
const callbackServer = startCallbackServer();
|
|
170
|
-
// Small delay to let server bind
|
|
171
|
-
await new Promise((r) => setTimeout(r, 100));
|
|
172
|
-
const port = callbackServer.getPort();
|
|
173
|
-
if (!port) {
|
|
174
|
-
console.log(' Failed to start auth server.');
|
|
175
|
-
callbackServer.close();
|
|
176
|
-
return false;
|
|
177
|
-
}
|
|
178
|
-
const callbackUrl = `http://localhost:${port}/callback`;
|
|
179
|
-
const authUrl = withUtm(`${AUTH_BASE_URL}?callback=${encodeURIComponent(callbackUrl)}&state=${callbackServer.state}`);
|
|
180
|
-
// Try to open browser
|
|
181
|
-
let browserOpened = false;
|
|
182
|
-
try {
|
|
183
|
-
const openModule = await import('open');
|
|
184
|
-
await openModule.default(authUrl);
|
|
185
|
-
browserOpened = true;
|
|
186
|
-
}
|
|
187
|
-
catch {
|
|
188
|
-
browserOpened = false;
|
|
189
|
-
}
|
|
190
|
-
if (browserOpened) {
|
|
191
|
-
console.log(' Opening browser to sign in with Google...');
|
|
192
|
-
console.log('');
|
|
193
|
-
console.log(' Waiting for login... (press Ctrl+C to cancel)');
|
|
194
|
-
}
|
|
195
|
-
else {
|
|
196
|
-
// Fallback: show URL and offer manual token paste
|
|
197
|
-
console.log(' Could not open browser. Sign in manually:');
|
|
198
|
-
console.log('');
|
|
199
|
-
console.log(` 1. Open: ${authUrl}`);
|
|
200
|
-
console.log(' 2. Sign in with Google');
|
|
201
|
-
console.log('');
|
|
202
|
-
console.log(' Waiting for login callback...');
|
|
203
|
-
console.log(' Or paste your relay token below:');
|
|
204
|
-
console.log('');
|
|
205
|
-
// Race: callback server vs manual token paste
|
|
206
|
-
const manualToken = prompt(' Relay token: ').then((token) => {
|
|
207
|
-
if (token) {
|
|
208
|
-
callbackServer.close();
|
|
209
|
-
return token;
|
|
210
|
-
}
|
|
211
|
-
return null;
|
|
212
|
-
});
|
|
213
|
-
try {
|
|
214
|
-
const token = await Promise.race([
|
|
215
|
-
callbackServer.promise,
|
|
216
|
-
manualToken.then((t) => {
|
|
217
|
-
if (t)
|
|
218
|
-
return t;
|
|
219
|
-
return new Promise(() => { });
|
|
220
|
-
}),
|
|
221
|
-
]);
|
|
222
|
-
if (token) {
|
|
223
|
-
return await verifyAndSave(token);
|
|
224
|
-
}
|
|
225
|
-
}
|
|
226
|
-
catch (err) {
|
|
227
|
-
console.log(` ${err.message}`);
|
|
228
|
-
return false;
|
|
229
|
-
}
|
|
230
|
-
}
|
|
231
|
-
// Wait for callback
|
|
232
48
|
try {
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
console.log(` ${
|
|
238
|
-
return false;
|
|
239
|
-
}
|
|
240
|
-
}
|
|
241
|
-
async function verifyAndSave(token) {
|
|
242
|
-
// Validate token format before sending to server
|
|
243
|
-
if (!token || !/^[a-zA-Z0-9_\-.]{20,}$/.test(token)) {
|
|
244
|
-
console.log(' Invalid token format.');
|
|
245
|
-
return false;
|
|
246
|
-
}
|
|
247
|
-
console.log('');
|
|
248
|
-
console.log(' Verifying...');
|
|
249
|
-
const relay = new Relay();
|
|
250
|
-
/**
|
|
251
|
-
* ⚠⚠ THE ACTIVE RELAY, NOT A HARDCODED PROD CONSTANT. Both of these were `CLEARLY_RELAY_URL`, so
|
|
252
|
-
* a staging or dev user who signed in here was VERIFIED against prod and then had `endpoint`
|
|
253
|
-
* OVERWRITTEN to prod — silently moving them to another environment as a side effect of logging
|
|
254
|
-
* in. `beehaven env staging` writes the target; a login must honour it, never redefine it.
|
|
255
|
-
*
|
|
256
|
-
* ⚠ Read from the file rather than `Relay.loadConfig()`: that returns null without a token, which
|
|
257
|
-
* is exactly the state a first login is in. `resolveRelaySelection` defaults to prod when the
|
|
258
|
-
* config names nothing, so the no-config case is unchanged.
|
|
259
|
-
*/
|
|
260
|
-
const dialUrl = resolveRelaySelection(readConfigFile()).url;
|
|
261
|
-
const profile = await relay.verifyToken(token, dialUrl);
|
|
262
|
-
if (profile) {
|
|
263
|
-
saveRelayConfig(token, dialUrl);
|
|
264
|
-
if (profile.displayName) {
|
|
265
|
-
console.log(` Logged in as ${profile.displayName} (${profile.subscriptionPlan})`);
|
|
266
|
-
}
|
|
267
|
-
else {
|
|
268
|
-
console.log(' Logged in.');
|
|
269
|
-
}
|
|
49
|
+
// ⚠ Imported lazily: the Mac app loads this file for `logout` alone and should not pull in
|
|
50
|
+
// the sign-in flow's dependencies to do it.
|
|
51
|
+
const { browserLogin } = await import('./browser-login.js');
|
|
52
|
+
const who = await browserLogin();
|
|
53
|
+
console.log(` Logged in as ${who.email ?? who.userId}.`);
|
|
270
54
|
console.log(' Config saved to ~/.beehaven/config.json');
|
|
271
55
|
console.log('');
|
|
272
56
|
return true;
|
|
273
57
|
}
|
|
274
|
-
|
|
275
|
-
console.log(
|
|
58
|
+
catch (err) {
|
|
59
|
+
console.log(` ${err?.message ?? err}`);
|
|
60
|
+
console.log(' Run `beehaven login`, then start again.');
|
|
276
61
|
console.log('');
|
|
277
62
|
return false;
|
|
278
63
|
}
|
package/package.json
CHANGED
|
@@ -75,6 +75,17 @@ the admitted agent in the workspace, and soul reads/updates report whether the d
|
|
|
75
75
|
wearing the drawing or a preset. If the capability line is absent, it is disabled for that
|
|
76
76
|
deployment; the existing companion and presets remain available.
|
|
77
77
|
|
|
78
|
+
## End with a handoff
|
|
79
|
+
|
|
80
|
+
```bash
|
|
81
|
+
beehaven agent handoff "what you did · what is next · anything blocked"
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
The handoff is the note your next login, or the next agent you hand the work to, reads first,
|
|
85
|
+
under **Last time you were here**. Write it for a colleague who has none of your context: what changed,
|
|
86
|
+
where it is (ticket keys, document titles), what is next and what is blocked. It ends the work session
|
|
87
|
+
and keeps your identity bound. Without one, the next brief can show only the session's label and counts.
|
|
88
|
+
|
|
78
89
|
⚠ **Do not call `agent-login` immediately after `beehaven agent login`.** The RPC is an alias of
|
|
79
90
|
`agent-session-start`; doing both creates two sessions. Call it directly only when deliberately starting
|
|
80
91
|
or resuming an additional workspace session.
|
|
@@ -118,8 +129,8 @@ item queued. Every comment/description write returns `mentions`, `agentsQueued`
|
|
|
118
129
|
`beehaven comment <ref>` takes any id, key or exact title and the surface's anchor:
|
|
119
130
|
`--quote` (document passage) · `--slide 3` (deck, required) · `--cell B4` / `--cell "Tab!B4"` (sheet) ·
|
|
120
131
|
`--column "In review"` (board) · `--pin <id>` or `--x/--y` (canvas) · `--reply <id>`. An anchor a
|
|
121
|
-
surface cannot carry is **refused**, never dropped. Over MCP: `
|
|
122
|
-
|
|
132
|
+
surface cannot carry is **refused**, never dropped. Over MCP: `clearly_comment { target, content,
|
|
133
|
+
fields }`, `clearly_read { type:"comment", target }`, and `target:"@me"` for mentions.
|
|
123
134
|
|
|
124
135
|
## Inbox — people and agents message you, and you message them
|
|
125
136
|
|
|
@@ -140,7 +151,7 @@ beehaven inbox who # every handle you can put in <
|
|
|
140
151
|
|
|
141
152
|
Over MCP, `type: "message"` on the same tools: `clearly_read { type: "message", target: "@inbox" }`
|
|
142
153
|
lists your threads (`box`, `query`), `target: "@people"` lists who you can message, and a thread id
|
|
143
|
-
opens it (marks read). `
|
|
154
|
+
opens it (marks read). `clearly_comment { type: "message", fields: { to: ["ada"] }, content }` starts a
|
|
144
155
|
thread; with `target: <threadId>` it replies. `clearly_edit { changes: { read | starred | muted } }`
|
|
145
156
|
changes your view; `clearly_delete` archives (`restore: true` brings it back).
|
|
146
157
|
|
|
@@ -195,6 +195,15 @@ runs. The same steps sent as hundreds of separate calls each re-read the whole s
|
|
|
195
195
|
call fails with a dropped connection, read the scene (`composition-detail
|
|
196
196
|
{id, includeScene:true}`) before retrying: the work may have landed.
|
|
197
197
|
|
|
198
|
+
⚠ **One batch per canvas at a time.** A reply that times out ("outcome unknown") or drops does
|
|
199
|
+
not stop the batch — it is still running on the workspace. Resending at once used to start a
|
|
200
|
+
second, slow, unprotected batch and could half-apply both (CA-43). The workspace now queues a
|
|
201
|
+
second writer behind the first and answers `code: "canvas-busy"` (with the running batch's
|
|
202
|
+
progress) if it waits too long — nothing from that call was applied. Either way: read the scene,
|
|
203
|
+
then send only what is missing. A ~1,100-step build takes about 40 s; the CLI waits 180 s by
|
|
204
|
+
default and `--timeout <ms>` (or `BEEHAVEN_RPC_TIMEOUT_MS`) raises it. Never place component
|
|
205
|
+
instances after a build whose reply you did not see.
|
|
206
|
+
|
|
198
207
|
Keep review notes and status outside the delivered artwork. Use
|
|
199
208
|
[communications.md](communications.md) only when posting those blocks is useful.
|
|
200
209
|
|
|
@@ -163,11 +163,11 @@ the backing shows through as dark wedges at the corners.
|
|
|
163
163
|
|
|
164
164
|
### Images from MCP
|
|
165
165
|
|
|
166
|
-
The
|
|
166
|
+
The artifact MCP tools do not upload local binary files or dispatch `canvas-act`.
|
|
167
167
|
For an image already reachable by an absolute URL, read the composition with
|
|
168
168
|
`clearly_read {type:"composition",target:<id>}`; append an image node
|
|
169
169
|
`{id,type:"image",src,x,y,w,h,fit:"contain",parentId?}` to the returned complete node
|
|
170
|
-
array; then call `
|
|
170
|
+
array; then call `clearly_replace {type:"composition",target:<id>,content:<JSON array>,
|
|
171
171
|
expectedRev:<canvasNodesRev>}`. Preserve every existing node. A stale revision is a
|
|
172
172
|
merge prompt, never a reason to drop `expectedRev`. `clearly_edit` can change an
|
|
173
173
|
existing image's `src`, `fit` or geometry, but cannot create a node. For a local file,
|
|
@@ -212,8 +212,8 @@ MCP's artifact tools are split by operation:
|
|
|
212
212
|
|
|
213
213
|
- `clearly_catalog {type:"composition"}` exposes the current schemas.
|
|
214
214
|
- `clearly_read` returns nodes and `canvasNodesRev`.
|
|
215
|
-
- `
|
|
216
|
-
`expectedRev` from the read
|
|
215
|
+
- `clearly_create {type:"composition"}` makes a composition; `clearly_replace` replaces its full
|
|
216
|
+
node array — pass `expectedRev` from the read.
|
|
217
217
|
- `clearly_edit` patches existing nodes with `changes:{ids,patch}`.
|
|
218
218
|
|
|
219
219
|
Canvas action dispatch and targeted pixels use the CLI. Do not invent
|
package/skills/index.json
CHANGED
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
"project-plans.md",
|
|
18
18
|
"review.mjs"
|
|
19
19
|
],
|
|
20
|
-
"sha": "
|
|
20
|
+
"sha": "8b1890ca7588af6c"
|
|
21
21
|
},
|
|
22
22
|
{
|
|
23
23
|
"name": "agent-home",
|
|
@@ -81,6 +81,6 @@
|
|
|
81
81
|
"files": [
|
|
82
82
|
"SKILL.md"
|
|
83
83
|
],
|
|
84
|
-
"sha": "
|
|
84
|
+
"sha": "c16e1b5d08dbd669"
|
|
85
85
|
}
|
|
86
86
|
]
|