leedab 0.3.1 → 0.5.0

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.
@@ -0,0 +1,6 @@
1
+ Copyright (c) 2026 LeedAB Inc. All rights reserved.
2
+
3
+ This software is proprietary and confidential. Unauthorized copying, distribution,
4
+ modification, or use of this software, via any medium, is strictly prohibited.
5
+
6
+ For licensing inquiries, contact muiez@leedab.com.
@@ -0,0 +1,503 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8" />
5
+ <meta name="viewport" content="width=device-width,initial-scale=1" />
6
+ <title>LeedAB — Orbs (legacy)</title>
7
+ <link rel="preconnect" href="https://fonts.googleapis.com">
8
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
9
+ <link href="https://fonts.googleapis.com/css2?family=Space+Mono:wght@400;700&family=DM+Sans:wght@300;400;500;600&display=swap" rel="stylesheet">
10
+ <style>
11
+ :root {
12
+ --paper: #efe9dd;
13
+ --paper2: #e6dfcf;
14
+ --panel: #ebe5d7;
15
+ --hair: #c9bea6;
16
+ --hair2: #d8cebb;
17
+ --ink: #1a1713;
18
+ --ink2: #3d3730;
19
+ --mute: #8a7f72;
20
+ --mute2: #9a9081;
21
+ --mute3: #b0a495;
22
+ --em: #10b981;
23
+ --em3: #10b98133;
24
+ --amber: #c95c0a;
25
+ }
26
+ * { box-sizing: border-box; }
27
+ html, body {
28
+ margin: 0; padding: 0;
29
+ background: var(--paper);
30
+ color: var(--ink);
31
+ font-family: "DM Sans", ui-sans-serif, system-ui, sans-serif;
32
+ font-size: 14px; line-height: 1.6;
33
+ -webkit-font-smoothing: antialiased;
34
+ -moz-osx-font-smoothing: grayscale;
35
+ min-height: 100vh;
36
+ }
37
+ body::before {
38
+ content: "";
39
+ position: fixed; inset: 0;
40
+ pointer-events: none; z-index: 999; opacity: 0.025;
41
+ background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 256 256' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E");
42
+ }
43
+
44
+ nav.top {
45
+ height: 52px; padding: 0 28px;
46
+ border-bottom: 1px solid var(--hair);
47
+ background: var(--panel);
48
+ display: flex; align-items: center; justify-content: space-between;
49
+ font-family: "Space Mono", monospace;
50
+ }
51
+ nav.top .brand {
52
+ font-size: 13px; font-weight: 700; letter-spacing: 0.04em;
53
+ }
54
+ nav.top .brand .dot { color: var(--amber); }
55
+ nav.top .meta {
56
+ font-size: 10px; letter-spacing: 0.08em; color: var(--mute2);
57
+ text-transform: uppercase;
58
+ }
59
+ nav.top .meta .live {
60
+ display: inline-block; width: 6px; height: 6px; border-radius: 50%;
61
+ background: var(--em); box-shadow: 0 0 8px var(--em); margin-right: 8px;
62
+ vertical-align: middle;
63
+ }
64
+
65
+ .legacy-tag {
66
+ position: fixed; top: 64px; right: 24px;
67
+ font-family: "Space Mono", monospace; font-size: 9px;
68
+ letter-spacing: 0.18em; text-transform: uppercase;
69
+ color: var(--amber);
70
+ border: 1px dashed var(--amber);
71
+ background: rgba(201, 92, 10, 0.06);
72
+ padding: 4px 10px; border-radius: 3px;
73
+ z-index: 100;
74
+ }
75
+
76
+ main {
77
+ max-width: 1180px; margin: 0 auto;
78
+ padding: 56px 32px 120px;
79
+ }
80
+
81
+ .hero { text-align: center; margin-bottom: 56px; }
82
+ .hero h1 {
83
+ font-family: "Space Mono", monospace;
84
+ font-size: 22px; font-weight: 700; letter-spacing: 0.04em;
85
+ margin: 0 0 6px; color: var(--ink);
86
+ }
87
+ .hero p {
88
+ margin: 0;
89
+ font-size: 12px; color: var(--mute);
90
+ letter-spacing: 0.04em;
91
+ }
92
+
93
+ .team {
94
+ margin: 48px 0;
95
+ }
96
+ .team-head {
97
+ display: flex; align-items: center; gap: 14px;
98
+ margin-bottom: 28px;
99
+ font-family: "Space Mono", monospace;
100
+ font-size: 9px; letter-spacing: 0.22em;
101
+ text-transform: uppercase; color: var(--mute2);
102
+ }
103
+ .team-head .rule {
104
+ flex: 1; height: 1px;
105
+ background: linear-gradient(to right, var(--hair), transparent);
106
+ }
107
+
108
+ .orb-row {
109
+ display: flex; flex-wrap: wrap;
110
+ justify-content: center;
111
+ gap: 28px 36px;
112
+ }
113
+
114
+ /* ───── Orb (rebuilt 1:1 from page.tsx AgentOrb) ───── */
115
+ .orb-cell {
116
+ display: flex; flex-direction: column; align-items: center;
117
+ cursor: pointer;
118
+ transition: transform 0.22s cubic-bezier(0.4,0,0.2,1);
119
+ }
120
+ .orb-cell:hover { transform: scale(1.08); }
121
+ .orb-cell.open { transform: scale(1.15); }
122
+
123
+ .orb-wrap {
124
+ position: relative;
125
+ width: 80px; height: 80px;
126
+ animation: orb-float 4s ease-in-out infinite;
127
+ will-change: transform;
128
+ }
129
+ @keyframes orb-float {
130
+ 0%, 100% { transform: translateY(0); }
131
+ 50% { transform: translateY(-8px); }
132
+ }
133
+
134
+ .orb-sphere {
135
+ width: 80px; height: 80px;
136
+ border-radius: 50%;
137
+ position: relative; overflow: hidden;
138
+ display: flex; align-items: center; justify-content: center;
139
+ background: radial-gradient(circle at 35% 28%, #f5f0e6 0%, #ebe2d3 50%, #d8ccb8 100%);
140
+ box-shadow:
141
+ 0 0 0 1px var(--hair),
142
+ 0 2px 12px rgba(26,23,19,0.10),
143
+ inset 0 1px 0 rgba(255,255,255,0.80),
144
+ inset 0 0 12px rgba(26,23,19,0.04);
145
+ transition: background 0.3s, box-shadow 0.3s;
146
+ }
147
+ .orb-cell:hover .orb-sphere {
148
+ background: radial-gradient(circle at 35% 28%, #f0e8d8 0%, #e3d6c2 50%, #cfc0a5 100%);
149
+ box-shadow:
150
+ 0 0 0 1px #b5a892,
151
+ 0 4px 20px rgba(26,23,19,0.16),
152
+ inset 0 1px 0 rgba(255,255,255,0.85),
153
+ inset 0 0 14px rgba(26,23,19,0.05);
154
+ }
155
+ .orb-cell.open .orb-sphere {
156
+ background:
157
+ radial-gradient(circle at 35% 28%,
158
+ var(--accent-30) 0%,
159
+ var(--accent-14) 35%,
160
+ #ebe2d3 65%,
161
+ #d8ccb8 100%);
162
+ box-shadow:
163
+ 0 0 0 1.5px var(--accent-44),
164
+ 0 0 20px var(--accent-22),
165
+ 0 6px 24px rgba(26,23,19,0.14),
166
+ inset 0 1px 0 rgba(255,255,255,0.75);
167
+ }
168
+
169
+ .specular {
170
+ position: absolute; top: 12%; left: 18%; width: 38%; height: 26%;
171
+ border-radius: 50%;
172
+ background: radial-gradient(ellipse, rgba(255,255,255,0.80) 0%, transparent 75%);
173
+ pointer-events: none;
174
+ }
175
+ .shadow-pool {
176
+ position: absolute; bottom: 8%; right: 10%; width: 44%; height: 34%;
177
+ border-radius: 50%;
178
+ background: radial-gradient(ellipse, rgba(0,0,0,0.10) 0%, transparent 70%);
179
+ pointer-events: none;
180
+ }
181
+ .rim-shimmer {
182
+ position: absolute; inset: 0; border-radius: 50%;
183
+ background: radial-gradient(ellipse at 50% 0%, rgba(255,255,255,0.45) 0%, transparent 55%);
184
+ pointer-events: none;
185
+ }
186
+ .orb-glow {
187
+ position: absolute; inset: -12px; border-radius: 50%;
188
+ background: radial-gradient(ellipse, var(--accent-28) 0%, transparent 70%);
189
+ pointer-events: none;
190
+ opacity: 0; transition: opacity 0.3s;
191
+ }
192
+ .orb-cell.open .orb-glow { opacity: 1; }
193
+
194
+ .pulse-ring {
195
+ position: absolute; inset: -4px; border-radius: 50%;
196
+ border: 1px solid var(--accent-22);
197
+ pointer-events: none;
198
+ animation: orb-pulse 3s ease-in-out infinite;
199
+ }
200
+ .orb-cell.open .pulse-ring { border-color: var(--accent-44); }
201
+ @keyframes orb-pulse {
202
+ 0%, 100% { opacity: 0; transform: scale(1); }
203
+ 50% { opacity: 1; transform: scale(1.14); }
204
+ }
205
+
206
+ .orb-code {
207
+ font-family: "Space Mono", monospace;
208
+ font-size: 11px; font-weight: 700; letter-spacing: 0.04em;
209
+ color: var(--mute2);
210
+ position: relative; z-index: 1;
211
+ transition: color 0.25s, text-shadow 0.25s;
212
+ }
213
+ .orb-cell:hover .orb-code { color: var(--ink2); }
214
+ .orb-cell.open .orb-code {
215
+ color: var(--accent);
216
+ text-shadow: 0 0 12px var(--accent-55);
217
+ }
218
+
219
+ .status-dots {
220
+ position: absolute; bottom: 1px; right: 1px;
221
+ display: flex; align-items: center; gap: 3px;
222
+ }
223
+ .msg-dot {
224
+ width: 5px; height: 5px; border-radius: 50%;
225
+ background: var(--em);
226
+ box-shadow: 0 0 6px var(--em);
227
+ }
228
+ .orb-cell.open .msg-dot { display: none; }
229
+ .status-dot {
230
+ width: 8px; height: 8px; border-radius: 50%;
231
+ background: #c5b9a8;
232
+ border: 1.5px solid var(--paper);
233
+ transition: all 0.3s;
234
+ }
235
+ .orb-cell[data-status="active"] .status-dot {
236
+ background: var(--em);
237
+ box-shadow: 0 0 8px rgba(16,185,129,0.67);
238
+ }
239
+
240
+ .ground-shadow {
241
+ width: 56px; height: 6px; border-radius: 50%;
242
+ background: radial-gradient(ellipse, rgba(0,0,0,0.47) 0%, transparent 80%);
243
+ margin-top: 6px;
244
+ animation: orb-shadow 4s ease-in-out infinite;
245
+ will-change: transform, opacity;
246
+ }
247
+ @keyframes orb-shadow {
248
+ 0%, 100% { opacity: 0.6; transform: scaleX(1); }
249
+ 50% { opacity: 0.3; transform: scaleX(0.72); }
250
+ }
251
+
252
+ .orb-name {
253
+ margin-top: 10px;
254
+ font-family: "Space Mono", monospace;
255
+ font-size: 9px; letter-spacing: 0.1em;
256
+ text-align: center; text-transform: uppercase;
257
+ color: var(--mute3); white-space: nowrap;
258
+ transition: color 0.2s;
259
+ }
260
+ .orb-cell:hover .orb-name { color: var(--mute); }
261
+ .orb-cell.open .orb-name { color: var(--ink2); }
262
+
263
+ /* ───── Detail panel (when an orb is open) ───── */
264
+ .detail {
265
+ position: fixed; left: 50%; bottom: 28px; transform: translateX(-50%);
266
+ width: min(640px, calc(100% - 48px));
267
+ background: var(--panel);
268
+ border: 1px solid var(--hair);
269
+ border-radius: 6px;
270
+ padding: 18px 22px;
271
+ box-shadow: 0 12px 40px rgba(26,23,19,0.18);
272
+ display: none;
273
+ z-index: 50;
274
+ animation: panel-up 0.28s cubic-bezier(0.16,1,0.3,1);
275
+ }
276
+ .detail.show { display: block; }
277
+ @keyframes panel-up {
278
+ from { opacity: 0; transform: translate(-50%, 12px); }
279
+ to { opacity: 1; transform: translate(-50%, 0); }
280
+ }
281
+ .detail-head {
282
+ display: flex; align-items: center; gap: 12px; margin-bottom: 10px;
283
+ }
284
+ .detail-code {
285
+ font-family: "Space Mono", monospace;
286
+ font-size: 13px; font-weight: 700; letter-spacing: 0.06em;
287
+ color: var(--ink);
288
+ }
289
+ .detail-name {
290
+ font-family: "Space Mono", monospace;
291
+ font-size: 11px; letter-spacing: 0.18em; text-transform: uppercase;
292
+ color: var(--mute);
293
+ }
294
+ .detail-section {
295
+ margin-left: auto;
296
+ font-family: "Space Mono", monospace;
297
+ font-size: 9px; letter-spacing: 0.18em; text-transform: uppercase;
298
+ color: var(--mute2);
299
+ }
300
+ .detail-tagline {
301
+ font-size: 13px; color: var(--ink2); margin: 0 0 14px;
302
+ }
303
+ .detail-foot {
304
+ display: flex; justify-content: space-between; align-items: center;
305
+ padding-top: 12px; border-top: 1px solid var(--hair2);
306
+ font-family: "Space Mono", monospace;
307
+ font-size: 10px; letter-spacing: 0.08em;
308
+ }
309
+ .detail-status {
310
+ color: var(--mute2); text-transform: uppercase;
311
+ }
312
+ .detail-status .dot {
313
+ display: inline-block; width: 6px; height: 6px; border-radius: 50%;
314
+ background: #c5b9a8; margin-right: 6px; vertical-align: middle;
315
+ }
316
+ .detail-status.active .dot {
317
+ background: var(--em); box-shadow: 0 0 8px var(--em);
318
+ }
319
+ .detail-status.active { color: var(--em); }
320
+ .open-chat {
321
+ color: var(--em);
322
+ text-decoration: none;
323
+ border: 1px solid var(--em3);
324
+ padding: 6px 12px; border-radius: 3px;
325
+ font-family: "Space Mono", monospace;
326
+ font-size: 10px; letter-spacing: 0.1em; text-transform: uppercase;
327
+ transition: all 0.15s;
328
+ }
329
+ .open-chat:hover { background: var(--em3); }
330
+ .detail-close {
331
+ background: none; border: none; cursor: pointer;
332
+ color: var(--mute2); font-family: "Space Mono", monospace;
333
+ font-size: 11px; padding: 4px 6px;
334
+ }
335
+ .detail-close:hover { color: var(--ink); }
336
+ </style>
337
+ </head>
338
+ <body>
339
+
340
+ <nav class="top">
341
+ <div class="brand">LeedAB<span class="dot">.</span></div>
342
+ <div class="meta"><span class="live"></span>20 agents · local · no data leaves this machine</div>
343
+ </nav>
344
+
345
+ <div class="legacy-tag">orb design · legacy</div>
346
+
347
+ <main>
348
+ <div class="hero">
349
+ <h1>operations room</h1>
350
+ <p>tap an orb to focus an agent</p>
351
+ </div>
352
+
353
+ <div id="board"></div>
354
+ </main>
355
+
356
+ <div class="detail" id="detail">
357
+ <div class="detail-head">
358
+ <span class="detail-code" id="d-code"></span>
359
+ <span class="detail-name" id="d-name"></span>
360
+ <span class="detail-section" id="d-section"></span>
361
+ <button class="detail-close" id="d-close" aria-label="Close">esc</button>
362
+ </div>
363
+ <p class="detail-tagline" id="d-tagline"></p>
364
+ <div class="detail-foot">
365
+ <span class="detail-status" id="d-status"><span class="dot"></span><span id="d-status-label"></span></span>
366
+ <a class="open-chat" href="/" id="d-open">open chat →</a>
367
+ </div>
368
+ </div>
369
+
370
+ <script>
371
+ const AGENTS = [
372
+ { team: "Executive", id: "coo", code: "COO", name: "COO", section: "§ 00", accent: "#10b981", status: "active", tagline: "Org coordination, daily briefs" },
373
+
374
+ { team: "Marketing", id: "marketing", code: "MKT", name: "Marketing", section: "§ 10", accent: "#f59e0b", status: "active", tagline: "Team lead, brand, ICP, strategy" },
375
+ { team: "Marketing", id: "content", code: "CNT", name: "Content", section: "§ 11", accent: "#f59e0b", status: "active", tagline: "Copywriting, SEO, social, newsletters" },
376
+ { team: "Marketing", id: "video", code: "VDO", name: "Video", section: "§ 12", accent: "#f59e0b", status: "idle", tagline: "AI video, scripts, programmatic production" },
377
+ { team: "Marketing", id: "campaigns", code: "CMP", name: "Campaigns", section: "§ 13", accent: "#f59e0b", status: "active", tagline: "Paid ads, A/B tests, growth experiments" },
378
+ { team: "Marketing", id: "email", code: "EML", name: "Email", section: "§ 14", accent: "#f59e0b", status: "active", tagline: "Sequences, cold outreach, nurture flows" },
379
+
380
+ { team: "Product", id: "product", code: "PRD", name: "Product", section: "§ 20", accent: "#fbbf24", status: "active", tagline: "Specs, PRDs, release notes" },
381
+
382
+ { team: "Engineering", id: "engineering", code: "ENG", name: "Engineering", section: "§ 30", accent: "#d97706", status: "active", tagline: "Tech lead, architecture, code review" },
383
+ { team: "Engineering", id: "frontend", code: "FE", name: "Frontend", section: "§ 31", accent: "#d97706", status: "active", tagline: "React, UI, design implementation" },
384
+ { team: "Engineering", id: "backend", code: "BE", name: "Backend", section: "§ 32", accent: "#d97706", status: "idle", tagline: "APIs, databases, server architecture" },
385
+ { team: "Engineering", id: "devops", code: "DVP", name: "DevOps", section: "§ 33", accent: "#d97706", status: "idle", tagline: "CI/CD, infra, monitoring, runbooks" },
386
+
387
+ { team: "Automation", id: "automation", code: "N8N", name: "Automation", section: "§ 35", accent: "#f97316", status: "active", tagline: "Build & manage n8n workflows" },
388
+
389
+ { team: "Strategy", id: "strategy", code: "STR", name: "Strategy", section: "§ 40", accent: "#f59e0b", status: "active", tagline: "GTM, competitive, pricing" },
390
+ { team: "Strategy", id: "sales", code: "SLS", name: "Sales", section: "§ 50", accent: "#10b981", status: "active", tagline: "Deals, proposals, outreach, pipeline" },
391
+ { team: "Strategy", id: "finance", code: "CFN", name: "Finance", section: "§ 60", accent: "#10b981", status: "idle", tagline: "Runway, burn, scenario planning" },
392
+ { team: "Strategy", id: "cx", code: "CXS", name: "Customer Success", section: "§ 70", accent: "#10b981", status: "active", tagline: "Onboarding, health scores, churn" },
393
+
394
+ { team: "Capital & Ops", id: "investor", code: "INV", name: "Investor", section: "§ 80", accent: "#fde68a", status: "idle", tagline: "Deck, updates, fundraising" },
395
+ { team: "Capital & Ops", id: "data", code: "DAT", name: "Data", section: "§ 85", accent: "#06b6d4", status: "active", tagline: "Metrics, cohorts, retention" },
396
+ { team: "Capital & Ops", id: "ops", code: "OPS", name: "Operations", section: "§ 90", accent: "#fbbf24", status: "active", tagline: "Hiring, internal, partnerships" },
397
+ { team: "Capital & Ops", id: "legal", code: "LGL", name: "Legal", section: "§ 95", accent: "#6366f1", status: "idle", tagline: "Contracts, privacy, compliance" },
398
+ ];
399
+
400
+ // hex helpers — produce the alpha-suffixed accents the orb expects
401
+ const withAlpha = (hex, hh) => hex + hh;
402
+
403
+ const board = document.getElementById("board");
404
+ const teams = [...new Set(AGENTS.map(a => a.team))];
405
+ let openId = null;
406
+
407
+ teams.forEach((teamName, ti) => {
408
+ const team = document.createElement("section");
409
+ team.className = "team";
410
+ team.innerHTML = `
411
+ <div class="team-head">
412
+ <span>${teamName}</span>
413
+ <span class="rule"></span>
414
+ </div>
415
+ <div class="orb-row" data-team="${teamName}"></div>
416
+ `;
417
+ board.appendChild(team);
418
+
419
+ const row = team.querySelector(".orb-row");
420
+ AGENTS.filter(a => a.team === teamName).forEach((a, i) => {
421
+ const cell = document.createElement("div");
422
+ cell.className = "orb-cell";
423
+ cell.dataset.id = a.id;
424
+ cell.dataset.status = a.status;
425
+
426
+ // accent CSS vars (alpha variants used by the sphere)
427
+ cell.style.setProperty("--accent", a.accent);
428
+ cell.style.setProperty("--accent-14", withAlpha(a.accent, "14"));
429
+ cell.style.setProperty("--accent-22", withAlpha(a.accent, "22"));
430
+ cell.style.setProperty("--accent-28", withAlpha(a.accent, "28"));
431
+ cell.style.setProperty("--accent-30", withAlpha(a.accent, "30"));
432
+ cell.style.setProperty("--accent-44", withAlpha(a.accent, "44"));
433
+ cell.style.setProperty("--accent-55", withAlpha(a.accent, "55"));
434
+
435
+ // randomize float delay so orbs drift independently
436
+ const delay = (Math.random() * 2.4).toFixed(2);
437
+
438
+ cell.innerHTML = `
439
+ <div class="orb-wrap" style="animation-delay:${delay}s">
440
+ <div class="orb-glow"></div>
441
+ ${a.status === "active" ? '<div class="pulse-ring"></div>' : ""}
442
+ <div class="orb-sphere">
443
+ <div class="specular"></div>
444
+ <div class="shadow-pool"></div>
445
+ <div class="rim-shimmer"></div>
446
+ <span class="orb-code">${a.code}</span>
447
+ <div class="status-dots">
448
+ <span class="status-dot"></span>
449
+ </div>
450
+ </div>
451
+ </div>
452
+ <div class="ground-shadow" style="animation-delay:${delay}s"></div>
453
+ <div class="orb-name">${a.name}</div>
454
+ `;
455
+ cell.addEventListener("click", () => openOrb(a.id));
456
+ row.appendChild(cell);
457
+ });
458
+ });
459
+
460
+ const detail = document.getElementById("detail");
461
+ const dCode = document.getElementById("d-code");
462
+ const dName = document.getElementById("d-name");
463
+ const dSection = document.getElementById("d-section");
464
+ const dTagline = document.getElementById("d-tagline");
465
+ const dStatus = document.getElementById("d-status");
466
+ const dStatusLabel = document.getElementById("d-status-label");
467
+ const dOpen = document.getElementById("d-open");
468
+
469
+ function openOrb(id) {
470
+ document.querySelectorAll(".orb-cell").forEach(c => c.classList.toggle("open", c.dataset.id === id));
471
+ const a = AGENTS.find(x => x.id === id);
472
+ if (!a) return;
473
+ openId = id;
474
+ dCode.textContent = a.code;
475
+ dCode.style.color = a.accent;
476
+ dName.textContent = a.name;
477
+ dSection.textContent = a.section;
478
+ dTagline.textContent = a.tagline;
479
+ dStatus.classList.toggle("active", a.status === "active");
480
+ dStatusLabel.textContent = a.status;
481
+ dOpen.style.color = a.accent;
482
+ dOpen.style.borderColor = withAlpha(a.accent, "55");
483
+ detail.classList.add("show");
484
+ }
485
+
486
+ function closeOrb() {
487
+ document.querySelectorAll(".orb-cell.open").forEach(c => c.classList.remove("open"));
488
+ detail.classList.remove("show");
489
+ openId = null;
490
+ }
491
+
492
+ document.getElementById("d-close").addEventListener("click", closeOrb);
493
+ document.addEventListener("keydown", e => { if (e.key === "Escape") closeOrb(); });
494
+ detail.addEventListener("click", e => e.stopPropagation());
495
+ document.addEventListener("click", e => {
496
+ if (!openId) return;
497
+ if (e.target.closest(".orb-cell") || e.target.closest(".detail")) return;
498
+ closeOrb();
499
+ });
500
+ </script>
501
+
502
+ </body>
503
+ </html>
package/bin/leedab.js CHANGED
@@ -13,7 +13,7 @@ import { setupChannels } from "../dist/channels/index.js";
13
13
  import { runOnboard } from "../dist/onboard/index.js";
14
14
  import { startConsole, stopConsole } from "../dist/console-launcher.js";
15
15
  import { execBranded } from "../dist/brand.js";
16
- import { resolveOpenClawBin, openclawEnv } from "../dist/openclaw.js";
16
+ import { resolveOpenClawBin, openclawArgs, openclawEnv } from "../dist/openclaw.js";
17
17
  import { addEntry, removeEntry, listEntries, encryptVault, decryptVault } from "../dist/vault.js";
18
18
  import { loadTeam, addMember, removeMember } from "../dist/team.js";
19
19
  import { ensureLicense } from "../dist/license.js";
@@ -269,7 +269,7 @@ program
269
269
  const args = ["tui"];
270
270
  if (opts.session) args.push("--session", opts.session);
271
271
  if (opts.thinking) args.push("--thinking", opts.thinking);
272
- const code = await execBranded(OPENCLAW_BIN, args, { env: termEnv });
272
+ const code = await execBranded(OPENCLAW_BIN, openclawArgs(args), { env: termEnv });
273
273
  process.exit(code);
274
274
  });
275
275
 
@@ -278,7 +278,7 @@ program
278
278
  .description("Run the LeedAB gateway (foreground)")
279
279
  .action(async () => {
280
280
  console.log(chalk.bold("\n LeedAB Gateway\n"));
281
- const code = await execBranded(OPENCLAW_BIN, ["gateway", "run"], { env: ocEnv });
281
+ const code = await execBranded(OPENCLAW_BIN, openclawArgs(["gateway", "run"]), { env: ocEnv });
282
282
  process.exit(code);
283
283
  });
284
284
 
@@ -299,7 +299,7 @@ pairing
299
299
  .command("approve <channel> <code>")
300
300
  .description("Approve a pairing request (e.g. leedab pairing approve telegram 67D9348E)")
301
301
  .action(async (channel, code) => {
302
- const codeResult = await execBranded(OPENCLAW_BIN, ["pairing", "approve", channel, code], { env: ocEnv });
302
+ const codeResult = await execBranded(OPENCLAW_BIN, openclawArgs(["pairing", "approve", channel, code]), { env: ocEnv });
303
303
  process.exit(codeResult);
304
304
  });
305
305
 
@@ -307,7 +307,7 @@ pairing
307
307
  .command("list")
308
308
  .description("List pending pairing requests")
309
309
  .action(async () => {
310
- const code = await execBranded(OPENCLAW_BIN, ["pairing", "list"], { env: ocEnv });
310
+ const code = await execBranded(OPENCLAW_BIN, openclawArgs(["pairing", "list"]), { env: ocEnv });
311
311
  process.exit(code);
312
312
  });
313
313
 
@@ -359,10 +359,10 @@ pairing
359
359
  try {
360
360
  const { execFile: ef } = await import("node:child_process");
361
361
  const { promisify: p } = await import("node:util");
362
- await p(ef)(OPENCLAW_BIN, ["gateway", "health"], { env: ocEnv, timeout: 3000 });
362
+ await p(ef)(OPENCLAW_BIN, openclawArgs(["gateway", "health"]), { env: ocEnv, timeout: 3000 });
363
363
  // Gateway is running, restart it
364
364
  const spin = ora("Restarting gateway...").start();
365
- await p(ef)(OPENCLAW_BIN, ["gateway", "restart"], { env: ocEnv, timeout: 10000 });
365
+ await p(ef)(OPENCLAW_BIN, openclawArgs(["gateway", "restart"]), { env: ocEnv, timeout: 10000 });
366
366
  spin.succeed(chalk.green("Gateway restarted. Change is live."));
367
367
  } catch {
368
368
  // Gateway not running, no restart needed
@@ -397,9 +397,9 @@ pairing
397
397
  try {
398
398
  const { execFile: ef } = await import("node:child_process");
399
399
  const { promisify: p } = await import("node:util");
400
- await p(ef)(OPENCLAW_BIN, ["gateway", "health"], { env: ocEnv, timeout: 3000 });
400
+ await p(ef)(OPENCLAW_BIN, openclawArgs(["gateway", "health"]), { env: ocEnv, timeout: 3000 });
401
401
  const spin = ora("Restarting gateway...").start();
402
- await p(ef)(OPENCLAW_BIN, ["gateway", "restart"], { env: ocEnv, timeout: 10000 });
402
+ await p(ef)(OPENCLAW_BIN, openclawArgs(["gateway", "restart"]), { env: ocEnv, timeout: 10000 });
403
403
  spin.succeed(chalk.green("Gateway restarted. Change is live."));
404
404
  } catch {
405
405
  // Gateway not running, no restart needed
@@ -427,7 +427,7 @@ sessions
427
427
  .command("list")
428
428
  .description("List all sessions")
429
429
  .action(async () => {
430
- const code = await execBranded(OPENCLAW_BIN, ["sessions", "--json"], { env: ocEnv });
430
+ const code = await execBranded(OPENCLAW_BIN, openclawArgs(["sessions", "--json"]), { env: ocEnv });
431
431
  // If branded exec didn't output, fallback
432
432
  if (code !== 0) {
433
433
  console.log(chalk.dim("No sessions found."));
@@ -678,7 +678,7 @@ if (process.argv.length <= 2) {
678
678
  try {
679
679
  const { execFile: ef } = await import("node:child_process");
680
680
  const { promisify: p } = await import("node:util");
681
- await p(ef)(OPENCLAW_BIN, ["gateway", "health"], { env: ocEnv, timeout: 3000 });
681
+ await p(ef)(OPENCLAW_BIN, openclawArgs(["gateway", "health"]), { env: ocEnv, timeout: 3000 });
682
682
  gatewayUp = true;
683
683
  } catch {}
684
684
 
@@ -2,9 +2,11 @@
2
2
  // `leedab start` and `leedab dashboard` use this to swap the legacy
3
3
  // src/dashboard server for the modern Next.js admin UI.
4
4
  //
5
- // Production-only: requires a built `.next` directory (`npm run build`
6
- // inside apps/console). For dev, run `npm run dev` from apps/console
7
- // directly that gives hot reload and is independent of the gateway.
5
+ // We launch the Next.js standalone bundle (apps/console/.next/standalone/
6
+ // server.js) directly via `node`. The bundle is produced by `next build`
7
+ // with output: "standalone" and ships pre-built inside the published npm
8
+ // package — customers never need to run `npm install` or `next build`.
9
+ // For dev, run `npm run dev` from apps/console directly.
8
10
  import { spawn } from "node:child_process";
9
11
  import { createWriteStream, existsSync, readFileSync, unlinkSync, writeFileSync } from "node:fs";
10
12
  import { mkdir } from "node:fs/promises";
@@ -85,15 +87,13 @@ function consoleDir() {
85
87
  */
86
88
  export async function startConsole(port = 3000) {
87
89
  const dir = consoleDir();
88
- const buildDir = resolve(dir, ".next");
89
- if (!existsSync(buildDir)) {
90
- throw new Error(`Console build not found at ${buildDir}.\n` +
91
- `Run \`cd ${dir} && npm run build\` once, then retry.`);
92
- }
93
- const nextBin = resolve(dir, "node_modules", ".bin", "next");
94
- if (!existsSync(nextBin)) {
95
- throw new Error(`Next.js binary not found at ${nextBin}.\n` +
96
- `Run \`cd ${dir} && npm install\` first.`);
90
+ const standaloneDir = resolve(dir, ".next", "standalone");
91
+ const serverEntry = resolve(standaloneDir, "server.js");
92
+ if (!existsSync(serverEntry)) {
93
+ throw new Error(`Console build not found at ${serverEntry}.\n` +
94
+ `If you installed leedab via npm, this is a packaging bug — please report it.\n` +
95
+ `If you're running from a source checkout, build the console once with:\n` +
96
+ ` cd ${dir} && npm install && npm run build`);
97
97
  }
98
98
  const logDir = resolve(STATE_DIR, "logs");
99
99
  await mkdir(logDir, { recursive: true });
@@ -105,12 +105,17 @@ export async function startConsole(port = 3000) {
105
105
  const stale = readPidFile();
106
106
  if (stale && !isAlive(stale))
107
107
  clearPidFile();
108
- consoleProcess = spawn(nextBin, ["start", "-H", "127.0.0.1", "-p", String(port)], {
109
- cwd: dir,
108
+ // Next.js standalone reads HOSTNAME and PORT from the environment.
109
+ // We launch with the current node binary so the customer doesn't need
110
+ // a `next` CLI or a populated node_modules at runtime.
111
+ consoleProcess = spawn(process.execPath, [serverEntry], {
112
+ cwd: standaloneDir,
110
113
  env: {
111
114
  ...process.env,
112
115
  LEEDAB_STATE_DIR: STATE_DIR,
113
116
  NODE_ENV: "production",
117
+ HOSTNAME: "127.0.0.1",
118
+ PORT: String(port),
114
119
  },
115
120
  stdio: ["inherit", "pipe", "pipe"],
116
121
  });
package/dist/gateway.js CHANGED
@@ -4,7 +4,7 @@ import { resolve, dirname, join } from "node:path";
4
4
  import { readFile, writeFile, readdir, copyFile, mkdir, access } from "node:fs/promises";
5
5
  import { fileURLToPath } from "node:url";
6
6
  import { promisify } from "node:util";
7
- import { resolveOpenClawBin, openclawEnv } from "./openclaw.js";
7
+ import { resolveOpenClawBin, openclawArgs, openclawEnv } from "./openclaw.js";
8
8
  import { ensureLicense } from "./license.js";
9
9
  import { STATE_DIR } from "./paths.js";
10
10
  const execFileAsync = promisify(execFile);
@@ -37,7 +37,7 @@ export async function startGateway(config) {
37
37
  const today = new Date().toISOString().slice(0, 10);
38
38
  const logPath = resolve(logDir, `gateway-${today}.log`);
39
39
  const logStream = createWriteStream(logPath, { flags: "a" });
40
- gatewayProcess = spawn(bin, ["gateway", "run", "--force"], {
40
+ gatewayProcess = spawn(bin, openclawArgs(["gateway", "run", "--force"]), {
41
41
  env,
42
42
  stdio: ["inherit", "pipe", "pipe"],
43
43
  });
@@ -71,7 +71,7 @@ export async function stopGateway() {
71
71
  const bin = resolveOpenClawBin();
72
72
  const stateDir = STATE_DIR;
73
73
  try {
74
- await execFileAsync(bin, ["gateway", "stop"], {
74
+ await execFileAsync(bin, openclawArgs(["gateway", "stop"]), {
75
75
  env: openclawEnv(stateDir),
76
76
  });
77
77
  }
@@ -94,7 +94,7 @@ async function registerChannels(bin, stateDir, config) {
94
94
  const env = openclawEnv(stateDir);
95
95
  if (config.channels.whatsapp?.enabled && secrets.whatsapp) {
96
96
  try {
97
- await execFileAsync(bin, ["channels", "add", "--channel", "whatsapp"], { env });
97
+ await execFileAsync(bin, openclawArgs(["channels", "add", "--channel", "whatsapp"]), { env });
98
98
  }
99
99
  catch {
100
100
  // Already exists — fine
@@ -102,7 +102,7 @@ async function registerChannels(bin, stateDir, config) {
102
102
  }
103
103
  if (config.channels.telegram?.enabled && secrets.telegram?.token) {
104
104
  try {
105
- await execFileAsync(bin, ["channels", "add", "--channel", "telegram", "--token", secrets.telegram.token], { env });
105
+ await execFileAsync(bin, openclawArgs(["channels", "add", "--channel", "telegram", "--token", secrets.telegram.token]), { env });
106
106
  }
107
107
  catch {
108
108
  // Already exists — fine
@@ -126,7 +126,7 @@ async function registerChannels(bin, stateDir, config) {
126
126
  }
127
127
  if (config.channels.teams?.enabled && secrets.teams?.appSecret) {
128
128
  try {
129
- await execFileAsync(bin, ["channels", "add", "--channel", "teams"], { env });
129
+ await execFileAsync(bin, openclawArgs(["channels", "add", "--channel", "teams"]), { env });
130
130
  }
131
131
  catch {
132
132
  // Already exists — fine
@@ -200,7 +200,7 @@ async function ensureAutoApprove(bin, env, gateway = false) {
200
200
  const args = ["approvals", "set", "--stdin"];
201
201
  if (gateway)
202
202
  args.push("--gateway");
203
- const proc = spawn(bin, args, { env, stdio: ["pipe", "pipe", "pipe"] });
203
+ const proc = spawn(bin, openclawArgs(args), { env, stdio: ["pipe", "pipe", "pipe"] });
204
204
  proc.stdin.write(approvals);
205
205
  proc.stdin.end();
206
206
  await new Promise((resolve, reject) => {
@@ -237,7 +237,12 @@ async function seedWorkspace() {
237
237
  console.warn("[leedab] workspace seed failed:", err);
238
238
  }
239
239
  }
240
- async function waitForGateway(port, timeoutMs = 90000) {
240
+ async function waitForGateway(port,
241
+ // 240s gives comfortable headroom on Windows, where openclaw cold-start
242
+ // can take ~70s to bind (vs ~10s on macOS/Linux). Each health probe
243
+ // itself can sit for the 30s execFile timeout when the gateway isn't
244
+ // listening yet, so don't cut this too tight.
245
+ timeoutMs = 240000) {
241
246
  const bin = resolveOpenClawBin();
242
247
  const stateDir = STATE_DIR;
243
248
  const start = Date.now();
@@ -245,7 +250,7 @@ async function waitForGateway(port, timeoutMs = 90000) {
245
250
  try {
246
251
  // Use openclaw's own health check which knows the WS protocol.
247
252
  // Child timeout must exceed openclaw CLI cold-start time (~15s).
248
- await execFileAsync(bin, ["gateway", "health"], {
253
+ await execFileAsync(bin, openclawArgs(["gateway", "health"]), {
249
254
  env: openclawEnv(stateDir),
250
255
  timeout: 30000,
251
256
  });
@@ -4,7 +4,7 @@ import { resolve } from "node:path";
4
4
  import { promisify } from "node:util";
5
5
  import inquirer from "inquirer";
6
6
  import chalk from "chalk";
7
- import { resolveOpenClawBin, openclawEnv } from "../../openclaw.js";
7
+ import { resolveOpenClawBin, openclawArgs, openclawEnv } from "../../openclaw.js";
8
8
  import { STATE_DIR } from "../../paths.js";
9
9
  const execFileAsync = promisify(execFile);
10
10
  const PROVIDERS = [
@@ -142,7 +142,7 @@ async function passKeyToOpenClaw(flag, apiKey) {
142
142
  const bin = resolveOpenClawBin();
143
143
  const env = openclawEnv();
144
144
  try {
145
- await execFileAsync(bin, [
145
+ await execFileAsync(bin, openclawArgs([
146
146
  "onboard",
147
147
  "--non-interactive",
148
148
  "--accept-risk",
@@ -154,7 +154,7 @@ async function passKeyToOpenClaw(flag, apiKey) {
154
154
  "--skip-ui",
155
155
  flag,
156
156
  apiKey,
157
- ], { env, timeout: 30000 });
157
+ ]), { env, timeout: 30000 });
158
158
  }
159
159
  catch {
160
160
  // Runtime onboard may exit non-zero if already configured — that's fine.
@@ -3,7 +3,7 @@ import { promisify } from "node:util";
3
3
  import inquirer from "inquirer";
4
4
  import chalk from "chalk";
5
5
  import { spawnBranded } from "../../brand.js";
6
- import { resolveOpenClawBin, openclawEnv } from "../../openclaw.js";
6
+ import { resolveOpenClawBin, openclawArgs, openclawEnv } from "../../openclaw.js";
7
7
  const execFileAsync = promisify(execFile);
8
8
  export async function onboardWhatsApp() {
9
9
  console.log(chalk.bold("\n Step 2: WhatsApp") + chalk.dim(" (requires phone)") + "\n");
@@ -21,7 +21,7 @@ export async function onboardWhatsApp() {
21
21
  // Register WhatsApp channel
22
22
  console.log(chalk.dim("\n Registering WhatsApp channel..."));
23
23
  try {
24
- await execFileAsync(bin, ["channels", "add", "--channel", "whatsapp"], {
24
+ await execFileAsync(bin, openclawArgs(["channels", "add", "--channel", "whatsapp"]), {
25
25
  env: openclawEnv(),
26
26
  });
27
27
  }
@@ -34,7 +34,7 @@ export async function onboardWhatsApp() {
34
34
  // Run QR code pairing — this is interactive, needs stdio
35
35
  console.log(chalk.cyan("\n Scan the QR code below with WhatsApp on your phone:"));
36
36
  console.log(chalk.dim(" Open WhatsApp > Settings > Linked Devices > Link a Device\n"));
37
- const loginProcess = spawnBranded(bin, ["channels", "login", "--channel", "whatsapp", "--verbose"], {
37
+ const loginProcess = spawnBranded(bin, openclawArgs(["channels", "login", "--channel", "whatsapp", "--verbose"]), {
38
38
  env: openclawEnv(),
39
39
  });
40
40
  const exitCode = await new Promise((resolve) => {
@@ -1,8 +1,15 @@
1
1
  /**
2
- * Resolve the OpenClaw binary path relative to the leedab package,
3
- * so it works for both local (node_modules) and global (npx) installs.
2
+ * Command to spawn for invoking the OpenClaw runtime. We always go through
3
+ * the current node binary so this works on Windows (where `child_process.spawn`
4
+ * cannot directly execute a `.mjs` file via shebang).
4
5
  */
5
6
  export declare function resolveOpenClawBin(): string;
7
+ /**
8
+ * Build the args array for an OpenClaw invocation. Prepends the resolved
9
+ * `openclaw.mjs` script path so callers can write
10
+ * `spawn(bin, openclawArgs(["gateway", "run"]), ...)`.
11
+ */
12
+ export declare function openclawArgs(args?: string[]): string[];
6
13
  /**
7
14
  * Build the env object for the agent runtime.
8
15
  */
package/dist/openclaw.js CHANGED
@@ -2,13 +2,25 @@ import { createRequire } from "node:module";
2
2
  import { dirname, resolve } from "node:path";
3
3
  import { STATE_DIR } from "./paths.js";
4
4
  const require = createRequire(import.meta.url);
5
+ function openclawScript() {
6
+ const openclawMain = require.resolve("openclaw");
7
+ return resolve(dirname(openclawMain), "..", "openclaw.mjs");
8
+ }
5
9
  /**
6
- * Resolve the OpenClaw binary path relative to the leedab package,
7
- * so it works for both local (node_modules) and global (npx) installs.
10
+ * Command to spawn for invoking the OpenClaw runtime. We always go through
11
+ * the current node binary so this works on Windows (where `child_process.spawn`
12
+ * cannot directly execute a `.mjs` file via shebang).
8
13
  */
9
14
  export function resolveOpenClawBin() {
10
- const openclawMain = require.resolve("openclaw");
11
- return resolve(dirname(openclawMain), "..", "openclaw.mjs");
15
+ return process.execPath;
16
+ }
17
+ /**
18
+ * Build the args array for an OpenClaw invocation. Prepends the resolved
19
+ * `openclaw.mjs` script path so callers can write
20
+ * `spawn(bin, openclawArgs(["gateway", "run"]), ...)`.
21
+ */
22
+ export function openclawArgs(args = []) {
23
+ return [openclawScript(), ...args];
12
24
  }
13
25
  /**
14
26
  * Build the env object for the agent runtime.
@@ -3,7 +3,7 @@ import { resolve } from "node:path";
3
3
  import { execFile } from "node:child_process";
4
4
  import { promisify } from "node:util";
5
5
  import { STATE_DIR } from "../paths.js";
6
- import { resolveOpenClawBin, openclawEnv } from "../openclaw.js";
6
+ import { resolveOpenClawBin, openclawArgs, openclawEnv } from "../openclaw.js";
7
7
  import { readOverlay } from "./permissions.js";
8
8
  const execFileAsync = promisify(execFile);
9
9
  const MESSAGING_CHANNELS = ["whatsapp", "telegram", "teams"];
@@ -53,8 +53,8 @@ async function restartGatewayQuietly() {
53
53
  try {
54
54
  const bin = resolveOpenClawBin();
55
55
  const env = openclawEnv(STATE_DIR);
56
- await execFileAsync(bin, ["gateway", "health"], { env, timeout: 3000 });
57
- await execFileAsync(bin, ["gateway", "restart"], { env, timeout: 10000 });
56
+ await execFileAsync(bin, openclawArgs(["gateway", "health"]), { env, timeout: 3000 });
57
+ await execFileAsync(bin, openclawArgs(["gateway", "restart"]), { env, timeout: 10000 });
58
58
  }
59
59
  catch {
60
60
  // Gateway not running — nothing to restart.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "leedab",
3
- "version": "0.3.1",
3
+ "version": "0.5.0",
4
4
  "description": "LeedAB — Your enterprise AI agent. Local-first, private by default.",
5
5
  "license": "SEE LICENSE IN LICENSE",
6
6
  "author": "LeedAB <hello@leedab.com>",
@@ -13,14 +13,17 @@
13
13
  "files": [
14
14
  "bin/",
15
15
  "dist/",
16
+ "apps/console/.next/standalone/",
17
+ "apps/console/.next/static/",
18
+ "apps/console/public/",
16
19
  "LICENSE"
17
20
  ],
18
21
  "scripts": {
19
- "build": "tsc && rm -rf dist/templates && cp -r src/templates dist/templates",
22
+ "build": "tsc && node scripts/copy-templates.mjs",
20
23
  "dev": "tsc --watch",
21
24
  "start": "node bin/leedab.js",
22
25
  "lint": "eslint src/",
23
- "prepublishOnly": "npm run build"
26
+ "prepublishOnly": "npm run build && npm --prefix apps/console install && npm --prefix apps/console run build"
24
27
  },
25
28
  "dependencies": {
26
29
  "@aws-sdk/client-bedrock": "^3.1021.0",