leedab 0.1.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.
Files changed (71) hide show
  1. package/LICENSE +6 -0
  2. package/README.md +85 -0
  3. package/bin/leedab.js +626 -0
  4. package/dist/analytics.d.ts +20 -0
  5. package/dist/analytics.js +57 -0
  6. package/dist/audit.d.ts +15 -0
  7. package/dist/audit.js +46 -0
  8. package/dist/brand.d.ts +9 -0
  9. package/dist/brand.js +57 -0
  10. package/dist/channels/index.d.ts +5 -0
  11. package/dist/channels/index.js +47 -0
  12. package/dist/config/index.d.ts +10 -0
  13. package/dist/config/index.js +49 -0
  14. package/dist/config/schema.d.ts +58 -0
  15. package/dist/config/schema.js +21 -0
  16. package/dist/dashboard/routes.d.ts +5 -0
  17. package/dist/dashboard/routes.js +410 -0
  18. package/dist/dashboard/server.d.ts +2 -0
  19. package/dist/dashboard/server.js +80 -0
  20. package/dist/dashboard/static/app.js +351 -0
  21. package/dist/dashboard/static/console.html +252 -0
  22. package/dist/dashboard/static/favicon.png +0 -0
  23. package/dist/dashboard/static/index.html +815 -0
  24. package/dist/dashboard/static/logo-dark.png +0 -0
  25. package/dist/dashboard/static/logo-light.png +0 -0
  26. package/dist/dashboard/static/sessions.html +182 -0
  27. package/dist/dashboard/static/settings.html +274 -0
  28. package/dist/dashboard/static/style.css +493 -0
  29. package/dist/dashboard/static/team.html +215 -0
  30. package/dist/gateway.d.ts +8 -0
  31. package/dist/gateway.js +213 -0
  32. package/dist/index.d.ts +6 -0
  33. package/dist/index.js +5 -0
  34. package/dist/license.d.ts +27 -0
  35. package/dist/license.js +92 -0
  36. package/dist/memory/index.d.ts +9 -0
  37. package/dist/memory/index.js +41 -0
  38. package/dist/onboard/index.d.ts +4 -0
  39. package/dist/onboard/index.js +263 -0
  40. package/dist/onboard/oauth-server.d.ts +13 -0
  41. package/dist/onboard/oauth-server.js +73 -0
  42. package/dist/onboard/steps/google.d.ts +12 -0
  43. package/dist/onboard/steps/google.js +178 -0
  44. package/dist/onboard/steps/provider.d.ts +10 -0
  45. package/dist/onboard/steps/provider.js +292 -0
  46. package/dist/onboard/steps/teams.d.ts +5 -0
  47. package/dist/onboard/steps/teams.js +51 -0
  48. package/dist/onboard/steps/telegram.d.ts +6 -0
  49. package/dist/onboard/steps/telegram.js +88 -0
  50. package/dist/onboard/steps/welcome.d.ts +1 -0
  51. package/dist/onboard/steps/welcome.js +10 -0
  52. package/dist/onboard/steps/whatsapp.d.ts +2 -0
  53. package/dist/onboard/steps/whatsapp.js +76 -0
  54. package/dist/openclaw.d.ts +9 -0
  55. package/dist/openclaw.js +20 -0
  56. package/dist/team.d.ts +13 -0
  57. package/dist/team.js +49 -0
  58. package/dist/templates/verticals/supply-chain/HEARTBEAT.md +12 -0
  59. package/dist/templates/verticals/supply-chain/SOUL.md +49 -0
  60. package/dist/templates/verticals/supply-chain/WORKFLOWS.md +148 -0
  61. package/dist/templates/verticals/supply-chain/vault-template.json +18 -0
  62. package/dist/templates/workspace/AGENTS.md +181 -0
  63. package/dist/templates/workspace/BOOTSTRAP.md +32 -0
  64. package/dist/templates/workspace/HEARTBEAT.md +9 -0
  65. package/dist/templates/workspace/IDENTITY.md +14 -0
  66. package/dist/templates/workspace/SOUL.md +32 -0
  67. package/dist/templates/workspace/TOOLS.md +40 -0
  68. package/dist/templates/workspace/USER.md +26 -0
  69. package/dist/vault.d.ts +24 -0
  70. package/dist/vault.js +123 -0
  71. package/package.json +58 -0
