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.
- package/LICENSE +6 -0
- package/README.md +85 -0
- package/bin/leedab.js +626 -0
- package/dist/analytics.d.ts +20 -0
- package/dist/analytics.js +57 -0
- package/dist/audit.d.ts +15 -0
- package/dist/audit.js +46 -0
- package/dist/brand.d.ts +9 -0
- package/dist/brand.js +57 -0
- package/dist/channels/index.d.ts +5 -0
- package/dist/channels/index.js +47 -0
- package/dist/config/index.d.ts +10 -0
- package/dist/config/index.js +49 -0
- package/dist/config/schema.d.ts +58 -0
- package/dist/config/schema.js +21 -0
- package/dist/dashboard/routes.d.ts +5 -0
- package/dist/dashboard/routes.js +410 -0
- package/dist/dashboard/server.d.ts +2 -0
- package/dist/dashboard/server.js +80 -0
- package/dist/dashboard/static/app.js +351 -0
- package/dist/dashboard/static/console.html +252 -0
- package/dist/dashboard/static/favicon.png +0 -0
- package/dist/dashboard/static/index.html +815 -0
- package/dist/dashboard/static/logo-dark.png +0 -0
- package/dist/dashboard/static/logo-light.png +0 -0
- package/dist/dashboard/static/sessions.html +182 -0
- package/dist/dashboard/static/settings.html +274 -0
- package/dist/dashboard/static/style.css +493 -0
- package/dist/dashboard/static/team.html +215 -0
- package/dist/gateway.d.ts +8 -0
- package/dist/gateway.js +213 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +5 -0
- package/dist/license.d.ts +27 -0
- package/dist/license.js +92 -0
- package/dist/memory/index.d.ts +9 -0
- package/dist/memory/index.js +41 -0
- package/dist/onboard/index.d.ts +4 -0
- package/dist/onboard/index.js +263 -0
- package/dist/onboard/oauth-server.d.ts +13 -0
- package/dist/onboard/oauth-server.js +73 -0
- package/dist/onboard/steps/google.d.ts +12 -0
- package/dist/onboard/steps/google.js +178 -0
- package/dist/onboard/steps/provider.d.ts +10 -0
- package/dist/onboard/steps/provider.js +292 -0
- package/dist/onboard/steps/teams.d.ts +5 -0
- package/dist/onboard/steps/teams.js +51 -0
- package/dist/onboard/steps/telegram.d.ts +6 -0
- package/dist/onboard/steps/telegram.js +88 -0
- package/dist/onboard/steps/welcome.d.ts +1 -0
- package/dist/onboard/steps/welcome.js +10 -0
- package/dist/onboard/steps/whatsapp.d.ts +2 -0
- package/dist/onboard/steps/whatsapp.js +76 -0
- package/dist/openclaw.d.ts +9 -0
- package/dist/openclaw.js +20 -0
- package/dist/team.d.ts +13 -0
- package/dist/team.js +49 -0
- package/dist/templates/verticals/supply-chain/HEARTBEAT.md +12 -0
- package/dist/templates/verticals/supply-chain/SOUL.md +49 -0
- package/dist/templates/verticals/supply-chain/WORKFLOWS.md +148 -0
- package/dist/templates/verticals/supply-chain/vault-template.json +18 -0
- package/dist/templates/workspace/AGENTS.md +181 -0
- package/dist/templates/workspace/BOOTSTRAP.md +32 -0
- package/dist/templates/workspace/HEARTBEAT.md +9 -0
- package/dist/templates/workspace/IDENTITY.md +14 -0
- package/dist/templates/workspace/SOUL.md +32 -0
- package/dist/templates/workspace/TOOLS.md +40 -0
- package/dist/templates/workspace/USER.md +26 -0
- package/dist/vault.d.ts +24 -0
- package/dist/vault.js +123 -0
- 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
|
+
}
|