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
package/bin/leedab.js ADDED
@@ -0,0 +1,626 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { createRequire } from "node:module";
4
+ import { existsSync } from "node:fs";
5
+ import { resolve } from "node:path";
6
+ import { program } from "commander";
7
+ import chalk from "chalk";
8
+ import ora from "ora";
9
+ import { startGateway, stopGateway } from "../dist/gateway.js";
10
+ import { loadConfig, initConfig } from "../dist/config/index.js";
11
+ import { setupChannels } from "../dist/channels/index.js";
12
+ import { runOnboard } from "../dist/onboard/index.js";
13
+ import { startDashboard } from "../dist/dashboard/server.js";
14
+ import { execBranded } from "../dist/brand.js";
15
+ import { resolveOpenClawBin, openclawEnv } from "../dist/openclaw.js";
16
+ import { addEntry, removeEntry, listEntries, encryptVault, decryptVault } from "../dist/vault.js";
17
+ import { loadTeam, addMember, removeMember } from "../dist/team.js";
18
+ import { ensureLicense } from "../dist/license.js";
19
+
20
+ const pkg = createRequire(import.meta.url)("../package.json");
21
+ const OPENCLAW_BIN = resolveOpenClawBin();
22
+ const STATE_DIR = resolve(".leedab");
23
+ const ocEnv = openclawEnv(STATE_DIR);
24
+
25
+ /** Commands that don't require an existing project directory or license. */
26
+ const NO_PROJECT_REQUIRED = new Set(["onboard", "init", "help"]);
27
+ /** Commands that need a project but not a license (onboard handles its own license check). */
28
+ const NO_LICENSE_REQUIRED = new Set(["onboard", "init", "help", "stop"]);
29
+
30
+ /**
31
+ * Check that we're inside a LeedAB project directory.
32
+ * Runs before every command except onboard/init.
33
+ */
34
+ function requireProject(command) {
35
+ const name = command.name();
36
+ // Also allow subcommands whose parent doesn't need a project
37
+ const parentName = command.parent?.name();
38
+ if (NO_PROJECT_REQUIRED.has(name) || NO_PROJECT_REQUIRED.has(parentName)) return;
39
+
40
+ const hasConfig = existsSync(resolve("leedab.config.json"));
41
+ const hasState = existsSync(STATE_DIR);
42
+ if (!hasConfig && !hasState) {
43
+ console.error(chalk.red("\n Not a LeedAB project directory.\n"));
44
+ console.error(chalk.dim(" Run ") + chalk.cyan("leedab onboard") + chalk.dim(" to set up a new project here,"));
45
+ console.error(chalk.dim(" or cd into an existing project directory.\n"));
46
+ process.exit(1);
47
+ }
48
+ }
49
+
50
+ /**
51
+ * Check for a valid license. Runs before commands that need one.
52
+ */
53
+ async function requireLicense(command) {
54
+ const name = command.name();
55
+ const parentName = command.parent?.name();
56
+ if (NO_LICENSE_REQUIRED.has(name) || NO_LICENSE_REQUIRED.has(parentName)) return;
57
+
58
+ // Only check if we're in a project (requireProject runs first)
59
+ const hasState = existsSync(STATE_DIR);
60
+ if (!hasState) return;
61
+
62
+ const license = await ensureLicense();
63
+ if (!license) {
64
+ console.error(chalk.red("\n Invalid or expired license.\n"));
65
+ console.error(chalk.dim(" Re-enter your key: ") + chalk.cyan("leedab onboard"));
66
+ console.error(chalk.dim(" Get a key at: ") + chalk.cyan("https://leedab.com\n"));
67
+ process.exit(1);
68
+ }
69
+ }
70
+
71
+ // Hook into every command automatically
72
+ program.hook("preAction", async (thisCommand, actionCommand) => {
73
+ requireProject(actionCommand);
74
+ await requireLicense(actionCommand);
75
+ });
76
+
77
+ program
78
+ .name("leedab")
79
+ .description("LeedAB — Your enterprise AI agent")
80
+ .version(pkg.version);
81
+
82
+ program
83
+ .command("onboard")
84
+ .description("Interactive setup wizard — connect channels and set up credential vault")
85
+ .action(async () => {
86
+ try {
87
+ await runOnboard();
88
+ } catch (err) {
89
+ console.error(chalk.red(`\nOnboard failed: ${err.message}`));
90
+ process.exit(1);
91
+ }
92
+ });
93
+
94
+ const configure = program
95
+ .command("configure")
96
+ .description("Reconfigure LeedAB settings");
97
+
98
+ configure
99
+ .command("model")
100
+ .description("Change the LLM provider and API key")
101
+ .action(async () => {
102
+ const { onboardProvider } = await import("../dist/onboard/steps/provider.js");
103
+ const result = await onboardProvider();
104
+ if (result) {
105
+ const { readFile, writeFile } = await import("node:fs/promises");
106
+ const configPath = resolve("leedab.config.json");
107
+ try {
108
+ const raw = JSON.parse(await readFile(configPath, "utf-8"));
109
+ raw.agent.model = result.model;
110
+ await writeFile(configPath, JSON.stringify(raw, null, 2) + "\n");
111
+ console.log(chalk.green(`\n Config updated. Restart with ${chalk.cyan("leedab start")} to apply.\n`));
112
+ } catch (err) {
113
+ console.error(chalk.red(`Failed to update config: ${err.message}`));
114
+ process.exit(1);
115
+ }
116
+ }
117
+ });
118
+
119
+ program
120
+ .command("init")
121
+ .description("Initialize LeedAB with default config (use 'onboard' for guided setup)")
122
+ .action(async () => {
123
+ const spinner = ora("Initializing LeedAB...").start();
124
+ try {
125
+ await initConfig();
126
+ spinner.succeed(chalk.green("LeedAB initialized. Edit leedab.config.json to configure."));
127
+ } catch (err) {
128
+ spinner.fail(chalk.red(`Init failed: ${err.message}`));
129
+ process.exit(1);
130
+ }
131
+ });
132
+
133
+ program
134
+ .command("start")
135
+ .description("Start the LeedAB agent and dashboard")
136
+ .option("-c, --config <path>", "Path to config file", "leedab.config.json")
137
+ .option("-p, --dashboard-port <port>", "Dashboard port", "3000")
138
+ .action(async (opts) => {
139
+ const spinner = ora("Starting LeedAB agent...").start();
140
+ try {
141
+ const config = await loadConfig(opts.config);
142
+ const { logPath } = await startGateway(config);
143
+ spinner.succeed(chalk.green("LeedAB is live"));
144
+ await startDashboard(config, parseInt(opts.dashboardPort));
145
+
146
+ const enabledChannels = Object.entries(config.channels)
147
+ .filter(([_, v]) => v?.enabled)
148
+ .map(([k]) => k);
149
+
150
+ console.log();
151
+ console.log(chalk.bold(" Ready to go."));
152
+ console.log();
153
+ const dashUrl = `http://localhost:${opts.dashboardPort}`;
154
+ const link = `\x1b]8;;${dashUrl}\x1b\\${dashUrl}\x1b]8;;\x1b\\`;
155
+ console.log(` ${chalk.cyan(link)} Dashboard`);
156
+ if (enabledChannels.length > 0) {
157
+ console.log(` ${chalk.green("Channels")} ${enabledChannels.join(", ")}`);
158
+ }
159
+ console.log();
160
+ console.log(chalk.dim(` Logs: ${logPath}`));
161
+ console.log();
162
+ } catch (err) {
163
+ spinner.fail(chalk.red(`Failed to start: ${err.message}`));
164
+ process.exit(1);
165
+ }
166
+ });
167
+
168
+ program
169
+ .command("dashboard")
170
+ .description("Open the setup dashboard (without starting the agent)")
171
+ .option("-c, --config <path>", "Path to config file", "leedab.config.json")
172
+ .option("-p, --port <port>", "Dashboard port", "3000")
173
+ .action(async (opts) => {
174
+ try {
175
+ const config = await loadConfig(opts.config);
176
+ console.log(chalk.bold("\n LeedAB Dashboard\n"));
177
+ await startDashboard(config, parseInt(opts.port));
178
+ console.log(chalk.dim(" Open the URL above in your browser.\n"));
179
+ } catch (err) {
180
+ console.error(chalk.red(`Failed: ${err.message}`));
181
+ process.exit(1);
182
+ }
183
+ });
184
+
185
+ program
186
+ .command("terminal")
187
+ .alias("os")
188
+ .description("Chat with the agent in the terminal")
189
+ .option("--session <key>", "Session key", "main")
190
+ .option("--thinking <level>", "Thinking level: off | low | medium | high")
191
+ .action(async (opts) => {
192
+ const { readFile } = await import("node:fs/promises");
193
+
194
+ // Read gateway token from openclaw.json so the TUI can authenticate
195
+ let termEnv = { ...ocEnv };
196
+ try {
197
+ const raw = JSON.parse(await readFile(resolve(STATE_DIR, "openclaw.json"), "utf-8"));
198
+ if (raw.gateway?.auth?.token) {
199
+ termEnv.OPENCLAW_GATEWAY_TOKEN = raw.gateway.auth.token;
200
+ }
201
+ } catch {}
202
+
203
+ const args = ["tui"];
204
+ if (opts.session) args.push("--session", opts.session);
205
+ if (opts.thinking) args.push("--thinking", opts.thinking);
206
+ const code = await execBranded(OPENCLAW_BIN, args, { env: termEnv });
207
+ process.exit(code);
208
+ });
209
+
210
+ program
211
+ .command("gateway")
212
+ .description("Run the LeedAB gateway (foreground)")
213
+ .action(async () => {
214
+ console.log(chalk.bold("\n LeedAB Gateway\n"));
215
+ const code = await execBranded(OPENCLAW_BIN, ["gateway", "run"], { env: ocEnv });
216
+ process.exit(code);
217
+ });
218
+
219
+ program
220
+ .command("stop")
221
+ .description("Stop the LeedAB agent")
222
+ .action(async () => {
223
+ await stopGateway();
224
+ console.log(chalk.green("LeedAB agent stopped."));
225
+ });
226
+
227
+ const pairing = program
228
+ .command("pairing")
229
+ .description("Manage channel pairing (approve users to message the agent)");
230
+
231
+ pairing
232
+ .command("approve <channel> <code>")
233
+ .description("Approve a pairing request (e.g. leedab pairing approve telegram 67D9348E)")
234
+ .action(async (channel, code) => {
235
+ const codeResult = await execBranded(OPENCLAW_BIN, ["pairing", "approve", channel, code], { env: ocEnv });
236
+ process.exit(codeResult);
237
+ });
238
+
239
+ pairing
240
+ .command("list")
241
+ .description("List pending pairing requests")
242
+ .action(async () => {
243
+ const code = await execBranded(OPENCLAW_BIN, ["pairing", "list"], { env: ocEnv });
244
+ process.exit(code);
245
+ });
246
+
247
+ pairing
248
+ .command("add <channel> <userId>")
249
+ .description("Add a user to a channel's allowlist (e.g. leedab pairing add telegram 6549960466)")
250
+ .action(async (channel, userId) => {
251
+ const { readFile, writeFile } = await import("node:fs/promises");
252
+ const { resolve } = await import("node:path");
253
+ const configPath = resolve(STATE_DIR, "openclaw.json");
254
+
255
+ try {
256
+ const config = JSON.parse(await readFile(configPath, "utf-8"));
257
+ if (!config.channels?.[channel]) {
258
+ console.error(chalk.red(`Channel "${channel}" not found in config.`));
259
+ process.exit(1);
260
+ }
261
+
262
+ if (!config.channels[channel].allowFrom) {
263
+ config.channels[channel].allowFrom = [];
264
+ }
265
+
266
+ if (config.channels[channel].allowFrom.includes(userId)) {
267
+ console.log(chalk.yellow(`User ${userId} is already allowed on ${channel}.`));
268
+ return;
269
+ }
270
+
271
+ config.channels[channel].allowFrom.push(userId);
272
+ config.channels[channel].dmPolicy = "allowlist";
273
+ await writeFile(configPath, JSON.stringify(config, null, 2) + "\n");
274
+ console.log(chalk.green(`Added ${chalk.bold(userId)} to ${channel} allowlist.`));
275
+
276
+ // Auto-restart gateway if running
277
+ try {
278
+ const { execFile: ef } = await import("node:child_process");
279
+ const { promisify: p } = await import("node:util");
280
+ await p(ef)(OPENCLAW_BIN, ["gateway", "health"], { env: ocEnv, timeout: 3000 });
281
+ // Gateway is running, restart it
282
+ const spin = ora("Restarting gateway...").start();
283
+ await p(ef)(OPENCLAW_BIN, ["gateway", "restart"], { env: ocEnv, timeout: 10000 });
284
+ spin.succeed(chalk.green("Gateway restarted. Change is live."));
285
+ } catch {
286
+ // Gateway not running, no restart needed
287
+ }
288
+ } catch (err) {
289
+ console.error(chalk.red(`Failed: ${err.message}`));
290
+ process.exit(1);
291
+ }
292
+ });
293
+
294
+ pairing
295
+ .command("remove <channel> <userId>")
296
+ .description("Remove a user from a channel's allowlist")
297
+ .action(async (channel, userId) => {
298
+ const { readFile, writeFile } = await import("node:fs/promises");
299
+ const { resolve } = await import("node:path");
300
+ const configPath = resolve(STATE_DIR, "openclaw.json");
301
+
302
+ try {
303
+ const config = JSON.parse(await readFile(configPath, "utf-8"));
304
+ const list = config.channels?.[channel]?.allowFrom;
305
+ if (!list || !list.includes(userId)) {
306
+ console.error(chalk.red(`User ${userId} not found in ${channel} allowlist.`));
307
+ process.exit(1);
308
+ }
309
+
310
+ config.channels[channel].allowFrom = list.filter((id) => id !== userId);
311
+ await writeFile(configPath, JSON.stringify(config, null, 2) + "\n");
312
+ console.log(chalk.green(`Removed ${chalk.bold(userId)} from ${channel} allowlist.`));
313
+
314
+ // Auto-restart gateway if running
315
+ try {
316
+ const { execFile: ef } = await import("node:child_process");
317
+ const { promisify: p } = await import("node:util");
318
+ await p(ef)(OPENCLAW_BIN, ["gateway", "health"], { env: ocEnv, timeout: 3000 });
319
+ const spin = ora("Restarting gateway...").start();
320
+ await p(ef)(OPENCLAW_BIN, ["gateway", "restart"], { env: ocEnv, timeout: 10000 });
321
+ spin.succeed(chalk.green("Gateway restarted. Change is live."));
322
+ } catch {
323
+ // Gateway not running, no restart needed
324
+ }
325
+ } catch (err) {
326
+ console.error(chalk.red(`Failed: ${err.message}`));
327
+ process.exit(1);
328
+ }
329
+ });
330
+
331
+ program
332
+ .command("channels")
333
+ .description("List configured channels and their status")
334
+ .option("-c, --config <path>", "Path to config file", "leedab.config.json")
335
+ .action(async (opts) => {
336
+ const config = await loadConfig(opts.config);
337
+ await setupChannels(config);
338
+ });
339
+
340
+ const sessions = program
341
+ .command("sessions")
342
+ .description("Manage conversation sessions");
343
+
344
+ sessions
345
+ .command("list")
346
+ .description("List all sessions")
347
+ .action(async () => {
348
+ const code = await execBranded(OPENCLAW_BIN, ["sessions", "--json"], { env: ocEnv });
349
+ // If branded exec didn't output, fallback
350
+ if (code !== 0) {
351
+ console.log(chalk.dim("No sessions found."));
352
+ }
353
+ });
354
+
355
+ sessions
356
+ .command("delete <key>")
357
+ .description("Delete a session by key (e.g. test21)")
358
+ .action(async (key) => {
359
+ const { readFile, writeFile, unlink } = await import("node:fs/promises");
360
+ const { resolve } = await import("node:path");
361
+ const storePath = resolve(STATE_DIR, "agents", "main", "sessions", "sessions.json");
362
+
363
+ let store;
364
+ try {
365
+ store = JSON.parse(await readFile(storePath, "utf-8"));
366
+ } catch {
367
+ console.error(chalk.red("No sessions store found."));
368
+ process.exit(1);
369
+ }
370
+
371
+ // Match by key suffix (user types "test21", actual key is "agent:main:test21")
372
+ const entries = Object.entries(store.sessions || {});
373
+ const match = entries.find(([k]) => k === key || k === `agent:main:${key}` || k.endsWith(`:${key}`));
374
+
375
+ if (!match) {
376
+ console.error(chalk.red(`No session found matching "${key}"`));
377
+ console.log(chalk.dim("Available: " + entries.map(([k]) => k.replace("agent:main:", "")).join(", ")));
378
+ process.exit(1);
379
+ }
380
+
381
+ const [fullKey, session] = match;
382
+
383
+ // Delete transcript file
384
+ if (session.sessionId) {
385
+ const transcript = resolve(STATE_DIR, "agents", "main", "sessions", `${session.sessionId}.jsonl`);
386
+ try { await unlink(transcript); } catch {}
387
+ }
388
+
389
+ // Remove from store
390
+ delete store.sessions[fullKey];
391
+ await writeFile(storePath, JSON.stringify(store, null, 2) + "\n");
392
+
393
+ console.log(chalk.green(`Deleted session ${chalk.bold(fullKey)}`));
394
+ });
395
+
396
+ sessions
397
+ .command("clear")
398
+ .description("Delete all sessions")
399
+ .action(async () => {
400
+ const { default: inquirer } = await import("inquirer");
401
+ const { confirm } = await inquirer.prompt([
402
+ { type: "confirm", name: "confirm", message: "Delete all sessions?", default: false },
403
+ ]);
404
+ if (!confirm) return;
405
+
406
+ const { readFile, writeFile, readdir, unlink } = await import("node:fs/promises");
407
+ const { resolve } = await import("node:path");
408
+ const sessionsDir = resolve(STATE_DIR, "agents", "main", "sessions");
409
+
410
+ // Delete all transcript files
411
+ try {
412
+ const files = await readdir(sessionsDir);
413
+ for (const f of files) {
414
+ if (f.endsWith(".jsonl")) {
415
+ await unlink(resolve(sessionsDir, f));
416
+ }
417
+ }
418
+ } catch {}
419
+
420
+ // Reset sessions store
421
+ const storePath = resolve(sessionsDir, "sessions.json");
422
+ try {
423
+ const store = JSON.parse(await readFile(storePath, "utf-8"));
424
+ store.sessions = {};
425
+ await writeFile(storePath, JSON.stringify(store, null, 2) + "\n");
426
+ } catch {}
427
+
428
+ console.log(chalk.green("All sessions deleted."));
429
+ });
430
+
431
+ const vault = program
432
+ .command("vault")
433
+ .description("Manage credentials the agent can use for browser login");
434
+
435
+ vault
436
+ .command("add <service>")
437
+ .description("Add or update credentials for a service")
438
+ .option("-u, --url <url>", "Login URL")
439
+ .option("-e, --email <email>", "Username or email")
440
+ .option("-p, --password <password>", "Password")
441
+ .option("-n, --notes <notes>", "Extra notes for the agent")
442
+ .action(async (service, opts) => {
443
+ if (!opts.email && !opts.password && !opts.url) {
444
+ console.error(chalk.red("Provide at least one of --email, --password, or --url"));
445
+ process.exit(1);
446
+ }
447
+ await addEntry(service, {
448
+ url: opts.url,
449
+ username: opts.email,
450
+ password: opts.password,
451
+ notes: opts.notes,
452
+ });
453
+ console.log(chalk.green(`Saved credentials for ${chalk.bold(service)}`));
454
+ });
455
+
456
+ vault
457
+ .command("list")
458
+ .description("List stored services (passwords hidden)")
459
+ .action(async () => {
460
+ const entries = await listEntries();
461
+ if (entries.length === 0) {
462
+ console.log(chalk.dim("No credentials stored. Use `leedab vault add <service>` to add some."));
463
+ return;
464
+ }
465
+ console.log(chalk.bold("\n Stored Credentials\n"));
466
+ for (const e of entries) {
467
+ console.log(` ${chalk.cyan(e.service)}${e.url ? chalk.dim(` — ${e.url}`) : ""}${e.username ? chalk.dim(` (${e.username})`) : ""}`);
468
+ }
469
+ console.log();
470
+ });
471
+
472
+ vault
473
+ .command("remove <service>")
474
+ .description("Remove credentials for a service")
475
+ .action(async (service) => {
476
+ const removed = await removeEntry(service);
477
+ if (removed) {
478
+ console.log(chalk.green(`Removed credentials for ${chalk.bold(service)}`));
479
+ } else {
480
+ console.error(chalk.red(`No credentials found for "${service}"`));
481
+ process.exit(1);
482
+ }
483
+ });
484
+
485
+ vault
486
+ .command("encrypt")
487
+ .description("Encrypt the vault with a master password")
488
+ .action(async () => {
489
+ const { default: inquirer } = await import("inquirer");
490
+ const { password } = await inquirer.prompt([
491
+ {
492
+ type: "password",
493
+ name: "password",
494
+ message: "Master password:",
495
+ mask: "*",
496
+ validate: (v) => v.length >= 8 || "Must be at least 8 characters",
497
+ },
498
+ ]);
499
+ const { confirm } = await inquirer.prompt([
500
+ { type: "password", name: "confirm", message: "Confirm password:", mask: "*" },
501
+ ]);
502
+ if (password !== confirm) {
503
+ console.error(chalk.red("Passwords do not match"));
504
+ process.exit(1);
505
+ }
506
+ await encryptVault(password);
507
+ console.log(chalk.green("Vault encrypted. Set LEEDAB_VAULT_KEY env var for auto-decrypt."));
508
+ });
509
+
510
+ vault
511
+ .command("decrypt")
512
+ .description("Decrypt the vault back to plaintext")
513
+ .action(async () => {
514
+ const { default: inquirer } = await import("inquirer");
515
+ const { password } = await inquirer.prompt([
516
+ { type: "password", name: "password", message: "Master password:", mask: "*" },
517
+ ]);
518
+ try {
519
+ await decryptVault(password);
520
+ console.log(chalk.green("Vault decrypted to plaintext."));
521
+ } catch (err) {
522
+ console.error(chalk.red(`Decryption failed: ${err.message}`));
523
+ process.exit(1);
524
+ }
525
+ });
526
+
527
+ const team = program
528
+ .command("team")
529
+ .description("Manage team members");
530
+
531
+ team
532
+ .command("list")
533
+ .description("List team members")
534
+ .action(async () => {
535
+ const members = await loadTeam();
536
+ if (members.length === 0) {
537
+ console.log(chalk.dim("No team members. Use `leedab team add <name>` to add someone."));
538
+ return;
539
+ }
540
+ console.log(chalk.bold("\n Team Members\n"));
541
+ for (const m of members) {
542
+ const role = m.role === "admin" ? chalk.hex("#AE5630")(m.role) : m.role === "operator" ? chalk.green(m.role) : chalk.dim(m.role);
543
+ console.log(` ${chalk.cyan(m.name)} — ${role}${m.email ? chalk.dim(` (${m.email})`) : ""}`);
544
+ }
545
+ console.log();
546
+ });
547
+
548
+ team
549
+ .command("add <name>")
550
+ .description("Add a team member")
551
+ .option("-e, --email <email>", "Email address")
552
+ .option("-r, --role <role>", "Role: admin, operator, viewer", "operator")
553
+ .action(async (name, opts) => {
554
+ const member = await addMember({
555
+ name,
556
+ email: opts.email,
557
+ role: opts.role,
558
+ channels: [],
559
+ });
560
+ console.log(chalk.green(`Added ${chalk.bold(name)} as ${opts.role} (${member.id})`));
561
+ });
562
+
563
+ team
564
+ .command("remove <id>")
565
+ .description("Remove a team member by ID")
566
+ .action(async (id) => {
567
+ const removed = await removeMember(id);
568
+ if (removed) {
569
+ console.log(chalk.green("Member removed."));
570
+ } else {
571
+ console.error(chalk.red(`No member found with ID "${id}"`));
572
+ process.exit(1);
573
+ }
574
+ });
575
+
576
+ // Default action: show status if in a project, nudge to onboard if not
577
+ if (process.argv.length <= 2) {
578
+ const hasProject = existsSync(resolve("leedab.config.json")) || existsSync(STATE_DIR);
579
+ if (!hasProject) {
580
+ console.log(chalk.bold("\n LeedAB") + chalk.dim(" — Your enterprise AI agent\n"));
581
+ console.log(chalk.dim(" Get started:\n"));
582
+ console.log(` ${chalk.cyan("leedab onboard")} Interactive setup wizard`);
583
+ console.log(` ${chalk.cyan("leedab init")} Quick init with defaults`);
584
+ console.log(` ${chalk.cyan("leedab --help")} All commands\n`);
585
+ process.exit(0);
586
+ } else {
587
+ // Show project status
588
+ (async () => {
589
+ try {
590
+ const config = await loadConfig("leedab.config.json");
591
+ const channels = Object.entries(config.channels)
592
+ .filter(([_, v]) => v?.enabled)
593
+ .map(([k]) => k);
594
+
595
+ // Check if gateway is running
596
+ let gatewayUp = false;
597
+ try {
598
+ const { execFile: ef } = await import("node:child_process");
599
+ const { promisify: p } = await import("node:util");
600
+ await p(ef)(OPENCLAW_BIN, ["gateway", "health"], { env: ocEnv, timeout: 3000 });
601
+ gatewayUp = true;
602
+ } catch {}
603
+
604
+ console.log(chalk.bold("\n LeedAB") + chalk.dim(` — ${config.agent.name}\n`));
605
+ console.log(` Status ${gatewayUp ? chalk.green("running") : chalk.yellow("stopped")}`);
606
+ console.log(` Model ${chalk.cyan(config.agent.model)}`);
607
+ if (channels.length > 0) {
608
+ console.log(` Channels ${channels.join(", ")}`);
609
+ }
610
+ console.log();
611
+ if (!gatewayUp) {
612
+ console.log(` ${chalk.cyan("leedab start")} Start the agent`);
613
+ } else {
614
+ console.log(` ${chalk.cyan("leedab terminal")} Chat with your agent`);
615
+ console.log(` ${chalk.cyan("leedab stop")} Stop the agent`);
616
+ }
617
+ console.log(` ${chalk.cyan("leedab --help")} All commands`);
618
+ console.log();
619
+ } catch {
620
+ program.parse();
621
+ }
622
+ })();
623
+ }
624
+ } else {
625
+ program.parse();
626
+ }
@@ -0,0 +1,20 @@
1
+ export interface AnalyticsSummary {
2
+ messagesPerDay: {
3
+ date: string;
4
+ count: number;
5
+ }[];
6
+ avgResponseTimeMs: number;
7
+ topQueries: {
8
+ query: string;
9
+ count: number;
10
+ }[];
11
+ channelBreakdown: {
12
+ channel: string;
13
+ count: number;
14
+ percentage: number;
15
+ }[];
16
+ activeUsers: number;
17
+ totalMessages: number;
18
+ periodDays: number;
19
+ }
20
+ export declare function getAnalytics(days?: number): Promise<AnalyticsSummary>;
@@ -0,0 +1,57 @@
1
+ import { readAuditLog } from "./audit.js";
2
+ export async function getAnalytics(days = 30) {
3
+ const since = new Date();
4
+ since.setDate(since.getDate() - days);
5
+ const entries = await readAuditLog({ since });
6
+ // Messages per day
7
+ const byDay = new Map();
8
+ for (const e of entries) {
9
+ const date = e.timestamp.slice(0, 10); // YYYY-MM-DD
10
+ byDay.set(date, (byDay.get(date) ?? 0) + 1);
11
+ }
12
+ const messagesPerDay = Array.from(byDay.entries())
13
+ .map(([date, count]) => ({ date, count }))
14
+ .sort((a, b) => a.date.localeCompare(b.date));
15
+ // Average response time
16
+ const withDuration = entries.filter((e) => e.durationMs != null);
17
+ const avgResponseTimeMs = withDuration.length > 0
18
+ ? Math.round(withDuration.reduce((sum, e) => sum + (e.durationMs ?? 0), 0) /
19
+ withDuration.length)
20
+ : 0;
21
+ // Top queries (by first 50 chars, deduplicated)
22
+ const queryMap = new Map();
23
+ for (const e of entries) {
24
+ if (e.query) {
25
+ const key = e.query.slice(0, 50).toLowerCase().trim();
26
+ queryMap.set(key, (queryMap.get(key) ?? 0) + 1);
27
+ }
28
+ }
29
+ const topQueries = Array.from(queryMap.entries())
30
+ .map(([query, count]) => ({ query, count }))
31
+ .sort((a, b) => b.count - a.count)
32
+ .slice(0, 10);
33
+ // Channel breakdown
34
+ const channelMap = new Map();
35
+ for (const e of entries) {
36
+ channelMap.set(e.channel, (channelMap.get(e.channel) ?? 0) + 1);
37
+ }
38
+ const total = entries.length || 1;
39
+ const channelBreakdown = Array.from(channelMap.entries())
40
+ .map(([channel, count]) => ({
41
+ channel,
42
+ count,
43
+ percentage: Math.round((count / total) * 100),
44
+ }))
45
+ .sort((a, b) => b.count - a.count);
46
+ // Active users
47
+ const users = new Set(entries.map((e) => e.user));
48
+ return {
49
+ messagesPerDay,
50
+ avgResponseTimeMs,
51
+ topQueries,
52
+ channelBreakdown,
53
+ activeUsers: users.size,
54
+ totalMessages: entries.length,
55
+ periodDays: days,
56
+ };
57
+ }