@@ -0,0 +1,410 @@
1
+ import { execFile, spawn } from "node:child_process";
2
+ import { readFile, writeFile, mkdir } from "node:fs/promises";
3
+ import { resolve } from "node:path";
4
+ import { promisify } from "node:util";
5
+ import { resolveOpenClawBin, openclawEnv } from "../openclaw.js";
6
+ import { addEntry, removeEntry, listEntries } from "../vault.js";
7
+ import { readAuditLog } from "../audit.js";
8
+ import { getAnalytics } from "../analytics.js";
9
+ import { loadTeam, addMember, removeMember, updateRole } from "../team.js";
10
+ const execFileAsync = promisify(execFile);
11
+ async function restartGatewayQuietly(stateDir) {
12
+ try {
13
+ const bin = resolveOpenClawBin();
14
+ const env = openclawEnv(stateDir);
15
+ await execFileAsync(bin, ["gateway", "health"], { env, timeout: 3000 });
16
+ await execFileAsync(bin, ["gateway", "restart"], { env, timeout: 10000 });
17
+ }
18
+ catch {
19
+ // Gateway not running, no restart needed
20
+ }
21
+ }
22
+ export function createRoutes(config) {
23
+ const bin = resolveOpenClawBin();
24
+ const stateDir = resolve(".leedab");
25
+ return {
26
+ /**
27
+ * POST /api/chat — send a message to the agent, get a response
28
+ */
29
+ "POST /api/chat": async (req, res) => {
30
+ const body = await readBody(req);
31
+ const { message, session } = JSON.parse(body);
32
+ if (!message) {
33
+ json(res, { error: "message is required" }, 400);
34
+ return;
35
+ }
36
+ try {
37
+ const args = [
38
+ "agent",
39
+ "--message", message,
40
+ "--session-id", session ?? "console",
41
+ "--json",
42
+ ];
43
+ const { stdout, stderr } = await execFileAsync(bin, args, {
44
+ env: openclawEnv(stateDir),
45
+ timeout: 120000,
46
+ });
47
+ try {
48
+ const result = JSON.parse(stdout);
49
+ // Extract text from various response shapes
50
+ const reply = result.result?.payloads?.[0]?.text ??
51
+ result.reply ??
52
+ result.text ??
53
+ result.content ??
54
+ stdout.trim();
55
+ json(res, { reply, session: session ?? "console" });
56
+ }
57
+ catch {
58
+ json(res, {
59
+ reply: stdout.trim() || stderr.trim() || "No response",
60
+ session: session ?? "console",
61
+ });
62
+ }
63
+ }
64
+ catch (err) {
65
+ json(res, { error: err.message }, 500);
66
+ }
67
+ },
68
+ /**
69
+ * GET /api/chat/history — get conversation history
70
+ */
71
+ "GET /api/chat/history": async (_req, res, url) => {
72
+ const session = url.searchParams.get("session") ?? "console";
73
+ try {
74
+ const { stdout } = await execFileAsync(bin, ["sessions", "history", "--session-id", session, "--json"], { env: openclawEnv(stateDir) });
75
+ json(res, JSON.parse(stdout));
76
+ }
77
+ catch {
78
+ json(res, []);
79
+ }
80
+ },
81
+ /**
82
+ * GET /api/status — channel health status
83
+ */
84
+ "GET /api/status": async (_req, res) => {
85
+ let secrets = {};
86
+ try {
87
+ const raw = await readFile(resolve(stateDir, "secrets.json"), "utf-8");
88
+ secrets = JSON.parse(raw);
89
+ }
90
+ catch { }
91
+ const status = {
92
+ whatsapp: {
93
+ connected: !!secrets.whatsapp?.connected || !!secrets.whatsapp?.accessToken,
94
+ label: "WhatsApp",
95
+ },
96
+ telegram: {
97
+ connected: !!secrets.telegram?.token,
98
+ label: "Telegram",
99
+ },
100
+ teams: {
101
+ connected: !!secrets.teams?.appSecret,
102
+ label: "Microsoft Teams",
103
+ },
104
+ };
105
+ json(res, status);
106
+ },
107
+ /**
108
+ * POST /api/whatsapp/connect — register channel and start QR pairing
109
+ */
110
+ "POST /api/whatsapp/connect": async (_req, res) => {
111
+ try {
112
+ // Register channel if not already added
113
+ await execFileAsync(bin, ["channels", "add", "--channel", "whatsapp"], {
114
+ env: openclawEnv(stateDir),
115
+ }).catch(() => { });
116
+ // Spawn login and capture output (including QR)
117
+ const login = spawn(bin, ["channels", "login", "--channel", "whatsapp"], {
118
+ env: {
119
+ ...openclawEnv(stateDir),
120
+ NO_COLOR: "1",
121
+ },
122
+ });
123
+ let output = "";
124
+ const qrReady = new Promise((resolve, reject) => {
125
+ const timeout = setTimeout(() => {
126
+ resolve(output);
127
+ }, 10000);
128
+ const onData = (chunk) => {
129
+ output += chunk.toString();
130
+ if (output.includes("▄▄██")) {
131
+ clearTimeout(timeout);
132
+ resolve(output);
133
+ }
134
+ };
135
+ login.stdout?.on("data", onData);
136
+ login.stderr?.on("data", onData);
137
+ login.on("error", reject);
138
+ });
139
+ const qrOutput = await qrReady;
140
+ // Mark connected when login completes
141
+ login.on("close", async (code) => {
142
+ if (code === 0) {
143
+ await mkdir(stateDir, { recursive: true });
144
+ let secrets = {};
145
+ try {
146
+ const raw = await readFile(resolve(stateDir, "secrets.json"), "utf-8");
147
+ secrets = JSON.parse(raw);
148
+ }
149
+ catch { }
150
+ secrets.whatsapp = { connected: true };
151
+ await writeFile(resolve(stateDir, "secrets.json"), JSON.stringify(secrets, null, 2) + "\n");
152
+ }
153
+ });
154
+ json(res, { status: "pairing", qr: qrOutput });
155
+ }
156
+ catch (err) {
157
+ json(res, { error: err.message }, 500);
158
+ }
159
+ },
160
+ /**
161
+ * GET /api/sessions — list all conversation sessions
162
+ */
163
+ "GET /api/sessions": async (_req, res) => {
164
+ try {
165
+ const { stdout } = await execFileAsync(bin, ["sessions", "--json"], { env: openclawEnv(stateDir), timeout: 10000 });
166
+ const parsed = JSON.parse(stdout);
167
+ const sessions = parsed.sessions ?? (Array.isArray(parsed) ? parsed : []);
168
+ json(res, sessions);
169
+ }
170
+ catch {
171
+ json(res, []);
172
+ }
173
+ },
174
+ /**
175
+ * GET /api/audit — recent audit log entries
176
+ */
177
+ "GET /api/audit": async (_req, res, url) => {
178
+ const limit = parseInt(url.searchParams.get("limit") ?? "50", 10);
179
+ const channel = url.searchParams.get("channel") ?? undefined;
180
+ const sinceParam = url.searchParams.get("since");
181
+ const since = sinceParam ? new Date(sinceParam) : undefined;
182
+ const entries = await readAuditLog({ limit, channel, since });
183
+ json(res, entries);
184
+ },
185
+ /**
186
+ * GET /api/analytics — usage analytics summary
187
+ */
188
+ "GET /api/analytics": async (_req, res, url) => {
189
+ const days = parseInt(url.searchParams.get("days") ?? "30", 10);
190
+ const summary = await getAnalytics(days);
191
+ json(res, summary);
192
+ },
193
+ /**
194
+ * GET /api/vault — list stored credentials (no passwords)
195
+ */
196
+ "GET /api/vault": async (_req, res) => {
197
+ const entries = await listEntries();
198
+ json(res, entries);
199
+ },
200
+ /**
201
+ * POST /api/vault — add a credential to the vault
202
+ */
203
+ "POST /api/vault": async (req, res) => {
204
+ const body = await readBody(req);
205
+ const { service, url, username, password, notes } = JSON.parse(body);
206
+ if (!service || typeof service !== "string") {
207
+ json(res, { error: "service name is required" }, 400);
208
+ return;
209
+ }
210
+ await addEntry(service.trim(), {
211
+ url: url || undefined,
212
+ username: username || undefined,
213
+ password: password || undefined,
214
+ notes: notes || undefined,
215
+ });
216
+ json(res, { ok: true, service: service.trim() });
217
+ },
218
+ /**
219
+ * POST /api/telegram/connect — validate token and save
220
+ */
221
+ "POST /api/telegram/connect": async (req, res) => {
222
+ const body = await readBody(req);
223
+ const { token } = JSON.parse(body);
224
+ if (!token || typeof token !== "string") {
225
+ json(res, { error: "token is required" }, 400);
226
+ return;
227
+ }
228
+ // Validate token via Telegram API
229
+ try {
230
+ const tgRes = await fetch(`https://api.telegram.org/bot${token.trim()}/getMe`);
231
+ const data = await tgRes.json();
232
+ if (!data.ok) {
233
+ json(res, { error: "Invalid bot token" }, 400);
234
+ return;
235
+ }
236
+ // Save to secrets
237
+ await mkdir(stateDir, { recursive: true });
238
+ let secrets = {};
239
+ try {
240
+ const raw = await readFile(resolve(stateDir, "secrets.json"), "utf-8");
241
+ secrets = JSON.parse(raw);
242
+ }
243
+ catch { }
244
+ secrets.telegram = { token: token.trim() };
245
+ await writeFile(resolve(stateDir, "secrets.json"), JSON.stringify(secrets, null, 2) + "\n");
246
+ json(res, { connected: true, username: data.result.username });
247
+ }
248
+ catch {
249
+ json(res, { error: "Could not reach Telegram API" }, 500);
250
+ }
251
+ },
252
+ /**
253
+ * GET /api/allowlist — get allowlist for a channel
254
+ * Expects ?channel=<name> query param
255
+ */
256
+ "GET /api/allowlist": async (_req, res, url) => {
257
+ const channel = url.searchParams.get("channel");
258
+ if (!channel) {
259
+ json(res, { error: "channel query param is required" }, 400);
260
+ return;
261
+ }
262
+ try {
263
+ const raw = await readFile(resolve(stateDir, "openclaw.json"), "utf-8");
264
+ const config = JSON.parse(raw);
265
+ const allowFrom = config.channels?.[channel]?.allowFrom ?? [];
266
+ const dmPolicy = config.channels?.[channel]?.dmPolicy ?? "pairing";
267
+ json(res, { channel, dmPolicy, allowFrom });
268
+ }
269
+ catch {
270
+ json(res, { channel, dmPolicy: "pairing", allowFrom: [] });
271
+ }
272
+ },
273
+ /**
274
+ * POST /api/allowlist — add a user to a channel's allowlist
275
+ */
276
+ "POST /api/allowlist": async (req, res) => {
277
+ const body = await readBody(req);
278
+ const { channel, userId } = JSON.parse(body);
279
+ if (!channel || !userId) {
280
+ json(res, { error: "channel and userId are required" }, 400);
281
+ return;
282
+ }
283
+ try {
284
+ const configPath = resolve(stateDir, "openclaw.json");
285
+ const config = JSON.parse(await readFile(configPath, "utf-8"));
286
+ if (!config.channels?.[channel]) {
287
+ json(res, { error: `Channel "${channel}" not configured` }, 400);
288
+ return;
289
+ }
290
+ if (!config.channels[channel].allowFrom) {
291
+ config.channels[channel].allowFrom = [];
292
+ }
293
+ if (!config.channels[channel].allowFrom.includes(userId)) {
294
+ config.channels[channel].allowFrom.push(userId);
295
+ }
296
+ config.channels[channel].dmPolicy = "allowlist";
297
+ await writeFile(configPath, JSON.stringify(config, null, 2) + "\n");
298
+ await restartGatewayQuietly(stateDir);
299
+ json(res, { ok: true });
300
+ }
301
+ catch (err) {
302
+ json(res, { error: err.message }, 500);
303
+ }
304
+ },
305
+ /**
306
+ * DELETE /api/allowlist — remove a user from a channel's allowlist
307
+ * Expects ?channel=<name>&userId=<id> query params
308
+ */
309
+ "DELETE /api/allowlist": async (_req, res, url) => {
310
+ const channel = url.searchParams.get("channel");
311
+ const userId = url.searchParams.get("userId");
312
+ if (!channel || !userId) {
313
+ json(res, { error: "channel and userId query params are required" }, 400);
314
+ return;
315
+ }
316
+ try {
317
+ const configPath = resolve(stateDir, "openclaw.json");
318
+ const config = JSON.parse(await readFile(configPath, "utf-8"));
319
+ const list = config.channels?.[channel]?.allowFrom;
320
+ if (!list) {
321
+ json(res, { ok: false, error: "No allowlist for this channel" }, 400);
322
+ return;
323
+ }
324
+ config.channels[channel].allowFrom = list.filter((id) => id !== userId);
325
+ await writeFile(configPath, JSON.stringify(config, null, 2) + "\n");
326
+ await restartGatewayQuietly(stateDir);
327
+ json(res, { ok: true });
328
+ }
329
+ catch (err) {
330
+ json(res, { error: err.message }, 500);
331
+ }
332
+ },
333
+ /**
334
+ * DELETE /api/vault — remove a credential from the vault
335
+ * Expects ?service=<name> query param
336
+ */
337
+ "DELETE /api/vault": async (_req, res, url) => {
338
+ const service = url.searchParams.get("service");
339
+ if (!service) {
340
+ json(res, { error: "service query param is required" }, 400);
341
+ return;
342
+ }
343
+ const removed = await removeEntry(service);
344
+ json(res, { ok: removed, service });
345
+ },
346
+ /**
347
+ * GET /api/team — list team members
348
+ */
349
+ "GET /api/team": async (_req, res) => {
350
+ const team = await loadTeam();
351
+ json(res, team);
352
+ },
353
+ /**
354
+ * POST /api/team — add a team member
355
+ */
356
+ "POST /api/team": async (req, res) => {
357
+ const body = await readBody(req);
358
+ const { name, email, role, channels } = JSON.parse(body);
359
+ if (!name || typeof name !== "string") {
360
+ json(res, { error: "name is required" }, 400);
361
+ return;
362
+ }
363
+ const member = await addMember({
364
+ name: name.trim(),
365
+ email: email || undefined,
366
+ role: role || "operator",
367
+ channels: channels || [],
368
+ });
369
+ json(res, member, 201);
370
+ },
371
+ /**
372
+ * DELETE /api/team — remove a team member
373
+ * Expects ?id=<uuid> query param
374
+ */
375
+ "DELETE /api/team": async (_req, res, url) => {
376
+ const id = url.searchParams.get("id");
377
+ if (!id) {
378
+ json(res, { error: "id query param is required" }, 400);
379
+ return;
380
+ }
381
+ const removed = await removeMember(id);
382
+ json(res, { ok: removed });
383
+ },
384
+ /**
385
+ * PUT /api/team/role — update a member's role
386
+ */
387
+ "PUT /api/team/role": async (req, res) => {
388
+ const body = await readBody(req);
389
+ const { id, role } = JSON.parse(body);
390
+ if (!id || !role) {
391
+ json(res, { error: "id and role are required" }, 400);
392
+ return;
393
+ }
394
+ const updated = await updateRole(id, role);
395
+ json(res, { ok: updated });
396
+ },
397
+ };
398
+ }
399
+ function json(res, data, status = 200) {
400
+ res.writeHead(status, { "Content-Type": "application/json" });
401
+ res.end(JSON.stringify(data));
402
+ }
403
+ function readBody(req) {
404
+ return new Promise((resolve, reject) => {
405
+ let body = "";
406
+ req.on("data", (chunk) => (body += chunk));
407
+ req.on("end", () => resolve(body));
408
+ req.on("error", reject);
409
+ });
410
+ }
@@ -0,0 +1,2 @@
1
+ import type { LeedABConfig } from "../config/schema.js";
2
+ export declare function startDashboard(config: LeedABConfig, port?: number): Promise<void>;
@@ -0,0 +1,80 @@
1
+ import { createServer } from "node:http";
2
+ import { readFile } from "node:fs/promises";
3
+ import { resolve, extname } from "node:path";
4
+ import { createRoutes } from "./routes.js";
5
+ const MIME_TYPES = {
6
+ ".html": "text/html",
7
+ ".css": "text/css",
8
+ ".js": "application/javascript",
9
+ ".json": "application/json",
10
+ ".svg": "image/svg+xml",
11
+ ".png": "image/png",
12
+ };
13
+ export async function startDashboard(config, port = 3000) {
14
+ const routes = createRoutes(config);
15
+ const staticDir = resolve(import.meta.dirname, "static");
16
+ const server = createServer(async (req, res) => {
17
+ const url = new URL(req.url ?? "/", `http://localhost:${port}`);
18
+ const method = req.method ?? "GET";
19
+ // API routes
20
+ if (url.pathname.startsWith("/api/")) {
21
+ const handler = routes[`${method} ${url.pathname}`];
22
+ if (handler) {
23
+ try {
24
+ await handler(req, res, url);
25
+ }
26
+ catch (err) {
27
+ res.writeHead(500, { "Content-Type": "application/json" });
28
+ res.end(JSON.stringify({ error: err.message }));
29
+ }
30
+ return;
31
+ }
32
+ res.writeHead(404, { "Content-Type": "application/json" });
33
+ res.end(JSON.stringify({ error: "not found" }));
34
+ return;
35
+ }
36
+ // Static files — try .html extension for clean URLs (/console -> /console.html)
37
+ let filePath = url.pathname === "/" ? "/index.html" : url.pathname;
38
+ if (!extname(filePath))
39
+ filePath += ".html";
40
+ try {
41
+ const fullPath = resolve(staticDir, filePath.slice(1));
42
+ const content = await readFile(fullPath);
43
+ const ext = extname(fullPath);
44
+ res.writeHead(200, { "Content-Type": MIME_TYPES[ext] ?? "text/plain" });
45
+ res.end(content);
46
+ }
47
+ catch {
48
+ // SPA fallback
49
+ try {
50
+ const index = await readFile(resolve(staticDir, "index.html"));
51
+ res.writeHead(200, { "Content-Type": "text/html" });
52
+ res.end(index);
53
+ }
54
+ catch {
55
+ res.writeHead(404);
56
+ res.end("Not found");
57
+ }
58
+ }
59
+ });
60
+ return new Promise((resolve, reject) => {
61
+ server.on("error", (err) => {
62
+ if (err.code === "EADDRINUSE") {
63
+ // Try next port
64
+ const nextPort = port + 1;
65
+ console.log(` Port ${port} in use, trying ${nextPort}...`);
66
+ server.listen(nextPort, "127.0.0.1");
67
+ server.once("listening", () => {
68
+ console.log(` Dashboard: http://localhost:${nextPort}`);
69
+ resolve();
70
+ });
71
+ }
72
+ else {
73
+ reject(err);
74
+ }
75
+ });
76
+ server.listen(port, "127.0.0.1", () => {
77
+ resolve();
78
+ });
79
+ });
80
+ }