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 +30 -4
- package/assets/dash-channels.png +0 -0
- package/assets/dash-providers.png +0 -0
- package/assets/dash-settings.png +0 -0
- package/bin/bajaclaw.mjs +228 -63
- package/package.json +1 -1
- package/plugins/hermes-brain/store.mjs +7 -0
- package/src/daemon/api.mjs +196 -0
- package/src/daemon/daemon.mjs +7 -1
- package/src/daemon/gateway.mjs +12 -2
- package/src/onboarding/onboard.mjs +83 -46
- package/web/dist/assets/index-9QUVnzQL.js +43 -0
- package/web/dist/assets/{index-e6iMsHvC.css → index-DESHxRe5.css} +1 -1
- package/web/dist/index.html +2 -2
- package/web/dist/assets/index-Bu1IU_uS.js +0 -43
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` |
|
|
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
|
|
63
|
+
| `bajaclaw status` / `doctor` | Health and environment. |
|
|
62
64
|
| `bajaclaw ui` | Open the web interface. |
|
|
63
|
-
| `bajaclaw
|
|
64
|
-
|
|
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("
|
|
30
|
-
`${color.bold("bajaclaw
|
|
31
|
-
`${color.bold("bajaclaw
|
|
32
|
-
`${color.bold("bajaclaw restart")}
|
|
33
|
-
`${color.bold("bajaclaw status")}
|
|
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
|
|
37
|
-
|
|
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
|
-
|
|
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
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
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(
|
|
100
|
-
sp.stop(glyph.ok, `checked
|
|
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
|
-
|
|
104
|
-
|
|
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
|
-
|
|
168
|
+
// ---- manage ----
|
|
169
|
+
function cmdProviders() {
|
|
108
170
|
const cfg = load();
|
|
109
|
-
const
|
|
110
|
-
|
|
111
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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.
|
|
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
|
+
}
|