leedab 0.2.5 → 0.3.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/bin/leedab.js +38 -9
- package/dist/console-launcher.d.ts +11 -0
- package/dist/console-launcher.js +184 -0
- package/dist/gateway.js +43 -3
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/license.d.ts +2 -0
- package/dist/license.js +2 -0
- package/dist/onboard/steps/provider.d.ts +7 -1
- package/dist/onboard/steps/provider.js +25 -9
- package/package.json +3 -3
- package/dist/dashboard/routes.d.ts +0 -17
- package/dist/dashboard/routes.js +0 -777
- package/dist/dashboard/server.d.ts +0 -2
- package/dist/dashboard/server.js +0 -85
- package/dist/dashboard/static/admin.html +0 -695
- package/dist/dashboard/static/favicon.png +0 -0
- package/dist/dashboard/static/index.html +0 -936
- package/dist/dashboard/static/logo-dark.png +0 -0
- package/dist/dashboard/static/logo-light.png +0 -0
- package/dist/dashboard/static/sessions.html +0 -162
- package/dist/dashboard/static/style.css +0 -493
package/bin/leedab.js
CHANGED
|
@@ -11,7 +11,7 @@ import { startGateway, stopGateway } from "../dist/gateway.js";
|
|
|
11
11
|
import { loadConfig, initConfig } from "../dist/config/index.js";
|
|
12
12
|
import { setupChannels } from "../dist/channels/index.js";
|
|
13
13
|
import { runOnboard } from "../dist/onboard/index.js";
|
|
14
|
-
import {
|
|
14
|
+
import { startConsole, stopConsole } from "../dist/console-launcher.js";
|
|
15
15
|
import { execBranded } from "../dist/brand.js";
|
|
16
16
|
import { resolveOpenClawBin, openclawEnv } from "../dist/openclaw.js";
|
|
17
17
|
import { addEntry, removeEntry, listEntries, encryptVault, decryptVault } from "../dist/vault.js";
|
|
@@ -174,12 +174,38 @@ program
|
|
|
174
174
|
.option("-c, --config <path>", "Path to config file", resolve(STATE_DIR, "config.json"))
|
|
175
175
|
.option("-p, --dashboard-port <port>", "Dashboard port", "3000")
|
|
176
176
|
.action(async (opts) => {
|
|
177
|
+
const startupMessages = [
|
|
178
|
+
"Loading skills...",
|
|
179
|
+
"Waking up the agent...",
|
|
180
|
+
"Connecting channels...",
|
|
181
|
+
"Warming up the model...",
|
|
182
|
+
"Syncing memory...",
|
|
183
|
+
"Checking in with the gateway...",
|
|
184
|
+
"Polishing the SOUL...",
|
|
185
|
+
"Calibrating heartbeat...",
|
|
186
|
+
"Reading the workspace...",
|
|
187
|
+
"Tuning the agent's instincts...",
|
|
188
|
+
"Pouring the coffee...",
|
|
189
|
+
"Almost there...",
|
|
190
|
+
];
|
|
177
191
|
const spinner = ora("Starting LeedAB agent...").start();
|
|
192
|
+
let lastIdx = -1;
|
|
193
|
+
const rotate = setInterval(() => {
|
|
194
|
+
let next;
|
|
195
|
+
do {
|
|
196
|
+
next = Math.floor(Math.random() * startupMessages.length);
|
|
197
|
+
} while (next === lastIdx && startupMessages.length > 1);
|
|
198
|
+
lastIdx = next;
|
|
199
|
+
spinner.text = startupMessages[next];
|
|
200
|
+
}, 4500);
|
|
178
201
|
try {
|
|
179
202
|
const config = await loadConfig(opts.config);
|
|
180
203
|
const { logPath } = await startGateway(config);
|
|
204
|
+
clearInterval(rotate);
|
|
181
205
|
spinner.succeed(chalk.green("LeedAB is live"));
|
|
182
|
-
|
|
206
|
+
|
|
207
|
+
// Boot the Next.js console (apps/console).
|
|
208
|
+
const { logPath: consoleLogPath } = await startConsole(parseInt(opts.dashboardPort));
|
|
183
209
|
|
|
184
210
|
const enabledChannels = Object.entries(config.channels)
|
|
185
211
|
.filter(([_, v]) => v?.enabled)
|
|
@@ -190,14 +216,16 @@ program
|
|
|
190
216
|
console.log();
|
|
191
217
|
const dashUrl = `http://localhost:${opts.dashboardPort}`;
|
|
192
218
|
const link = `\x1b]8;;${dashUrl}\x1b\\${dashUrl}\x1b]8;;\x1b\\`;
|
|
193
|
-
console.log(` ${chalk.cyan(link)}
|
|
219
|
+
console.log(` ${chalk.cyan(link)} Console`);
|
|
194
220
|
if (enabledChannels.length > 0) {
|
|
195
221
|
console.log(` ${chalk.green("Channels")} ${enabledChannels.join(", ")}`);
|
|
196
222
|
}
|
|
197
223
|
console.log();
|
|
198
|
-
console.log(chalk.dim(`
|
|
224
|
+
console.log(chalk.dim(` Gateway log: ${logPath}`));
|
|
225
|
+
console.log(chalk.dim(` Console log: ${consoleLogPath}`));
|
|
199
226
|
console.log();
|
|
200
227
|
} catch (err) {
|
|
228
|
+
clearInterval(rotate);
|
|
201
229
|
spinner.fail(chalk.red(`Failed to start: ${err.message}`));
|
|
202
230
|
process.exit(1);
|
|
203
231
|
}
|
|
@@ -205,14 +233,14 @@ program
|
|
|
205
233
|
|
|
206
234
|
program
|
|
207
235
|
.command("dashboard")
|
|
208
|
-
.description("Open the
|
|
236
|
+
.description("Open the LeedAB console (without starting the agent)")
|
|
209
237
|
.option("-c, --config <path>", "Path to config file", resolve(STATE_DIR, "config.json"))
|
|
210
|
-
.option("-p, --port <port>", "
|
|
238
|
+
.option("-p, --port <port>", "Console port", "3000")
|
|
211
239
|
.action(async (opts) => {
|
|
212
240
|
try {
|
|
213
|
-
|
|
214
|
-
console.log(chalk.bold("\n LeedAB
|
|
215
|
-
await
|
|
241
|
+
await loadConfig(opts.config);
|
|
242
|
+
console.log(chalk.bold("\n LeedAB Console\n"));
|
|
243
|
+
await startConsole(parseInt(opts.port));
|
|
216
244
|
console.log(chalk.dim(" Open the URL above in your browser.\n"));
|
|
217
245
|
} catch (err) {
|
|
218
246
|
console.error(chalk.red(`Failed: ${err.message}`));
|
|
@@ -258,6 +286,7 @@ program
|
|
|
258
286
|
.command("stop")
|
|
259
287
|
.description("Stop the LeedAB agent")
|
|
260
288
|
.action(async () => {
|
|
289
|
+
await stopConsole().catch(() => {});
|
|
261
290
|
await stopGateway();
|
|
262
291
|
console.log(chalk.green("LeedAB agent stopped."));
|
|
263
292
|
});
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Start the console subprocess on the given port. Resolves once the
|
|
3
|
+
* subprocess is alive (we don't poll the port; `next start` exits on its
|
|
4
|
+
* own if the port is taken or the build is missing, so a process-error
|
|
5
|
+
* would surface there).
|
|
6
|
+
*/
|
|
7
|
+
export declare function startConsole(port?: number): Promise<{
|
|
8
|
+
url: string;
|
|
9
|
+
logPath: string;
|
|
10
|
+
}>;
|
|
11
|
+
export declare function stopConsole(): Promise<void>;
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
// Spawn the LeedAB console (Next.js app at apps/console) as a subprocess.
|
|
2
|
+
// `leedab start` and `leedab dashboard` use this to swap the legacy
|
|
3
|
+
// src/dashboard server for the modern Next.js admin UI.
|
|
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.
|
|
8
|
+
import { spawn } from "node:child_process";
|
|
9
|
+
import { createWriteStream, existsSync, readFileSync, unlinkSync, writeFileSync } from "node:fs";
|
|
10
|
+
import { mkdir } from "node:fs/promises";
|
|
11
|
+
import { resolve, dirname } from "node:path";
|
|
12
|
+
import { fileURLToPath } from "node:url";
|
|
13
|
+
import { STATE_DIR } from "./paths.js";
|
|
14
|
+
let consoleProcess = null;
|
|
15
|
+
// Cross-process handle for `leedab stop`. The leedab start process and the
|
|
16
|
+
// leedab stop process are different OS processes, so an in-memory ref to
|
|
17
|
+
// the spawned console isn't enough — stop reads the PID from this file.
|
|
18
|
+
const PID_FILE = resolve(STATE_DIR, "console.pid");
|
|
19
|
+
function writePidFile(pid) {
|
|
20
|
+
try {
|
|
21
|
+
writeFileSync(PID_FILE, String(pid), "utf8");
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
// best-effort — losing the pid file just means leedab stop falls back
|
|
25
|
+
// to the in-process handle, which is correct in the common case.
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
function readPidFile() {
|
|
29
|
+
try {
|
|
30
|
+
const raw = readFileSync(PID_FILE, "utf8").trim();
|
|
31
|
+
const pid = Number.parseInt(raw, 10);
|
|
32
|
+
return Number.isFinite(pid) && pid > 0 ? pid : null;
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
function clearPidFile() {
|
|
39
|
+
try {
|
|
40
|
+
unlinkSync(PID_FILE);
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
// already gone, fine
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
function isAlive(pid) {
|
|
47
|
+
try {
|
|
48
|
+
process.kill(pid, 0);
|
|
49
|
+
return true;
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
return false;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
/** Walk up from this module to find the leedab repo root. */
|
|
56
|
+
function findRepoRoot() {
|
|
57
|
+
let dir = dirname(fileURLToPath(import.meta.url));
|
|
58
|
+
for (let i = 0; i < 10; i++) {
|
|
59
|
+
const pkgPath = resolve(dir, "package.json");
|
|
60
|
+
if (existsSync(pkgPath)) {
|
|
61
|
+
try {
|
|
62
|
+
const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
|
|
63
|
+
if (pkg.name === "leedab")
|
|
64
|
+
return dir;
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
// ignore unreadable / non-JSON
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
const parent = resolve(dir, "..");
|
|
71
|
+
if (parent === dir)
|
|
72
|
+
break;
|
|
73
|
+
dir = parent;
|
|
74
|
+
}
|
|
75
|
+
throw new Error("Could not locate the leedab repo root from the console launcher.");
|
|
76
|
+
}
|
|
77
|
+
function consoleDir() {
|
|
78
|
+
return resolve(findRepoRoot(), "apps", "console");
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Start the console subprocess on the given port. Resolves once the
|
|
82
|
+
* subprocess is alive (we don't poll the port; `next start` exits on its
|
|
83
|
+
* own if the port is taken or the build is missing, so a process-error
|
|
84
|
+
* would surface there).
|
|
85
|
+
*/
|
|
86
|
+
export async function startConsole(port = 3000) {
|
|
87
|
+
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.`);
|
|
97
|
+
}
|
|
98
|
+
const logDir = resolve(STATE_DIR, "logs");
|
|
99
|
+
await mkdir(logDir, { recursive: true });
|
|
100
|
+
const today = new Date().toISOString().slice(0, 10);
|
|
101
|
+
const logPath = resolve(logDir, `console-${today}.log`);
|
|
102
|
+
const logStream = createWriteStream(logPath, { flags: "a" });
|
|
103
|
+
// If a previous run left a stale pid, clear it before spawning so a
|
|
104
|
+
// failed start doesn't leave us pointing at a dead process.
|
|
105
|
+
const stale = readPidFile();
|
|
106
|
+
if (stale && !isAlive(stale))
|
|
107
|
+
clearPidFile();
|
|
108
|
+
consoleProcess = spawn(nextBin, ["start", "-H", "127.0.0.1", "-p", String(port)], {
|
|
109
|
+
cwd: dir,
|
|
110
|
+
env: {
|
|
111
|
+
...process.env,
|
|
112
|
+
LEEDAB_STATE_DIR: STATE_DIR,
|
|
113
|
+
NODE_ENV: "production",
|
|
114
|
+
},
|
|
115
|
+
stdio: ["inherit", "pipe", "pipe"],
|
|
116
|
+
});
|
|
117
|
+
if (consoleProcess.pid)
|
|
118
|
+
writePidFile(consoleProcess.pid);
|
|
119
|
+
if (consoleProcess.stdout)
|
|
120
|
+
consoleProcess.stdout.pipe(logStream);
|
|
121
|
+
if (consoleProcess.stderr)
|
|
122
|
+
consoleProcess.stderr.pipe(logStream);
|
|
123
|
+
consoleProcess.on("error", (err) => {
|
|
124
|
+
console.error(`Console process error: ${err.message}`);
|
|
125
|
+
});
|
|
126
|
+
consoleProcess.on("exit", () => {
|
|
127
|
+
// If the subprocess crashed or was killed externally, drop the pid
|
|
128
|
+
// file so the next stop/start sequence isn't confused.
|
|
129
|
+
clearPidFile();
|
|
130
|
+
});
|
|
131
|
+
// Clean up on parent SIGTERM/SIGINT alongside the gateway.
|
|
132
|
+
const cleanup = () => {
|
|
133
|
+
if (consoleProcess) {
|
|
134
|
+
consoleProcess.kill("SIGTERM");
|
|
135
|
+
consoleProcess = null;
|
|
136
|
+
}
|
|
137
|
+
clearPidFile();
|
|
138
|
+
};
|
|
139
|
+
process.once("SIGTERM", cleanup);
|
|
140
|
+
process.once("SIGINT", cleanup);
|
|
141
|
+
return { url: `http://localhost:${port}`, logPath };
|
|
142
|
+
}
|
|
143
|
+
export async function stopConsole() {
|
|
144
|
+
// Same-process path first: if we hold the handle, kill directly.
|
|
145
|
+
if (consoleProcess) {
|
|
146
|
+
consoleProcess.kill("SIGTERM");
|
|
147
|
+
consoleProcess = null;
|
|
148
|
+
clearPidFile();
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
// Cross-process path (`leedab stop` from a different terminal): signal
|
|
152
|
+
// the PID we wrote at start time. If it's already gone or was reaped,
|
|
153
|
+
// clear the stale file and return cleanly.
|
|
154
|
+
const pid = readPidFile();
|
|
155
|
+
if (!pid)
|
|
156
|
+
return;
|
|
157
|
+
if (!isAlive(pid)) {
|
|
158
|
+
clearPidFile();
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
try {
|
|
162
|
+
process.kill(pid, "SIGTERM");
|
|
163
|
+
}
|
|
164
|
+
catch {
|
|
165
|
+
// already gone in the race window — fine.
|
|
166
|
+
}
|
|
167
|
+
// Give next a moment to flush + exit; if it's still alive after the
|
|
168
|
+
// grace window, escalate to SIGKILL so the operator isn't left with a
|
|
169
|
+
// zombie they have to hunt with `lsof`.
|
|
170
|
+
for (let i = 0; i < 20; i++) {
|
|
171
|
+
if (!isAlive(pid))
|
|
172
|
+
break;
|
|
173
|
+
await new Promise((r) => setTimeout(r, 250));
|
|
174
|
+
}
|
|
175
|
+
if (isAlive(pid)) {
|
|
176
|
+
try {
|
|
177
|
+
process.kill(pid, "SIGKILL");
|
|
178
|
+
}
|
|
179
|
+
catch {
|
|
180
|
+
// already gone
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
clearPidFile();
|
|
184
|
+
}
|
package/dist/gateway.js
CHANGED
|
@@ -25,6 +25,10 @@ export async function startGateway(config) {
|
|
|
25
25
|
await seedWorkspace();
|
|
26
26
|
// Auto-approve all tool executions (enterprise local deployment)
|
|
27
27
|
await ensureAutoApprove(bin, env);
|
|
28
|
+
// Make sure the OpenAI-compat /v1/chat/completions HTTP endpoint is on.
|
|
29
|
+
// The Next.js console proxies every chat through it, and OpenClaw ships
|
|
30
|
+
// it disabled by default.
|
|
31
|
+
await ensureChatCompletionsEndpoint(stateDir);
|
|
28
32
|
// Register channels before starting gateway
|
|
29
33
|
await registerChannels(bin, stateDir, config);
|
|
30
34
|
// Start gateway, pipe output to log file instead of terminal
|
|
@@ -129,6 +133,41 @@ async function registerChannels(bin, stateDir, config) {
|
|
|
129
133
|
}
|
|
130
134
|
}
|
|
131
135
|
}
|
|
136
|
+
/**
|
|
137
|
+
* Idempotently flip `gateway.http.endpoints.chatCompletions.enabled` to true
|
|
138
|
+
* in ~/.leedab/openclaw.json. The console (Next.js, apps/console) proxies
|
|
139
|
+
* every chat call through that endpoint, and OpenClaw ships it disabled.
|
|
140
|
+
*
|
|
141
|
+
* Safe to run on every startGateway: if the file is missing (first run
|
|
142
|
+
* before onboard) we no-op; if the flag is already true we don't write.
|
|
143
|
+
*/
|
|
144
|
+
async function ensureChatCompletionsEndpoint(stateDir) {
|
|
145
|
+
const configPath = resolve(stateDir, "openclaw.json");
|
|
146
|
+
let raw;
|
|
147
|
+
try {
|
|
148
|
+
raw = await readFile(configPath, "utf-8");
|
|
149
|
+
}
|
|
150
|
+
catch {
|
|
151
|
+
// No openclaw.json yet — onboard hasn't run. Skip; openclaw will create
|
|
152
|
+
// the file and a subsequent `leedab start` will flip the flag.
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
let cfg;
|
|
156
|
+
try {
|
|
157
|
+
cfg = JSON.parse(raw);
|
|
158
|
+
}
|
|
159
|
+
catch {
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
cfg.gateway = cfg.gateway ?? {};
|
|
163
|
+
cfg.gateway.http = cfg.gateway.http ?? {};
|
|
164
|
+
cfg.gateway.http.endpoints = cfg.gateway.http.endpoints ?? {};
|
|
165
|
+
cfg.gateway.http.endpoints.chatCompletions = cfg.gateway.http.endpoints.chatCompletions ?? {};
|
|
166
|
+
if (cfg.gateway.http.endpoints.chatCompletions.enabled === true)
|
|
167
|
+
return;
|
|
168
|
+
cfg.gateway.http.endpoints.chatCompletions.enabled = true;
|
|
169
|
+
await writeFile(configPath, JSON.stringify(cfg, null, 2) + "\n");
|
|
170
|
+
}
|
|
132
171
|
/**
|
|
133
172
|
* Set up exec-approvals so the agent can run tools without prompting.
|
|
134
173
|
* Safe for local enterprise deployments where the admin controls the box.
|
|
@@ -198,16 +237,17 @@ async function seedWorkspace() {
|
|
|
198
237
|
console.warn("[leedab] workspace seed failed:", err);
|
|
199
238
|
}
|
|
200
239
|
}
|
|
201
|
-
async function waitForGateway(port, timeoutMs =
|
|
240
|
+
async function waitForGateway(port, timeoutMs = 90000) {
|
|
202
241
|
const bin = resolveOpenClawBin();
|
|
203
242
|
const stateDir = STATE_DIR;
|
|
204
243
|
const start = Date.now();
|
|
205
244
|
while (Date.now() - start < timeoutMs) {
|
|
206
245
|
try {
|
|
207
|
-
// Use openclaw's own health check which knows the WS protocol
|
|
246
|
+
// Use openclaw's own health check which knows the WS protocol.
|
|
247
|
+
// Child timeout must exceed openclaw CLI cold-start time (~15s).
|
|
208
248
|
await execFileAsync(bin, ["gateway", "health"], {
|
|
209
249
|
env: openclawEnv(stateDir),
|
|
210
|
-
timeout:
|
|
250
|
+
timeout: 30000,
|
|
211
251
|
});
|
|
212
252
|
return;
|
|
213
253
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -2,5 +2,5 @@ export { startGateway, stopGateway } from "./gateway.js";
|
|
|
2
2
|
export { loadConfig, initConfig } from "./config/index.js";
|
|
3
3
|
export { setupChannels } from "./channels/index.js";
|
|
4
4
|
export { runOnboard } from "./onboard/index.js";
|
|
5
|
-
export {
|
|
5
|
+
export { startConsole, stopConsole } from "./console-launcher.js";
|
|
6
6
|
export type { LeedABConfig } from "./config/schema.js";
|
package/dist/index.js
CHANGED
|
@@ -2,4 +2,4 @@ export { startGateway, stopGateway } from "./gateway.js";
|
|
|
2
2
|
export { loadConfig, initConfig } from "./config/index.js";
|
|
3
3
|
export { setupChannels } from "./channels/index.js";
|
|
4
4
|
export { runOnboard } from "./onboard/index.js";
|
|
5
|
-
export {
|
|
5
|
+
export { startConsole, stopConsole } from "./console-launcher.js";
|
package/dist/license.d.ts
CHANGED
package/dist/license.js
CHANGED
|
@@ -38,6 +38,8 @@ export async function validateLicenseKey(key) {
|
|
|
38
38
|
seatsUsed: data.seats_used ?? 0,
|
|
39
39
|
maxSeats: data.max_seats ?? 1,
|
|
40
40
|
validatedAt: new Date().toISOString(),
|
|
41
|
+
expiresAt: data.expires_at ?? undefined,
|
|
42
|
+
signature: data.signature ?? undefined,
|
|
41
43
|
email: data.email ?? undefined,
|
|
42
44
|
name: data.name ?? undefined,
|
|
43
45
|
orgName: data.org_name ?? undefined,
|
|
@@ -5,6 +5,12 @@ export interface ProviderResult {
|
|
|
5
5
|
}
|
|
6
6
|
/**
|
|
7
7
|
* Write the chosen model into .leedab/openclaw.json so the gateway actually uses it.
|
|
8
|
+
*
|
|
9
|
+
* Allowlist also gets every sibling model for the chosen provider, so the console's
|
|
10
|
+
* per-agent defaultModel (which can vary across COO, Procurement, CX, etc.) is
|
|
11
|
+
* accepted without re-onboarding. A single-model allowlist forces every agent to
|
|
12
|
+
* the same backend and surfaces as "Model X is not allowed for agent main" the
|
|
13
|
+
* first time a workflow routes through a non-primary agent.
|
|
8
14
|
*/
|
|
9
|
-
export declare function updateOpenClawModel(openclawModel: string): Promise<void>;
|
|
15
|
+
export declare function updateOpenClawModel(openclawModel: string, providerValue?: string): Promise<void>;
|
|
10
16
|
export declare function onboardProvider(): Promise<ProviderResult | null>;
|
|
@@ -12,9 +12,9 @@ const PROVIDERS = [
|
|
|
12
12
|
name: "Anthropic (Claude)", value: "anthropic", flag: "--anthropic-api-key", envVar: "ANTHROPIC_API_KEY",
|
|
13
13
|
defaultModelIndex: 1,
|
|
14
14
|
models: [
|
|
15
|
-
{ name: "Claude Opus 4.
|
|
15
|
+
{ name: "Claude Opus 4.7 — most capable", value: "claude-opus-4-7", openclaw: "anthropic/claude-opus-4-7" },
|
|
16
16
|
{ name: "Claude Sonnet 4.6 — fast, capable (recommended)", value: "claude-sonnet-4-6", openclaw: "anthropic/claude-sonnet-4-6" },
|
|
17
|
-
{ name: "Claude Haiku 4.5 — fastest, cheapest", value: "claude-haiku-4-5", openclaw: "anthropic/claude-haiku-4-5" },
|
|
17
|
+
{ name: "Claude Haiku 4.5 — fastest, cheapest", value: "claude-haiku-4-5", openclaw: "anthropic/claude-haiku-4-5-20251001" },
|
|
18
18
|
],
|
|
19
19
|
},
|
|
20
20
|
{
|
|
@@ -55,8 +55,8 @@ const PROVIDERS = [
|
|
|
55
55
|
defaultModelIndex: 0,
|
|
56
56
|
models: [
|
|
57
57
|
{ name: "Claude Sonnet 4.6 via Bedrock", value: "anthropic.claude-sonnet-4-6", openclaw: "anthropic/claude-sonnet-4-6" },
|
|
58
|
-
{ name: "Claude Opus 4.
|
|
59
|
-
{ name: "Claude Haiku 4.5 via Bedrock", value: "anthropic.claude-haiku-4-5", openclaw: "anthropic/claude-haiku-4-5" },
|
|
58
|
+
{ name: "Claude Opus 4.7 via Bedrock", value: "anthropic.claude-opus-4-7", openclaw: "anthropic/claude-opus-4-7" },
|
|
59
|
+
{ name: "Claude Haiku 4.5 via Bedrock", value: "anthropic.claude-haiku-4-5", openclaw: "anthropic/claude-haiku-4-5-20251001" },
|
|
60
60
|
],
|
|
61
61
|
},
|
|
62
62
|
{
|
|
@@ -163,8 +163,14 @@ async function passKeyToOpenClaw(flag, apiKey) {
|
|
|
163
163
|
}
|
|
164
164
|
/**
|
|
165
165
|
* Write the chosen model into .leedab/openclaw.json so the gateway actually uses it.
|
|
166
|
+
*
|
|
167
|
+
* Allowlist also gets every sibling model for the chosen provider, so the console's
|
|
168
|
+
* per-agent defaultModel (which can vary across COO, Procurement, CX, etc.) is
|
|
169
|
+
* accepted without re-onboarding. A single-model allowlist forces every agent to
|
|
170
|
+
* the same backend and surfaces as "Model X is not allowed for agent main" the
|
|
171
|
+
* first time a workflow routes through a non-primary agent.
|
|
166
172
|
*/
|
|
167
|
-
export async function updateOpenClawModel(openclawModel) {
|
|
173
|
+
export async function updateOpenClawModel(openclawModel, providerValue) {
|
|
168
174
|
const configPath = resolve(STATE_DIR, "openclaw.json");
|
|
169
175
|
try {
|
|
170
176
|
const raw = JSON.parse(await readFile(configPath, "utf-8"));
|
|
@@ -173,7 +179,17 @@ export async function updateOpenClawModel(openclawModel) {
|
|
|
173
179
|
if (!raw.agents.defaults)
|
|
174
180
|
raw.agents.defaults = {};
|
|
175
181
|
raw.agents.defaults.model = { primary: openclawModel };
|
|
176
|
-
|
|
182
|
+
const allowed = new Set([openclawModel]);
|
|
183
|
+
if (providerValue) {
|
|
184
|
+
const provider = PROVIDERS.find(p => p.value === providerValue);
|
|
185
|
+
if (provider)
|
|
186
|
+
for (const m of provider.models)
|
|
187
|
+
allowed.add(m.openclaw);
|
|
188
|
+
}
|
|
189
|
+
const models = {};
|
|
190
|
+
for (const id of allowed)
|
|
191
|
+
models[id] = {};
|
|
192
|
+
raw.agents.defaults.models = models;
|
|
177
193
|
await writeFile(configPath, JSON.stringify(raw, null, 2) + "\n");
|
|
178
194
|
}
|
|
179
195
|
catch {
|
|
@@ -203,7 +219,7 @@ export async function onboardProvider() {
|
|
|
203
219
|
if (existing.provider.flag) {
|
|
204
220
|
await passKeyToOpenClaw(existing.provider.flag, apiKey);
|
|
205
221
|
}
|
|
206
|
-
await updateOpenClawModel(model.openclaw);
|
|
222
|
+
await updateOpenClawModel(model.openclaw, existing.provider.value);
|
|
207
223
|
console.log(chalk.green(` Using ${existing.provider.name}. Model: ${model.value}`));
|
|
208
224
|
return {
|
|
209
225
|
provider: existing.provider.value,
|
|
@@ -238,7 +254,7 @@ export async function onboardProvider() {
|
|
|
238
254
|
}
|
|
239
255
|
const bedrockProvider = PROVIDERS.find((p) => p.value === "bedrock");
|
|
240
256
|
const model = await pickModel(bedrockProvider);
|
|
241
|
-
await updateOpenClawModel(model.openclaw);
|
|
257
|
+
await updateOpenClawModel(model.openclaw, "bedrock");
|
|
242
258
|
return {
|
|
243
259
|
provider: "bedrock",
|
|
244
260
|
apiKey: "",
|
|
@@ -283,7 +299,7 @@ export async function onboardProvider() {
|
|
|
283
299
|
await passKeyToOpenClaw(selected.flag, trimmedKey);
|
|
284
300
|
}
|
|
285
301
|
const model = await pickModel(selected);
|
|
286
|
-
await updateOpenClawModel(model.openclaw);
|
|
302
|
+
await updateOpenClawModel(model.openclaw, selected.value);
|
|
287
303
|
console.log(chalk.green(`\n Using ${selected.name}. Model: ${model.value}`));
|
|
288
304
|
return {
|
|
289
305
|
provider: selected.value,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "leedab",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.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>",
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
"LICENSE"
|
|
17
17
|
],
|
|
18
18
|
"scripts": {
|
|
19
|
-
"build": "tsc && rm -rf dist/
|
|
19
|
+
"build": "tsc && rm -rf dist/templates && cp -r src/templates dist/templates",
|
|
20
20
|
"dev": "tsc --watch",
|
|
21
21
|
"start": "node bin/leedab.js",
|
|
22
22
|
"lint": "eslint src/",
|
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
"conf": "^13.0.0",
|
|
32
32
|
"grammy": "^1.41.1",
|
|
33
33
|
"inquirer": "^12.11.1",
|
|
34
|
-
"openclaw": "
|
|
34
|
+
"openclaw": "2026.4.15",
|
|
35
35
|
"ora": "^8.1.0"
|
|
36
36
|
},
|
|
37
37
|
"devDependencies": {
|
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
import { type IncomingMessage, type ServerResponse } from "node:http";
|
|
2
|
-
import type { LeedABConfig } from "../config/schema.js";
|
|
3
|
-
import { type ChannelName } from "../team/permissions.js";
|
|
4
|
-
type RouteHandler = (req: IncomingMessage, res: ServerResponse, url: URL) => Promise<void>;
|
|
5
|
-
export declare function createRoutes(config: LeedABConfig): Record<string, RouteHandler>;
|
|
6
|
-
/**
|
|
7
|
-
* Build the permission preamble injected into the agent prompt for a given
|
|
8
|
-
* channel message. Returns null when we have nothing to add (unknown user
|
|
9
|
-
* on a non-dashboard channel — the allowlist will already have rejected
|
|
10
|
-
* those).
|
|
11
|
-
*
|
|
12
|
-
* This preamble is the sole workflow-permission gate. We trust the agent to
|
|
13
|
-
* honor it. There is no server-side hard gate; admins restrict access by
|
|
14
|
-
* naming the workflows a member may use in the team page.
|
|
15
|
-
*/
|
|
16
|
-
export declare function buildPermissionPreamble(channel: ChannelName, userId: string): Promise<string | null>;
|
|
17
|
-
export {};
|