bajaclaw 1.0.0 → 1.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/README.md CHANGED
@@ -53,15 +53,41 @@ Open the UI with `bajaclaw ui` (defaults to `http://127.0.0.1:18790`).
53
53
 
54
54
  ## Commands
55
55
 
56
+ **Run**
57
+
56
58
  | Command | What it does |
57
59
  |---|---|
58
- | `bajaclaw onboard` | First-run setup. ChatGPT by default; all other providers available. |
60
+ | `bajaclaw onboard` | Guided setup: model, fallbacks, local API, channels, features, daemon. |
59
61
  | `bajaclaw start` | Start everything (gateway + web UI + local API + channels). |
60
62
  | `bajaclaw stop` / `restart` | Control the daemon. |
61
- | `bajaclaw status` | Health at a glance. |
63
+ | `bajaclaw status` / `doctor` | Health and environment. |
62
64
  | `bajaclaw ui` | Open the web interface. |
63
- | `bajaclaw update` | Check upstream inspirations and write an approve-to-merge proposal. |
64
- | `bajaclaw doctor` | Environment and health checks. |
65
+ | `bajaclaw uninstall` | Remove the boot daemon. |
66
+
67
+ **Talk**
68
+
69
+ | Command | What it does |
70
+ |---|---|
71
+ | `bajaclaw ask "…"` | One-shot question, streamed to your terminal. |
72
+ | `bajaclaw chat` | Interactive terminal chat. |
73
+ | `bajaclaw cowork "…"` | Run a goal end-to-end and print the steps. |
74
+
75
+ **Manage**
76
+
77
+ | Command | What it does |
78
+ |---|---|
79
+ | `bajaclaw providers` | List providers and what is configured. |
80
+ | `bajaclaw login <id>` / `logout <id>` | Provider auth (ChatGPT OAuth or API key). |
81
+ | `bajaclaw channels [enable\|disable\|token] <id>` | Messaging channels. |
82
+ | `bajaclaw memory [query\|clear]` | Browse, search, or clear memory. |
83
+ | `bajaclaw skills` | Learned skills. |
84
+ | `bajaclaw update` | Check upstreams and write an approve-to-merge proposal. |
85
+ | `bajaclaw config [get\|set]` | View or change settings. |
86
+ | `bajaclaw logs` | Tail the daemon logs. |
87
+
88
+ The web dashboard mirrors most of this: set your default model and keys, enable
89
+ channels, search memory, run Cowork goals, toggle features, and watch live
90
+ activity.
65
91
 
66
92
  ## Models
67
93
 
Binary file
Binary file
Binary file
package/bin/bajaclaw.mjs CHANGED
@@ -1,23 +1,33 @@
1
1
  #!/usr/bin/env node
2
2
  // BajaClaw CLI. Branded, simple, one command to run everything.
3
3
  import { join } from "node:path";
4
+ import { readFileSync, existsSync } from "node:fs";
4
5
  import { createRequire } from "node:module";
6
+ import { createInterface } from "node:readline/promises";
7
+ import { stdin, stdout } from "node:process";
5
8
  import { color, glyph, wordmark, brandline, panel, spinner } from "../src/ui/theme.mjs";
6
9
  import { openUrl } from "../src/util.mjs";
7
- import { load, isOnboarded, CONFIG_DIR } from "../src/config/config.mjs";
10
+ import { load, save, isOnboarded, CONFIG_DIR, PROVIDERS } from "../src/config/config.mjs";
8
11
  import * as daemon from "../src/daemon/daemon.mjs";
9
12
  import { onboard, onboardNonInteractive } from "../src/onboarding/onboard.mjs";
10
13
  import { startEndpoint } from "../plugins/openai-endpoint/server.mjs";
11
14
  import { startUiServer } from "../src/daemon/uiserver.mjs";
12
15
  import { startGateway } from "../src/daemon/gateway.mjs";
13
16
  import { startChannels } from "../channels/manager.mjs";
14
- import { listReadyProviders } from "../src/agent/agent.mjs";
17
+ import { listReadyProviders, respond, streamRespond } from "../src/agent/agent.mjs";
18
+ import { REGISTRY, isConfigured } from "../src/llm/client.mjs";
19
+ import { saveCred, removeCred, hasCred } from "../src/auth/store.mjs";
20
+ import { login as chatgptLogin } from "../src/auth/chatgpt-oauth.mjs";
21
+ import * as brain from "../plugins/hermes-brain/store.mjs";
22
+ import { runOutcome } from "../plugins/cowork-mode/flow.mjs";
15
23
  import { check as checkUpdates } from "../plugins/self-updater/updater.mjs";
16
24
 
17
25
  const PKG_VERSION = createRequire(import.meta.url)("../package.json").version;
18
26
  const argv = process.argv.slice(2);
19
27
  const cmd = argv[0];
28
+ const rest = argv.slice(1);
20
29
  const flags = new Set(argv.filter((a) => a.startsWith("-")));
30
+ const positional = rest.filter((a) => !a.startsWith("-"));
21
31
 
22
32
  function ok(s) { console.log(`${glyph.ok} ${s}`); }
23
33
  function info(s) { console.log(`${glyph.info} ${s}`); }
@@ -26,32 +36,47 @@ function err(s) { console.log(`${glyph.err} ${color.red(s)}`); }
26
36
 
27
37
  function help() {
28
38
  console.log("\n" + wordmark() + "\n");
29
- console.log(panel("Commands", [
30
- `${color.bold("bajaclaw start")} ${color.dim("start everything (gateway daemon + OpenAI endpoint)")}`,
31
- `${color.bold("bajaclaw stop")} ${color.dim("stop the daemon")}`,
32
- `${color.bold("bajaclaw restart")} ${color.dim("restart the daemon")}`,
33
- `${color.bold("bajaclaw status")} ${color.dim("show health")}`,
34
- `${color.bold("bajaclaw onboard")} ${color.dim("first-run setup (ChatGPT by default, all LLMs available)")}`,
39
+ console.log(panel("Run", [
40
+ `${color.bold("bajaclaw onboard")} ${color.dim("guided first-run setup (model, channels, features)")}`,
41
+ `${color.bold("bajaclaw start")} ${color.dim("start everything (gateway + UI + local API + channels)")}`,
42
+ `${color.bold("bajaclaw stop")} / ${color.bold("restart")} ${color.dim("control the daemon")}`,
43
+ `${color.bold("bajaclaw status")} / ${color.bold("doctor")} ${color.dim("health and environment")}`,
35
44
  `${color.bold("bajaclaw ui")} ${color.dim("open the web interface")}`,
36
- `${color.bold("bajaclaw update")} ${color.dim("check upstreams, write an approve-to-merge proposal")}`,
37
- `${color.bold("bajaclaw doctor")} ${color.dim("environment + health checks")}`,
45
+ `${color.bold("bajaclaw uninstall")} ${color.dim("remove the boot daemon")}`,
46
+ ]) + "\n");
47
+ console.log(panel("Talk", [
48
+ `${color.bold("bajaclaw ask")} "…" ${color.dim("one-shot question")}`,
49
+ `${color.bold("bajaclaw chat")} ${color.dim("interactive terminal chat")}`,
50
+ `${color.bold("bajaclaw cowork")} "…" ${color.dim("run a goal end-to-end")}`,
51
+ ]) + "\n");
52
+ console.log(panel("Manage", [
53
+ `${color.bold("bajaclaw providers")} ${color.dim("list models/providers and what is configured")}`,
54
+ `${color.bold("bajaclaw login")} <id> / ${color.bold("logout")} <id> ${color.dim("provider auth")}`,
55
+ `${color.bold("bajaclaw channels")} [enable|disable|token] ${color.dim("messaging channels")}`,
56
+ `${color.bold("bajaclaw memory")} [query|clear] ${color.dim("browse/search/clear memory")}`,
57
+ `${color.bold("bajaclaw skills")} ${color.dim("learned skills")}`,
58
+ `${color.bold("bajaclaw update")} ${color.dim("check upstreams, write a proposal")}`,
59
+ `${color.bold("bajaclaw config")} [get|set] ${color.dim("view or change settings")}`,
60
+ `${color.bold("bajaclaw logs")} ${color.dim("tail the daemon logs")}`,
38
61
  ]) + "\n");
39
62
  console.log(color.dim(` config: ${CONFIG_DIR}`) + "\n");
40
63
  }
41
64
 
65
+ function openUiUrl() {
66
+ const cfg = load();
67
+ const url = `http://${cfg.ui.host}:${cfg.ui.port}/`;
68
+ info(`Opening ${url}`);
69
+ if (!openUrl(url)) warn(`Open it manually: ${url}`);
70
+ }
71
+
42
72
  async function cmdStart() {
43
73
  console.log("\n" + brandline("start") + "\n");
44
- if (!isOnboarded()) {
45
- info("First run - writing default config (ChatGPT). Run 'bajaclaw onboard' to sign in.");
46
- onboardNonInteractive();
47
- }
74
+ if (!isOnboarded()) { info("First run - writing default config (ChatGPT). Run 'bajaclaw onboard' to sign in."); onboardNonInteractive(); }
48
75
  const sp = spinner("starting gateway daemon").start();
49
76
  const res = daemon.start();
50
77
  sp.stop(glyph.ok, `gateway daemon up (${res.method})`);
51
78
  const cfg = load();
52
- if (cfg.openaiEndpoint.enabled) {
53
- info(`OpenAI endpoint will serve at ${color.bold(`http://${cfg.openaiEndpoint.host}:${cfg.openaiEndpoint.port}/v1`)}`);
54
- }
79
+ if (cfg.openaiEndpoint.enabled) info(`OpenAI endpoint: ${color.bold(`http://${cfg.openaiEndpoint.host}:${cfg.openaiEndpoint.port}/v1`)}`);
55
80
  info(`Web UI: ${color.bold(`http://${cfg.ui.host}:${cfg.ui.port}/`)}`);
56
81
  const ready = listReadyProviders();
57
82
  if (!ready.length) warn("No model configured yet. Run 'bajaclaw onboard' to sign in with ChatGPT (or another provider).");
@@ -59,15 +84,8 @@ async function cmdStart() {
59
84
  ok("BajaClaw started. After a reboot, just run 'bajaclaw start' again.");
60
85
  }
61
86
 
62
- function cmdStop() {
63
- daemon.stop();
64
- ok("Stopped.");
65
- }
66
-
67
- async function cmdRestart() {
68
- daemon.stop();
69
- await cmdStart();
70
- }
87
+ function cmdStop() { daemon.stop(); ok("Stopped."); }
88
+ async function cmdRestart() { daemon.stop(); await cmdStart(); }
71
89
 
72
90
  function cmdStatus() {
73
91
  const s = daemon.status();
@@ -82,64 +100,201 @@ function cmdStatus() {
82
100
  }
83
101
 
84
102
  function cmdDoctor() {
85
- const checks = [];
86
- const nodeOk = Number(process.versions.node.split(".")[0]) >= 22;
87
103
  const ready = listReadyProviders();
88
- checks.push([nodeOk, `Node ${process.versions.node} (need >=22.19)`]);
89
- checks.push([ready.length > 0, `A model is configured (${ready.join(", ") || "none yet"})`]);
90
- checks.push([isOnboarded(), "Onboarded (config present)"]);
91
- checks.push([process.platform === "darwin", `Platform ${process.platform} (launchd daemon = macOS)`]);
92
- console.log("\n" + panel("doctor", checks.map(([good, label]) =>
93
- `${good ? glyph.ok : glyph.warn} ${label}`)) + "\n");
104
+ const checks = [
105
+ [Number(process.versions.node.split(".")[0]) >= 22, `Node ${process.versions.node} (need >=22.19)`],
106
+ [ready.length > 0, `A model is configured (${ready.join(", ") || "none yet"})`],
107
+ [isOnboarded(), "Onboarded (config present)"],
108
+ [process.platform === "darwin", `Platform ${process.platform} (launchd daemon = macOS)`],
109
+ ];
110
+ console.log("\n" + panel("doctor", checks.map(([g, l]) => `${g ? glyph.ok : glyph.warn} ${l}`)) + "\n");
94
111
  }
95
112
 
96
113
  async function cmdUpdate() {
97
- const cfg = load();
98
114
  const sp = spinner("checking OpenClaw / Hermes / Cowork upstreams").start();
99
- const r = await checkUpdates(cfg, { write: !flags.has("--check") });
100
- sp.stop(glyph.ok, `checked ${Object.keys(r.raw).length} sources`);
115
+ const r = await checkUpdates(load(), { write: !flags.has("--check") });
116
+ sp.stop(glyph.ok, `checked sources`);
101
117
  if (!r.findings.length) { ok("Everything up to date."); return; }
102
- console.log("\n" + panel(`${r.findings.length} update(s)`, r.findings.map((f) =>
103
- `${glyph.arrow} ${f.label}: ${color.dim(f.from)} ${color.bold(color.amber(f.to))}`)) + "\n");
104
- if (r.proposalPath) ok(`Proposal written → ${color.dim(r.proposalPath)} (review & approve; nothing auto-merged)`);
118
+ console.log("\n" + panel(`${r.findings.length} update(s)`, r.findings.map((f) => `${glyph.arrow} ${f.label}: ${color.dim(f.from)} -> ${color.bold(color.amber(f.to))}`)) + "\n");
119
+ if (r.proposalPath) ok(`Proposal written -> ${color.dim(r.proposalPath)}`);
120
+ }
121
+
122
+ // ---- talk ----
123
+ async function streamToStdout(messages) {
124
+ let any = false;
125
+ for await (const c of streamRespond(messages)) {
126
+ if (c.delta) { process.stdout.write(c.delta); any = true; }
127
+ else if (c.text) process.stdout.write(c.text);
128
+ }
129
+ process.stdout.write("\n");
130
+ return any;
131
+ }
132
+ async function cmdAsk() {
133
+ const prompt = positional.join(" ").trim();
134
+ if (!prompt) { err('usage: bajaclaw ask "your question"'); process.exitCode = 1; return; }
135
+ await streamToStdout([{ role: "user", content: prompt }]);
136
+ }
137
+ async function cmdChat() {
138
+ console.log("\n" + brandline("chat") + color.dim(" (type 'exit' to quit)") + "\n");
139
+ const rl = createInterface({ input: stdin, output: stdout });
140
+ const history = [];
141
+ try {
142
+ for (;;) {
143
+ const line = (await rl.question(color.amber("you ") + color.dim("› "))).trim();
144
+ if (!line || line === "exit" || line === "quit") break;
145
+ history.push({ role: "user", content: line });
146
+ process.stdout.write(color.teal("bajaclaw ") + color.dim("› "));
147
+ let acc = "";
148
+ for await (const c of streamRespond(history)) {
149
+ if (c.delta) { process.stdout.write(c.delta); acc += c.delta; }
150
+ else if (c.text) { process.stdout.write(c.text); acc += c.text; }
151
+ }
152
+ process.stdout.write("\n\n");
153
+ history.push({ role: "assistant", content: acc });
154
+ }
155
+ } finally { rl.close(); }
156
+ }
157
+ async function cmdCowork() {
158
+ const goal = positional.join(" ").trim();
159
+ if (!goal) { err('usage: bajaclaw cowork "your goal"'); process.exitCode = 1; return; }
160
+ const sp = spinner(`working: ${goal}`).start();
161
+ const result = await runOutcome(goal, {
162
+ execute: async (step) => { const { text } = await respond([{ role: "user", content: `${step.title}: ${step.detail}` }]); return { ok: true, note: text.slice(0, 500) }; },
163
+ });
164
+ sp.stop(glyph.ok, `outcome ${result.status}`);
165
+ console.log("\n" + panel(goal, result.results.map((r) => `${glyph.arrow} ${color.bold(r.step.title)}\n ${color.dim(r.result?.note || "")}`)) + "\n");
105
166
  }
106
167
 
107
- function cmdUi() {
168
+ // ---- manage ----
169
+ function cmdProviders() {
108
170
  const cfg = load();
109
- const url = `http://${cfg.ui.host}:${cfg.ui.port}/`;
110
- info(`Opening ${url}`);
111
- if (!openUrl(url)) warn(`Open it manually: ${url}`);
171
+ const rows = PROVIDERS.map((p) => {
172
+ const conf = isConfigured(p.id);
173
+ const def = cfg.defaultProvider === p.id;
174
+ const tag = def ? color.amber("default") : conf ? color.teal("ready ") : color.dim("not set");
175
+ return `${glyph.bullet} ${tag} ${p.label} ${color.dim(REGISTRY[p.id]?.defaultModel || "")}`;
176
+ });
177
+ console.log("\n" + panel("Providers", rows) + "\n");
178
+ }
179
+
180
+ async function cmdLogin() {
181
+ const id = positional[0];
182
+ if (!id || !REGISTRY[id]) { err(`usage: bajaclaw login <provider> (${PROVIDERS.map((p) => p.id).join(", ")})`); process.exitCode = 1; return; }
183
+ if (id === "openai-codex") {
184
+ try { await chatgptLogin({ print: (m) => console.log(color.dim(m)) }); ok("Signed in with ChatGPT."); }
185
+ catch (e) { err(`sign-in failed: ${e.message}`); process.exitCode = 1; }
186
+ return;
187
+ }
188
+ const meta = PROVIDERS.find((p) => p.id === id);
189
+ if (meta?.kind === "local") { ok(`${meta.label} is local; no key needed.`); return; }
190
+ const rl = createInterface({ input: stdin, output: stdout });
191
+ try {
192
+ const key = (await rl.question(`Paste your ${meta?.label || id} API key: `)).trim();
193
+ if (!key) { warn("No key entered."); return; }
194
+ saveCred(id, { type: "key", api_key: key });
195
+ ok(`Saved ${id} key.`);
196
+ } finally { rl.close(); }
197
+ }
198
+ function cmdLogout() {
199
+ const id = positional[0];
200
+ if (!id) { err("usage: bajaclaw logout <provider>"); process.exitCode = 1; return; }
201
+ if (!hasCred(id)) { warn(`${id} was not signed in.`); return; }
202
+ removeCred(id);
203
+ ok(`Logged out of ${id}.`);
204
+ }
205
+
206
+ function cmdChannels() {
207
+ const cfg = load();
208
+ const sub = positional[0];
209
+ if (!sub) {
210
+ const rows = Object.entries(cfg.channels).map(([id, c]) => `${glyph.bullet} ${c.enabled ? color.teal("on ") : color.dim("off")} ${id} ${c.token ? color.dim("token set") : color.dim("no token")}`);
211
+ console.log("\n" + panel("Channels", rows) + "\n");
212
+ info("bajaclaw channels enable|disable <id> · bajaclaw channels token <id> <token>");
213
+ return;
214
+ }
215
+ const id = positional[1];
216
+ if (!id || !(id in cfg.channels)) { err(`unknown channel: ${id}`); process.exitCode = 1; return; }
217
+ if (sub === "enable" || sub === "disable") { cfg.channels[id].enabled = sub === "enable"; save(cfg); ok(`${id} ${sub}d. Run 'bajaclaw restart' to apply.`); }
218
+ else if (sub === "token") { const tok = positional[2]; if (!tok) { err("usage: bajaclaw channels token <id> <token>"); return; } cfg.channels[id].token = tok; save(cfg); ok(`${id} token set. Run 'bajaclaw restart' to apply.`); }
219
+ else { err(`unknown subcommand: ${sub}`); process.exitCode = 1; }
220
+ }
221
+
222
+ function cmdMemory() {
223
+ const sub = positional[0];
224
+ if (sub === "clear") { brain.clear(); ok("Memory cleared."); return; }
225
+ const items = sub ? brain.recall(positional.join(" "), { limit: 20 }) : brain.all().slice(-20).reverse();
226
+ if (!items.length) { info("No memory yet."); return; }
227
+ console.log("\n" + panel(sub ? `Memory: "${positional.join(" ")}"` : "Recent memory", items.map((m) => `${glyph.bullet} ${m.task} ${color.dim("- " + (m.outcome || "").slice(0, 60))}`)) + "\n");
228
+ }
229
+
230
+ function cmdSkills() {
231
+ const skills = brain.synthesizeSkills({ minSuccesses: 2 });
232
+ if (!skills.length) { info("No learned skills yet. Repeat a kind of task a few times."); return; }
233
+ console.log("\n" + panel("Learned skills", skills.map((s) => `${glyph.bullet} ${color.bold(s.name)} ${color.dim(`(${s.from}x) ` + s.summary)}`)) + "\n");
234
+ }
235
+
236
+ function cmdConfig() {
237
+ const cfg = load();
238
+ const sub = positional[0];
239
+ if (!sub) {
240
+ const redacted = structuredClone(cfg);
241
+ for (const id of Object.keys(redacted.channels || {})) if (redacted.channels[id].token) redacted.channels[id].token = "***";
242
+ console.log(JSON.stringify(redacted, null, 2));
243
+ return;
244
+ }
245
+ const at = (obj, path) => path.split(".").reduce((o, k) => (o == null ? o : o[k]), obj);
246
+ if (sub === "get") { const v = at(cfg, positional[1] || ""); console.log(typeof v === "object" ? JSON.stringify(v, null, 2) : String(v)); return; }
247
+ if (sub === "set") {
248
+ const key = positional[1]; let val = positional.slice(2).join(" ");
249
+ if (!key) { err("usage: bajaclaw config set <key> <value>"); process.exitCode = 1; return; }
250
+ if (val === "true") val = true; else if (val === "false") val = false; else if (/^-?\d+$/.test(val)) val = parseInt(val, 10);
251
+ const parts = key.split("."); let o = cfg;
252
+ for (let i = 0; i < parts.length - 1; i++) o = o[parts[i]] ??= {};
253
+ o[parts[parts.length - 1]] = val;
254
+ save(cfg);
255
+ ok(`Set ${key} = ${val}. Run 'bajaclaw restart' if it affects the daemon.`);
256
+ return;
257
+ }
258
+ err(`unknown subcommand: ${sub}`); process.exitCode = 1;
112
259
  }
113
260
 
114
- // Internal: the supervised process the daemon runs. Boots gateway + endpoint.
261
+ function cmdLogs() {
262
+ const out = join(CONFIG_DIR, "logs", "gateway.out.log");
263
+ const errp = join(CONFIG_DIR, "logs", "gateway.err.log");
264
+ for (const [label, f] of [["out", out], ["err", errp]]) {
265
+ if (!existsSync(f)) continue;
266
+ const lines = readFileSync(f, "utf8").trim().split("\n");
267
+ if (!lines[0]) continue;
268
+ console.log("\n" + color.dim(`--- ${label} (last 30) ---`));
269
+ console.log(lines.slice(-30).join("\n"));
270
+ }
271
+ console.log("");
272
+ }
273
+
274
+ function cmdUninstall() {
275
+ daemon.uninstall();
276
+ ok("Daemon removed. (Config in ~/.bajaclaw and the npm package are untouched; 'npm rm -g bajaclaw' to uninstall the CLI.)");
277
+ }
278
+
279
+ // Internal: the supervised process the daemon runs. Boots gateway + endpoint + ui + channels.
115
280
  async function cmdServe() {
116
281
  const cfg = load();
117
- // 1) Native gateway: health + live event stream for the web UI.
118
282
  startGateway(cfg, {
283
+ version: PKG_VERSION,
119
284
  onListen: (g) => console.log(`[bajaclaw] gateway on http://${g.host}:${g.port}/`),
120
285
  status: () => ({ models: listReadyProviders() }),
121
286
  });
122
- // 2) Local OpenAI-compatible endpoint (this agent as a local LLM).
123
- if (cfg.openaiEndpoint.enabled) {
124
- startEndpoint(cfg, { onListen: (ep) => console.log(`[bajaclaw] openai endpoint on http://${ep.host}:${ep.port}/v1`) });
125
- }
126
- // 3) Web UI (served from web/dist).
287
+ if (cfg.openaiEndpoint.enabled) startEndpoint(cfg, { onListen: (ep) => console.log(`[bajaclaw] openai endpoint on http://${ep.host}:${ep.port}/v1`) });
127
288
  const dist = join(import.meta.dirname, "..", "web", "dist");
128
- startUiServer({ dist, host: cfg.ui.host, port: cfg.ui.port,
129
- onListen: (u) => console.log(`[bajaclaw] web ui on http://${u.host}:${u.port}/`) });
130
- // 4) Native channels (Telegram/Discord/... when enabled in config).
289
+ startUiServer({ dist, host: cfg.ui.host, port: cfg.ui.port, onListen: (u) => console.log(`[bajaclaw] web ui on http://${u.host}:${u.port}/`) });
131
290
  startChannels({ log: console.log }).catch((e) => console.log(`[channels] ${e.message}`));
132
- // Keep the supervised process alive.
133
291
  process.stdin.resume();
134
292
  }
135
293
 
136
294
  async function main() {
137
295
  if (flags.has("--version") || flags.has("-v") || cmd === "version") return console.log(PKG_VERSION);
138
296
  switch (cmd) {
139
- case undefined:
140
- case "help":
141
- case "--help":
142
- case "-h": return help();
297
+ case undefined: case "help": case "--help": case "-h": return help();
143
298
  case "onboard": return void (await onboard({ yes: flags.has("--yes") || flags.has("-y") }));
144
299
  case "start": return await cmdStart();
145
300
  case "stop": return cmdStop();
@@ -147,12 +302,22 @@ async function main() {
147
302
  case "status": return cmdStatus();
148
303
  case "doctor": return cmdDoctor();
149
304
  case "update": return await cmdUpdate();
150
- case "ui": return cmdUi();
305
+ case "ui": return openUiUrl();
306
+ case "uninstall": return cmdUninstall();
307
+ case "ask": return await cmdAsk();
308
+ case "chat": return await cmdChat();
309
+ case "cowork": return await cmdCowork();
310
+ case "providers": case "models": return cmdProviders();
311
+ case "login": return await cmdLogin();
312
+ case "logout": return cmdLogout();
313
+ case "channels": return cmdChannels();
314
+ case "memory": return cmdMemory();
315
+ case "skills": return cmdSkills();
316
+ case "config": return cmdConfig();
317
+ case "logs": return cmdLogs();
151
318
  case "_serve": return await cmdServe();
152
319
  default:
153
- err(`unknown command: ${cmd}`);
154
- help();
155
- process.exitCode = 1;
320
+ err(`unknown command: ${cmd}`); help(); process.exitCode = 1;
156
321
  }
157
322
  }
158
323
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bajaclaw",
3
- "version": "1.0.0",
3
+ "version": "1.1.0",
4
4
  "description": "All-in-one personal AI agent — OpenClaw spine + Hermes brain + Cowork outcome mode, with ChatGPT-subscription login, a local OpenAI-compatible endpoint, and a clean web UI.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -37,6 +37,13 @@ export function all() {
37
37
  return _cache;
38
38
  }
39
39
 
40
+ export function clear() {
41
+ ensure();
42
+ writeFileSync(MEM, "");
43
+ _cache = [];
44
+ _cacheKey = statKey();
45
+ }
46
+
40
47
  export function remember({ task, outcome, success, tags = [], at } = {}) {
41
48
  ensure();
42
49
  const rec = {
@@ -0,0 +1,196 @@
1
+ // Local control API for the web dashboard. Binds to localhost via the gateway.
2
+ // JSON over HTTP. No secrets are returned (keys/tokens are write-only).
3
+ import { load, save, PROVIDERS } from "../config/config.mjs";
4
+ import { REGISTRY, isConfigured } from "../llm/client.mjs";
5
+ import { saveCred, loadCred, removeCred, hasCred } from "../auth/store.mjs";
6
+ import { listReadyProviders } from "../agent/agent.mjs";
7
+ import * as brain from "../../plugins/hermes-brain/store.mjs";
8
+ import { runOutcome } from "../../plugins/cowork-mode/flow.mjs";
9
+ import { check as checkUpdates } from "../../plugins/self-updater/updater.mjs";
10
+
11
+ function send(res, code, obj) {
12
+ const body = JSON.stringify(obj);
13
+ res.writeHead(code, { "content-type": "application/json", "access-control-allow-origin": "*" });
14
+ res.end(body);
15
+ }
16
+ function readJson(req) {
17
+ return new Promise((resolve) => {
18
+ const chunks = [];
19
+ req.on("data", (c) => chunks.push(c));
20
+ req.on("end", () => { try { resolve(chunks.length ? JSON.parse(Buffer.concat(chunks).toString()) : {}); } catch { resolve({}); } });
21
+ req.on("error", () => resolve({}));
22
+ });
23
+ }
24
+
25
+ // Config without secrets (channel tokens redacted to a boolean).
26
+ function publicConfig(cfg) {
27
+ const c = structuredClone(cfg);
28
+ for (const id of Object.keys(c.channels || {})) {
29
+ const has = !!c.channels[id].token;
30
+ if ("token" in c.channels[id]) c.channels[id].token = undefined;
31
+ c.channels[id].hasToken = has;
32
+ }
33
+ return c;
34
+ }
35
+
36
+ function providerList(cfg) {
37
+ return PROVIDERS.map((p) => ({
38
+ id: p.id,
39
+ label: p.label,
40
+ kind: p.kind,
41
+ recommended: !!p.recommended,
42
+ configured: isConfigured(p.id),
43
+ isDefault: cfg.defaultProvider === p.id,
44
+ defaultModel: REGISTRY[p.id]?.defaultModel || null,
45
+ }));
46
+ }
47
+
48
+ function channelList(cfg) {
49
+ const native = new Set(["telegram", "discord"]);
50
+ return Object.entries(cfg.channels || {}).map(([id, c]) => ({
51
+ id,
52
+ enabled: !!c.enabled,
53
+ hasToken: !!c.token,
54
+ native: native.has(id),
55
+ }));
56
+ }
57
+
58
+ async function reloadChannels() {
59
+ try {
60
+ const mgr = await import("../../channels/manager.mjs");
61
+ mgr.stopChannels();
62
+ return await mgr.startChannels({ log: () => {} });
63
+ } catch { return []; }
64
+ }
65
+
66
+ // Merge only allow-listed config fields from a PATCH body.
67
+ function applyConfigPatch(cfg, body) {
68
+ const set = (path, val) => {
69
+ const parts = path.split(".");
70
+ let o = cfg;
71
+ for (let i = 0; i < parts.length - 1; i++) o = o[parts[i]] ??= {};
72
+ o[parts[parts.length - 1]] = val;
73
+ };
74
+ if (typeof body.defaultProvider === "string" && REGISTRY[body.defaultProvider]) {
75
+ set("defaultProvider", body.defaultProvider);
76
+ cfg.providerOrder = [body.defaultProvider, ...(cfg.providerOrder || []).filter((p) => p !== body.defaultProvider)];
77
+ }
78
+ if (Array.isArray(body.providerOrder)) set("providerOrder", body.providerOrder.filter((p) => REGISTRY[p]));
79
+ if (body.openaiEndpoint) {
80
+ if (typeof body.openaiEndpoint.enabled === "boolean") set("openaiEndpoint.enabled", body.openaiEndpoint.enabled);
81
+ if (["agent", "raw"].includes(body.openaiEndpoint.mode)) set("openaiEndpoint.mode", body.openaiEndpoint.mode);
82
+ if (Number.isInteger(body.openaiEndpoint.port)) set("openaiEndpoint.port", body.openaiEndpoint.port);
83
+ }
84
+ if (body.selfUpdate) {
85
+ if (typeof body.selfUpdate.enabled === "boolean") set("selfUpdate.enabled", body.selfUpdate.enabled);
86
+ if (["propose", "sandbox", "notify"].includes(body.selfUpdate.mode)) set("selfUpdate.mode", body.selfUpdate.mode);
87
+ }
88
+ if (body.features) {
89
+ if (typeof body.features.hermesBrain === "boolean") set("features.hermesBrain", body.features.hermesBrain);
90
+ if (typeof body.features.coworkMode === "boolean") set("features.coworkMode", body.features.coworkMode);
91
+ }
92
+ return cfg;
93
+ }
94
+
95
+ // Returns true if it handled the request.
96
+ export async function handleApi(req, res, url, ctx = {}) {
97
+ const p = url.pathname;
98
+ if (!p.startsWith("/api/")) return false;
99
+ const method = req.method;
100
+ const cfg = load();
101
+
102
+ try {
103
+ if (p === "/api/status" && method === "GET") {
104
+ send(res, 200, {
105
+ version: ctx.version || null,
106
+ ready: listReadyProviders(),
107
+ defaultProvider: cfg.defaultProvider,
108
+ ports: { gateway: cfg.gateway.port, ui: cfg.ui.port, openaiEndpoint: cfg.openaiEndpoint.port },
109
+ endpointMode: cfg.openaiEndpoint.mode,
110
+ channels: channelList(cfg).filter((c) => c.enabled).map((c) => c.id),
111
+ memoryCount: (() => { try { return brain.all().length; } catch { return 0; } })(),
112
+ });
113
+ return true;
114
+ }
115
+
116
+ if (p === "/api/config" && method === "GET") { send(res, 200, publicConfig(cfg)); return true; }
117
+ if (p === "/api/config" && (method === "PATCH" || method === "POST")) {
118
+ const body = await readJson(req);
119
+ save(applyConfigPatch(cfg, body));
120
+ send(res, 200, { ok: true, config: publicConfig(load()) });
121
+ return true;
122
+ }
123
+
124
+ if (p === "/api/providers" && method === "GET") { send(res, 200, { providers: providerList(cfg) }); return true; }
125
+ let m;
126
+ if ((m = p.match(/^\/api\/providers\/([\w-]+)\/key$/)) && method === "PUT") {
127
+ const body = await readJson(req);
128
+ if (!REGISTRY[m[1]]) { send(res, 404, { error: "unknown provider" }); return true; }
129
+ if (!body.key) { send(res, 400, { error: "missing key" }); return true; }
130
+ saveCred(m[1], { type: "key", api_key: String(body.key) });
131
+ send(res, 200, { ok: true });
132
+ return true;
133
+ }
134
+ if ((m = p.match(/^\/api\/providers\/([\w-]+)$/)) && method === "DELETE") {
135
+ removeCred(m[1]);
136
+ send(res, 200, { ok: true });
137
+ return true;
138
+ }
139
+
140
+ if (p === "/api/channels" && method === "GET") { send(res, 200, { channels: channelList(cfg) }); return true; }
141
+ if ((m = p.match(/^\/api\/channels\/([\w-]+)$/)) && (method === "PUT" || method === "PATCH")) {
142
+ const body = await readJson(req);
143
+ const id = m[1];
144
+ cfg.channels[id] = cfg.channels[id] || {};
145
+ if (typeof body.enabled === "boolean") cfg.channels[id].enabled = body.enabled;
146
+ if (typeof body.token === "string" && body.token) cfg.channels[id].token = body.token;
147
+ save(cfg);
148
+ const active = await reloadChannels();
149
+ send(res, 200, { ok: true, active });
150
+ return true;
151
+ }
152
+
153
+ if (p === "/api/memory" && method === "GET") {
154
+ const q = url.searchParams.get("q");
155
+ const items = q ? brain.recall(q, { limit: 25 }) : brain.all().slice(-50).reverse();
156
+ send(res, 200, { items });
157
+ return true;
158
+ }
159
+ if (p === "/api/memory" && method === "DELETE") { brain.clear(); send(res, 200, { ok: true }); return true; }
160
+
161
+ if (p === "/api/skills" && method === "GET") { send(res, 200, { skills: brain.synthesizeSkills({ minSuccesses: 2 }) }); return true; }
162
+
163
+ if (p === "/api/updates" && method === "GET") {
164
+ const r = await checkUpdates(cfg, { write: false });
165
+ send(res, 200, { findings: r.findings, proposal: r.proposal });
166
+ return true;
167
+ }
168
+ if (p === "/api/updates/check" && method === "POST") {
169
+ const r = await checkUpdates(cfg, { write: true });
170
+ send(res, 200, { findings: r.findings, proposal: r.proposal, proposalPath: r.proposalPath });
171
+ return true;
172
+ }
173
+
174
+ if (p === "/api/cowork" && method === "POST") {
175
+ const body = await readJson(req);
176
+ const goal = String(body.goal || "").trim();
177
+ if (!goal) { send(res, 400, { error: "missing goal" }); return true; }
178
+ const { respond } = await import("../agent/agent.mjs");
179
+ const result = await runOutcome(goal, {
180
+ execute: async (step) => {
181
+ const { text } = await respond([{ role: "user", content: `${step.title}: ${step.detail}` }]);
182
+ return { ok: true, note: text.slice(0, 400) };
183
+ },
184
+ onProgress: (e) => ctx.publish?.({ type: "cowork", phase: e.phase, step: e.step?.title }),
185
+ });
186
+ send(res, 200, result);
187
+ return true;
188
+ }
189
+
190
+ send(res, 404, { error: `no api route ${method} ${p}` });
191
+ return true;
192
+ } catch (e) {
193
+ send(res, 500, { error: String(e?.message || e) });
194
+ return true;
195
+ }
196
+ }