@openclawcity/become 0.2.0 → 1.0.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 +146 -107
- package/dist/cli.cjs +1550 -63
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.d.cts +8 -0
- package/dist/cli.d.ts +8 -0
- package/dist/cli.js +1536 -65
- package/dist/cli.js.map +1 -1
- package/dist/dashboard.d.cts +1 -1
- package/dist/dashboard.d.ts +1 -1
- package/dist/index.cjs +594 -0
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +160 -3
- package/dist/index.d.ts +160 -3
- package/dist/index.js +586 -0
- package/dist/index.js.map +1 -1
- package/dist/{types-DzOc15AL.d.cts → types-BnbaKMTo.d.cts} +1 -1
- package/dist/{types-DzOc15AL.d.ts → types-BnbaKMTo.d.ts} +1 -1
- package/package.json +4 -1
package/dist/cli.cjs
CHANGED
|
@@ -6,6 +6,10 @@ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
|
6
6
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
7
7
|
var __getProtoOf = Object.getPrototypeOf;
|
|
8
8
|
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
9
|
+
var __export = (target, all) => {
|
|
10
|
+
for (var name in all)
|
|
11
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
12
|
+
};
|
|
9
13
|
var __copyProps = (to, from, except, desc) => {
|
|
10
14
|
if (from && typeof from === "object" || typeof from === "function") {
|
|
11
15
|
for (let key of __getOwnPropNames(from))
|
|
@@ -22,92 +26,1575 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
|
|
|
22
26
|
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
23
27
|
mod
|
|
24
28
|
));
|
|
29
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
25
30
|
|
|
26
31
|
// src/cli/init.ts
|
|
32
|
+
var init_exports = {};
|
|
33
|
+
__export(init_exports, {
|
|
34
|
+
runSetup: () => runSetup,
|
|
35
|
+
showStatus: () => showStatus,
|
|
36
|
+
start: () => start,
|
|
37
|
+
turnOff: () => turnOff,
|
|
38
|
+
turnOn: () => turnOn
|
|
39
|
+
});
|
|
40
|
+
module.exports = __toCommonJS(init_exports);
|
|
41
|
+
|
|
42
|
+
// src/cli/setup.ts
|
|
43
|
+
var readline = __toESM(require("readline"), 1);
|
|
44
|
+
|
|
45
|
+
// src/cli/config.ts
|
|
27
46
|
var import_node_fs = require("fs");
|
|
28
47
|
var import_node_path = require("path");
|
|
29
|
-
var
|
|
30
|
-
var
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
48
|
+
var import_node_os = require("os");
|
|
49
|
+
var DEFAULT_CONFIG = {
|
|
50
|
+
agent_type: "openclaw",
|
|
51
|
+
llm_provider: "anthropic",
|
|
52
|
+
llm_base_url: "https://api.anthropic.com",
|
|
53
|
+
llm_api_key: "",
|
|
54
|
+
proxy_port: 30001,
|
|
55
|
+
dashboard_port: 30002,
|
|
56
|
+
auto_extract: true,
|
|
57
|
+
max_skills_per_call: 15,
|
|
58
|
+
max_lessons_per_day: 20,
|
|
59
|
+
state: "off"
|
|
60
|
+
};
|
|
61
|
+
function getBecomeDir() {
|
|
62
|
+
return (0, import_node_path.join)((0, import_node_os.homedir)(), ".become");
|
|
63
|
+
}
|
|
64
|
+
function getConfigPath() {
|
|
65
|
+
return (0, import_node_path.join)(getBecomeDir(), "config.json");
|
|
66
|
+
}
|
|
67
|
+
function loadConfig() {
|
|
68
|
+
const configPath = getConfigPath();
|
|
69
|
+
if (!(0, import_node_fs.existsSync)(configPath)) {
|
|
70
|
+
throw new Error("become is not set up. Run `become setup` first.");
|
|
71
|
+
}
|
|
72
|
+
try {
|
|
73
|
+
const raw = (0, import_node_fs.readFileSync)(configPath, "utf-8");
|
|
74
|
+
return { ...DEFAULT_CONFIG, ...JSON.parse(raw) };
|
|
75
|
+
} catch {
|
|
76
|
+
throw new Error("Invalid config. Run `become setup` to reconfigure.");
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
function saveConfig(config) {
|
|
80
|
+
const dir = getBecomeDir();
|
|
81
|
+
(0, import_node_fs.mkdirSync)(dir, { recursive: true });
|
|
82
|
+
(0, import_node_fs.mkdirSync)((0, import_node_path.join)(dir, "skills"), { recursive: true });
|
|
83
|
+
(0, import_node_fs.mkdirSync)((0, import_node_path.join)(dir, "pending"), { recursive: true });
|
|
84
|
+
(0, import_node_fs.mkdirSync)((0, import_node_path.join)(dir, "rejected"), { recursive: true });
|
|
85
|
+
(0, import_node_fs.mkdirSync)((0, import_node_path.join)(dir, "state"), { recursive: true });
|
|
86
|
+
(0, import_node_fs.writeFileSync)(getConfigPath(), JSON.stringify(config, null, 2), "utf-8");
|
|
87
|
+
}
|
|
88
|
+
var LLM_DEFAULTS = {
|
|
89
|
+
anthropic: { base_url: "https://api.anthropic.com" },
|
|
90
|
+
openai: { base_url: "https://api.openai.com" },
|
|
91
|
+
ollama: { base_url: "http://localhost:11434" },
|
|
92
|
+
openrouter: { base_url: "https://openrouter.ai/api" }
|
|
93
|
+
};
|
|
40
94
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
95
|
+
// src/cli/setup.ts
|
|
96
|
+
var AGENT_TYPES = ["openclaw", "ironclaw", "nanoclaw", "generic"];
|
|
97
|
+
var LLM_PROVIDERS = ["anthropic", "openai", "ollama", "openrouter", "custom"];
|
|
98
|
+
function ask(rl, question) {
|
|
99
|
+
return new Promise((resolve) => rl.question(question, resolve));
|
|
100
|
+
}
|
|
101
|
+
async function runSetup() {
|
|
102
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
103
|
+
try {
|
|
104
|
+
console.log("\nWelcome to become \u2014 agent-to-agent learning.\n");
|
|
105
|
+
console.log("Which agent runtime are you using?");
|
|
106
|
+
AGENT_TYPES.forEach((t, i) => console.log(` ${i + 1}. ${t}`));
|
|
107
|
+
const agentChoice = await ask(rl, "> ");
|
|
108
|
+
const agentIdx = parseInt(agentChoice, 10) - 1;
|
|
109
|
+
const agent_type = AGENT_TYPES[agentIdx] ?? "openclaw";
|
|
110
|
+
console.log("\nWhich LLM provider?");
|
|
111
|
+
LLM_PROVIDERS.forEach((p, i) => console.log(` ${i + 1}. ${p}`));
|
|
112
|
+
const llmChoice = await ask(rl, "> ");
|
|
113
|
+
const llmIdx = parseInt(llmChoice, 10) - 1;
|
|
114
|
+
const llm_provider = LLM_PROVIDERS[llmIdx] ?? "anthropic";
|
|
115
|
+
const llm_api_key = await ask(rl, "\nYour API key: ");
|
|
116
|
+
if (!llm_api_key.trim()) {
|
|
117
|
+
console.log("API key is required.");
|
|
118
|
+
process.exit(1);
|
|
119
|
+
}
|
|
120
|
+
const defaultUrl = LLM_DEFAULTS[llm_provider]?.base_url ?? "";
|
|
121
|
+
let llm_base_url = defaultUrl;
|
|
122
|
+
if (llm_provider === "custom" || !defaultUrl) {
|
|
123
|
+
llm_base_url = await ask(rl, "LLM base URL: ");
|
|
124
|
+
}
|
|
125
|
+
const portInput = await ask(rl, `
|
|
126
|
+
Proxy port (default 30001): `);
|
|
127
|
+
const proxy_port = parseInt(portInput, 10) || 30001;
|
|
128
|
+
const dashInput = await ask(rl, `Dashboard port (default 30002): `);
|
|
129
|
+
const dashboard_port = parseInt(dashInput, 10) || 30002;
|
|
130
|
+
const config = {
|
|
131
|
+
agent_type,
|
|
132
|
+
llm_provider,
|
|
133
|
+
llm_base_url,
|
|
134
|
+
llm_api_key: llm_api_key.trim(),
|
|
135
|
+
proxy_port,
|
|
136
|
+
dashboard_port,
|
|
137
|
+
auto_extract: true,
|
|
138
|
+
max_skills_per_call: 15,
|
|
139
|
+
max_lessons_per_day: 20,
|
|
140
|
+
state: "off"
|
|
141
|
+
};
|
|
142
|
+
saveConfig(config);
|
|
143
|
+
console.log("\nConfig saved to ~/.become/config.json");
|
|
144
|
+
console.log("Run `become start` to start the proxy and dashboard.");
|
|
145
|
+
console.log("Run `become on` to route your agent through become.\n");
|
|
146
|
+
} finally {
|
|
147
|
+
rl.close();
|
|
148
|
+
}
|
|
149
|
+
}
|
|
45
150
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
151
|
+
// src/proxy/server.ts
|
|
152
|
+
var import_node_http = require("http");
|
|
153
|
+
|
|
154
|
+
// src/skills/store.ts
|
|
155
|
+
var import_node_fs2 = require("fs");
|
|
156
|
+
var import_node_path2 = require("path");
|
|
157
|
+
var import_node_crypto = require("crypto");
|
|
158
|
+
var FileSkillStore = class {
|
|
159
|
+
skillsDir;
|
|
160
|
+
pendingDir;
|
|
161
|
+
rejectedDir;
|
|
162
|
+
constructor(config) {
|
|
163
|
+
this.skillsDir = (0, import_node_path2.join)(config.baseDir, "skills");
|
|
164
|
+
this.pendingDir = (0, import_node_path2.join)(config.baseDir, "pending");
|
|
165
|
+
this.rejectedDir = (0, import_node_path2.join)(config.baseDir, "rejected");
|
|
166
|
+
(0, import_node_fs2.mkdirSync)(this.skillsDir, { recursive: true });
|
|
167
|
+
(0, import_node_fs2.mkdirSync)(this.pendingDir, { recursive: true });
|
|
168
|
+
(0, import_node_fs2.mkdirSync)(this.rejectedDir, { recursive: true });
|
|
169
|
+
}
|
|
170
|
+
// ── Read ────────────────────────────────────────────────────────────────
|
|
171
|
+
listApproved() {
|
|
172
|
+
return this.readDir(this.skillsDir);
|
|
173
|
+
}
|
|
174
|
+
listPending() {
|
|
175
|
+
return this.readDir(this.pendingDir);
|
|
176
|
+
}
|
|
177
|
+
listRejected() {
|
|
178
|
+
return this.readDir(this.rejectedDir);
|
|
179
|
+
}
|
|
180
|
+
getApproved(id) {
|
|
181
|
+
this.validateId(id);
|
|
182
|
+
return this.readFile((0, import_node_path2.join)(this.skillsDir, `${id}.md`));
|
|
183
|
+
}
|
|
184
|
+
// ── Write ───────────────────────────────────────────────────────────────
|
|
185
|
+
savePending(lesson) {
|
|
186
|
+
const normalized = lesson.instruction.toLowerCase().trim();
|
|
187
|
+
const allExisting = [...this.listApproved(), ...this.listPending()];
|
|
188
|
+
if (allExisting.some((s) => s.instruction.toLowerCase().trim() === normalized)) {
|
|
189
|
+
return null;
|
|
190
|
+
}
|
|
191
|
+
const id = this.generateId(lesson.name);
|
|
192
|
+
const file = { ...lesson, id, approved_at: void 0 };
|
|
193
|
+
this.writeFile((0, import_node_path2.join)(this.pendingDir, `${id}.md`), file);
|
|
194
|
+
return file;
|
|
195
|
+
}
|
|
196
|
+
approve(id) {
|
|
197
|
+
this.validateId(id);
|
|
198
|
+
const src = (0, import_node_path2.join)(this.pendingDir, `${id}.md`);
|
|
199
|
+
if (!(0, import_node_fs2.existsSync)(src)) return false;
|
|
200
|
+
const skill = this.readFile(src);
|
|
201
|
+
if (!skill) return false;
|
|
202
|
+
skill.approved_at = (/* @__PURE__ */ new Date()).toISOString();
|
|
203
|
+
const dest = (0, import_node_path2.join)(this.skillsDir, `${id}.md`);
|
|
204
|
+
this.writeFile(dest, skill);
|
|
205
|
+
(0, import_node_fs2.unlinkSync)(src);
|
|
206
|
+
return true;
|
|
207
|
+
}
|
|
208
|
+
reject(id) {
|
|
209
|
+
this.validateId(id);
|
|
210
|
+
const src = (0, import_node_path2.join)(this.pendingDir, `${id}.md`);
|
|
211
|
+
if (!(0, import_node_fs2.existsSync)(src)) return false;
|
|
212
|
+
const dest = (0, import_node_path2.join)(this.rejectedDir, `${id}.md`);
|
|
213
|
+
(0, import_node_fs2.renameSync)(src, dest);
|
|
214
|
+
return true;
|
|
215
|
+
}
|
|
216
|
+
disable(id) {
|
|
217
|
+
this.validateId(id);
|
|
218
|
+
const src = (0, import_node_path2.join)(this.skillsDir, `${id}.md`);
|
|
219
|
+
if (!(0, import_node_fs2.existsSync)(src)) return false;
|
|
220
|
+
const dest = (0, import_node_path2.join)(this.rejectedDir, `${id}.md`);
|
|
221
|
+
(0, import_node_fs2.renameSync)(src, dest);
|
|
222
|
+
return true;
|
|
223
|
+
}
|
|
224
|
+
remove(id) {
|
|
225
|
+
this.validateId(id);
|
|
226
|
+
for (const dir of [this.skillsDir, this.pendingDir, this.rejectedDir]) {
|
|
227
|
+
const path = (0, import_node_path2.join)(dir, `${id}.md`);
|
|
228
|
+
if ((0, import_node_fs2.existsSync)(path)) {
|
|
229
|
+
(0, import_node_fs2.unlinkSync)(path);
|
|
230
|
+
return true;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
return false;
|
|
49
234
|
}
|
|
235
|
+
// ── Helpers ─────────────────────────────────────────────────────────────
|
|
236
|
+
/**
|
|
237
|
+
* Validate that an id does not contain path traversal characters.
|
|
238
|
+
* Prevents attacks like id="../../.ssh/authorized_keys"
|
|
239
|
+
*/
|
|
240
|
+
validateId(id) {
|
|
241
|
+
if (!id || typeof id !== "string") throw new Error("Invalid id");
|
|
242
|
+
if (id.includes("/") || id.includes("\\") || id.includes("..") || id.includes("\0")) {
|
|
243
|
+
throw new Error("Invalid id: path traversal detected");
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
readDir(dir) {
|
|
247
|
+
if (!(0, import_node_fs2.existsSync)(dir)) return [];
|
|
248
|
+
const files = (0, import_node_fs2.readdirSync)(dir).filter((f) => f.endsWith(".md"));
|
|
249
|
+
const skills = [];
|
|
250
|
+
for (const f of files) {
|
|
251
|
+
const skill = this.readFile((0, import_node_path2.join)(dir, f));
|
|
252
|
+
if (skill) skills.push(skill);
|
|
253
|
+
}
|
|
254
|
+
return skills.sort((a, b) => b.created_at.localeCompare(a.created_at));
|
|
255
|
+
}
|
|
256
|
+
readFile(path) {
|
|
257
|
+
if (!(0, import_node_fs2.existsSync)(path)) return null;
|
|
258
|
+
try {
|
|
259
|
+
const content = (0, import_node_fs2.readFileSync)(path, "utf-8");
|
|
260
|
+
return this.parseSkillFile(content, (0, import_node_path2.basename)(path, ".md"));
|
|
261
|
+
} catch {
|
|
262
|
+
return null;
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
writeFile(path, skill) {
|
|
266
|
+
const content = this.formatSkillFile(skill);
|
|
267
|
+
(0, import_node_fs2.writeFileSync)(path, content, "utf-8");
|
|
268
|
+
}
|
|
269
|
+
parseSkillFile(content, id) {
|
|
270
|
+
const match = content.match(/^---\s*\n([\s\S]*?)\n---\s*\n([\s\S]*)$/);
|
|
271
|
+
if (!match) return null;
|
|
272
|
+
const [, frontmatter, body] = match;
|
|
273
|
+
const meta = {};
|
|
274
|
+
for (const line of frontmatter.split("\n")) {
|
|
275
|
+
const colonIdx = line.indexOf(":");
|
|
276
|
+
if (colonIdx === -1) continue;
|
|
277
|
+
const key = line.slice(0, colonIdx).trim();
|
|
278
|
+
const value = line.slice(colonIdx + 1).trim();
|
|
279
|
+
if (key && value) meta[key] = value;
|
|
280
|
+
}
|
|
281
|
+
return {
|
|
282
|
+
id,
|
|
283
|
+
name: meta.name ?? id,
|
|
284
|
+
instruction: body.trim(),
|
|
285
|
+
learned_from: meta.learned_from ?? "unknown",
|
|
286
|
+
source: meta.source ?? "conversation",
|
|
287
|
+
confidence: parseFloat(meta.confidence ?? "0.5"),
|
|
288
|
+
approved_at: meta.approved_at || void 0,
|
|
289
|
+
created_at: meta.created_at ?? (/* @__PURE__ */ new Date()).toISOString()
|
|
290
|
+
};
|
|
291
|
+
}
|
|
292
|
+
formatSkillFile(skill) {
|
|
293
|
+
const lines = [
|
|
294
|
+
"---",
|
|
295
|
+
`name: ${skill.name}`,
|
|
296
|
+
`learned_from: ${skill.learned_from}`,
|
|
297
|
+
`source: ${skill.source}`,
|
|
298
|
+
`confidence: ${skill.confidence}`,
|
|
299
|
+
`created_at: ${skill.created_at}`
|
|
300
|
+
];
|
|
301
|
+
if (skill.approved_at) lines.push(`approved_at: ${skill.approved_at}`);
|
|
302
|
+
lines.push("---");
|
|
303
|
+
lines.push("");
|
|
304
|
+
lines.push(skill.instruction);
|
|
305
|
+
lines.push("");
|
|
306
|
+
return lines.join("\n");
|
|
307
|
+
}
|
|
308
|
+
generateId(name) {
|
|
309
|
+
const clean = name.toLowerCase().replace(/[^a-z0-9]+/g, "_").slice(0, 40);
|
|
310
|
+
const hash = (0, import_node_crypto.createHash)("sha256").update(`${name}${Date.now()}${Math.random()}`).digest("hex").slice(0, 6);
|
|
311
|
+
return `${clean}_${hash}`;
|
|
312
|
+
}
|
|
313
|
+
};
|
|
314
|
+
|
|
315
|
+
// src/skills/trust.ts
|
|
316
|
+
var import_node_fs3 = require("fs");
|
|
317
|
+
var import_node_path3 = require("path");
|
|
318
|
+
var DEFAULT_TRUST = {
|
|
319
|
+
trusted: [],
|
|
320
|
+
blocked: [],
|
|
321
|
+
default: "pending"
|
|
322
|
+
};
|
|
323
|
+
var DEFAULT_RATE_LIMITS = {
|
|
324
|
+
max_lessons_per_day: 20,
|
|
325
|
+
max_lessons_per_agent: 10,
|
|
326
|
+
max_skills_per_call: 15
|
|
327
|
+
};
|
|
328
|
+
var TrustManager = class {
|
|
329
|
+
trustPath;
|
|
330
|
+
statsPath;
|
|
331
|
+
config;
|
|
332
|
+
dailyCounts;
|
|
333
|
+
constructor(baseDir) {
|
|
334
|
+
this.trustPath = (0, import_node_path3.join)(baseDir, "trust.json");
|
|
335
|
+
this.statsPath = (0, import_node_path3.join)(baseDir, "state", "daily_counts.json");
|
|
336
|
+
(0, import_node_fs3.mkdirSync)((0, import_node_path3.join)(baseDir, "state"), { recursive: true });
|
|
337
|
+
this.config = this.loadTrust();
|
|
338
|
+
this.dailyCounts = this.loadDailyCounts();
|
|
339
|
+
}
|
|
340
|
+
getLevel(agentId) {
|
|
341
|
+
if (this.config.trusted.includes(agentId)) return "trusted";
|
|
342
|
+
if (this.config.blocked.includes(agentId)) return "blocked";
|
|
343
|
+
return this.config.default;
|
|
344
|
+
}
|
|
345
|
+
setLevel(agentId, level) {
|
|
346
|
+
this.config.trusted = this.config.trusted.filter((a) => a !== agentId);
|
|
347
|
+
this.config.blocked = this.config.blocked.filter((a) => a !== agentId);
|
|
348
|
+
if (level === "trusted") this.config.trusted.push(agentId);
|
|
349
|
+
if (level === "blocked") this.config.blocked.push(agentId);
|
|
350
|
+
this.saveTrust();
|
|
351
|
+
}
|
|
352
|
+
setDefault(level) {
|
|
353
|
+
this.config.default = level;
|
|
354
|
+
this.saveTrust();
|
|
355
|
+
}
|
|
356
|
+
getConfig() {
|
|
357
|
+
return { ...this.config };
|
|
358
|
+
}
|
|
359
|
+
// ── Rate Limiting ───────────────────────────────────────────────────────
|
|
360
|
+
canLearn(agentId, limits = DEFAULT_RATE_LIMITS) {
|
|
361
|
+
this.refreshDailyCountsIfNewDay();
|
|
362
|
+
if (this.dailyCounts.total >= limits.max_lessons_per_day) return false;
|
|
363
|
+
const agentCount = this.dailyCounts.perAgent[agentId] ?? 0;
|
|
364
|
+
if (agentCount >= limits.max_lessons_per_agent) return false;
|
|
365
|
+
return true;
|
|
366
|
+
}
|
|
367
|
+
recordLesson(agentId) {
|
|
368
|
+
this.refreshDailyCountsIfNewDay();
|
|
369
|
+
this.dailyCounts.total++;
|
|
370
|
+
this.dailyCounts.perAgent[agentId] = (this.dailyCounts.perAgent[agentId] ?? 0) + 1;
|
|
371
|
+
this.saveDailyCounts();
|
|
372
|
+
}
|
|
373
|
+
getDailyCounts() {
|
|
374
|
+
this.refreshDailyCountsIfNewDay();
|
|
375
|
+
return { total: this.dailyCounts.total, perAgent: { ...this.dailyCounts.perAgent } };
|
|
376
|
+
}
|
|
377
|
+
// ── Private ─────────────────────────────────────────────────────────────
|
|
378
|
+
loadTrust() {
|
|
379
|
+
if (!(0, import_node_fs3.existsSync)(this.trustPath)) return { ...DEFAULT_TRUST, trusted: [], blocked: [] };
|
|
380
|
+
try {
|
|
381
|
+
const raw = JSON.parse((0, import_node_fs3.readFileSync)(this.trustPath, "utf-8"));
|
|
382
|
+
return {
|
|
383
|
+
trusted: Array.isArray(raw.trusted) ? raw.trusted.filter((a) => typeof a === "string") : [],
|
|
384
|
+
blocked: Array.isArray(raw.blocked) ? raw.blocked.filter((a) => typeof a === "string") : [],
|
|
385
|
+
default: ["trusted", "pending", "blocked"].includes(raw.default) ? raw.default : "pending"
|
|
386
|
+
};
|
|
387
|
+
} catch {
|
|
388
|
+
return { ...DEFAULT_TRUST, trusted: [], blocked: [] };
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
saveTrust() {
|
|
392
|
+
(0, import_node_fs3.mkdirSync)((0, import_node_path3.dirname)(this.trustPath), { recursive: true });
|
|
393
|
+
(0, import_node_fs3.writeFileSync)(this.trustPath, JSON.stringify(this.config, null, 2), "utf-8");
|
|
394
|
+
}
|
|
395
|
+
loadDailyCounts() {
|
|
396
|
+
const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
397
|
+
if (!(0, import_node_fs3.existsSync)(this.statsPath)) return { date: today, total: 0, perAgent: {} };
|
|
398
|
+
try {
|
|
399
|
+
const data = JSON.parse((0, import_node_fs3.readFileSync)(this.statsPath, "utf-8"));
|
|
400
|
+
if (data.date !== today) return { date: today, total: 0, perAgent: {} };
|
|
401
|
+
return data;
|
|
402
|
+
} catch {
|
|
403
|
+
return { date: today, total: 0, perAgent: {} };
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
saveDailyCounts() {
|
|
407
|
+
(0, import_node_fs3.writeFileSync)(this.statsPath, JSON.stringify(this.dailyCounts, null, 2), "utf-8");
|
|
408
|
+
}
|
|
409
|
+
refreshDailyCountsIfNewDay() {
|
|
410
|
+
const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
411
|
+
if (this.dailyCounts.date !== today) {
|
|
412
|
+
this.dailyCounts = { date: today, total: 0, perAgent: {} };
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
};
|
|
416
|
+
|
|
417
|
+
// src/skills/format.ts
|
|
418
|
+
function formatSkillsForInjection(skills) {
|
|
419
|
+
if (skills.length === 0) return "";
|
|
420
|
+
const lines = skills.map((s) => {
|
|
421
|
+
const source = s.source === "peer_review" ? "from a peer review" : s.source === "collaboration" ? "from a collaboration" : s.source === "teaching" ? "from being taught" : "from a conversation";
|
|
422
|
+
return `- ${s.instruction} (${source})`;
|
|
423
|
+
});
|
|
424
|
+
return [
|
|
425
|
+
"## Lessons learned from other agents",
|
|
426
|
+
"",
|
|
427
|
+
"You have learned the following from interactions with other agents. Follow these instructions:",
|
|
428
|
+
"",
|
|
429
|
+
...lines
|
|
430
|
+
].join("\n");
|
|
50
431
|
}
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
const
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
432
|
+
function injectSkillsIntoMessages(messages, skillText) {
|
|
433
|
+
if (!skillText) return;
|
|
434
|
+
const sysIdx = messages.findIndex((m) => m.role === "system");
|
|
435
|
+
if (sysIdx >= 0) {
|
|
436
|
+
messages[sysIdx].content = skillText + "\n\n---\n\n" + messages[sysIdx].content;
|
|
437
|
+
} else {
|
|
438
|
+
messages.unshift({ role: "system", content: skillText });
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
// src/proxy/detector.ts
|
|
443
|
+
var CHANNEL_PATTERN = /^\[([^\]]+)\s+says?\]:\s*/;
|
|
444
|
+
var DM_PATTERN = /^DM\s+from\s+([^:]+):\s*/;
|
|
445
|
+
var BUILDING_PATTERN = /^([a-zA-Z0-9]+[-_][a-zA-Z0-9_.-]+)\s+in\s+[^:]+:\s*/;
|
|
446
|
+
var REVIEW_KEYWORDS = ["strengths:", "weaknesses:", "verdict:", "assessment:", "suggestions:"];
|
|
447
|
+
function detectAgentConversation(messages) {
|
|
448
|
+
const negative = { isAgentToAgent: false };
|
|
449
|
+
if (!messages || messages.length === 0) return negative;
|
|
450
|
+
for (const msg of messages) {
|
|
451
|
+
if (msg.role !== "user" && msg.role !== "assistant") continue;
|
|
452
|
+
const content = typeof msg.content === "string" ? msg.content : "";
|
|
453
|
+
if (msg.name && msg.role === "user") {
|
|
454
|
+
return {
|
|
455
|
+
isAgentToAgent: true,
|
|
456
|
+
otherAgentId: msg.name,
|
|
457
|
+
exchangeType: "chat"
|
|
458
|
+
};
|
|
61
459
|
}
|
|
62
|
-
|
|
460
|
+
const channelMatch = content.match(CHANNEL_PATTERN);
|
|
461
|
+
if (channelMatch) {
|
|
462
|
+
return {
|
|
463
|
+
isAgentToAgent: true,
|
|
464
|
+
otherAgentId: channelMatch[1].trim(),
|
|
465
|
+
exchangeType: "channel"
|
|
466
|
+
};
|
|
467
|
+
}
|
|
468
|
+
const dmMatch = content.match(DM_PATTERN);
|
|
469
|
+
if (dmMatch) {
|
|
470
|
+
return {
|
|
471
|
+
isAgentToAgent: true,
|
|
472
|
+
otherAgentId: dmMatch[1].trim(),
|
|
473
|
+
exchangeType: "dm"
|
|
474
|
+
};
|
|
475
|
+
}
|
|
476
|
+
const buildingMatch = content.match(BUILDING_PATTERN);
|
|
477
|
+
if (buildingMatch) {
|
|
478
|
+
return {
|
|
479
|
+
isAgentToAgent: true,
|
|
480
|
+
otherAgentId: buildingMatch[1].trim(),
|
|
481
|
+
exchangeType: "chat"
|
|
482
|
+
};
|
|
483
|
+
}
|
|
484
|
+
const lowerContent = content.toLowerCase();
|
|
485
|
+
const reviewMatches = REVIEW_KEYWORDS.filter((kw) => lowerContent.includes(kw));
|
|
486
|
+
if (reviewMatches.length >= 2) {
|
|
487
|
+
return {
|
|
488
|
+
isAgentToAgent: true,
|
|
489
|
+
otherAgentId: void 0,
|
|
490
|
+
exchangeType: "peer_review"
|
|
491
|
+
};
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
return negative;
|
|
495
|
+
}
|
|
496
|
+
function extractExchangeText(messages) {
|
|
497
|
+
return messages.filter((m) => m.role === "user" || m.role === "assistant").map((m) => {
|
|
498
|
+
const speaker = m.name ?? m.role;
|
|
499
|
+
const content = typeof m.content === "string" ? m.content : "";
|
|
500
|
+
return `[${speaker}]: ${content}`;
|
|
501
|
+
}).join("\n").slice(0, 6e3);
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
// src/proxy/extractor.ts
|
|
505
|
+
var LessonExtractor = class {
|
|
506
|
+
constructor(store, trust, analyzer) {
|
|
507
|
+
this.store = store;
|
|
508
|
+
this.trust = trust;
|
|
509
|
+
this.analyzer = analyzer;
|
|
510
|
+
}
|
|
511
|
+
/**
|
|
512
|
+
* Analyze a conversation and extract lessons. Fire-and-forget.
|
|
513
|
+
*/
|
|
514
|
+
async extract(messages) {
|
|
515
|
+
const detection = detectAgentConversation(messages);
|
|
516
|
+
if (!detection.isAgentToAgent) return;
|
|
517
|
+
const agentId = detection.otherAgentId ?? "unknown-agent";
|
|
518
|
+
const trustLevel = this.trust.getLevel(agentId);
|
|
519
|
+
if (trustLevel === "blocked") return;
|
|
520
|
+
if (!this.trust.canLearn(agentId)) return;
|
|
521
|
+
const exchangeText = extractExchangeText(messages);
|
|
522
|
+
if (exchangeText.length < 20) return;
|
|
523
|
+
const prompt = `Analyze this conversation between an AI agent and another agent. Extract concrete, actionable lessons that the first agent (the "assistant") can learn from the other agent.
|
|
524
|
+
|
|
525
|
+
CONVERSATION:
|
|
526
|
+
${exchangeText.slice(0, 4e3)}
|
|
527
|
+
|
|
528
|
+
Output valid JSON array:
|
|
529
|
+
[{"skill": "skill_name_snake_case", "instruction": "concrete actionable lesson in 1-2 sentences", "confidence": 0.0-1.0}]
|
|
530
|
+
|
|
531
|
+
Rules:
|
|
532
|
+
- Only extract lessons where the other agent clearly teaches, corrects, or shares useful knowledge
|
|
533
|
+
- instruction must be concrete and actionable ("use X when Y" not "consider improving")
|
|
534
|
+
- confidence: 0.9 = explicitly taught, 0.7 = clearly implied, 0.5 = suggested, below 0.5 = skip
|
|
535
|
+
- Only include lessons with confidence >= 0.5
|
|
536
|
+
- Max 3 lessons per conversation
|
|
537
|
+
- If no real learning happened, return []`;
|
|
63
538
|
try {
|
|
64
|
-
const
|
|
65
|
-
const
|
|
66
|
-
const
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
539
|
+
const response = await this.analyzer.analyze(prompt);
|
|
540
|
+
const lessons = this.parseLessons(response);
|
|
541
|
+
for (const lesson of lessons.slice(0, 3)) {
|
|
542
|
+
const saved = this.store.savePending({
|
|
543
|
+
name: lesson.skill,
|
|
544
|
+
instruction: lesson.instruction.slice(0, 500),
|
|
545
|
+
learned_from: agentId,
|
|
546
|
+
source: detection.exchangeType ?? "conversation",
|
|
547
|
+
confidence: lesson.confidence,
|
|
548
|
+
created_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
549
|
+
});
|
|
550
|
+
if (saved) {
|
|
551
|
+
this.trust.recordLesson(agentId);
|
|
552
|
+
if (trustLevel === "trusted") {
|
|
553
|
+
this.store.approve(saved.id);
|
|
554
|
+
}
|
|
555
|
+
}
|
|
73
556
|
}
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
557
|
+
} catch {
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
parseLessons(response) {
|
|
561
|
+
const jsonMatch = response.match(/\[[\s\S]*\]/);
|
|
562
|
+
if (!jsonMatch) return [];
|
|
563
|
+
try {
|
|
564
|
+
const parsed = JSON.parse(jsonMatch[0]);
|
|
565
|
+
if (!Array.isArray(parsed)) return [];
|
|
566
|
+
return parsed.filter(
|
|
567
|
+
(r) => typeof r.skill === "string" && typeof r.instruction === "string" && typeof r.confidence === "number" && r.confidence >= 0.5 && r.skill.length > 0 && r.instruction.length > 0
|
|
568
|
+
);
|
|
569
|
+
} catch {
|
|
570
|
+
return [];
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
};
|
|
574
|
+
|
|
575
|
+
// src/proxy/server.ts
|
|
576
|
+
var SKILL_CACHE_TTL_MS = 5e3;
|
|
577
|
+
function createProxyServer(config, analyzer) {
|
|
578
|
+
const store = new FileSkillStore({ baseDir: config.baseDir });
|
|
579
|
+
const trust = new TrustManager(config.baseDir);
|
|
580
|
+
const extractor = analyzer ? new LessonExtractor(store, trust, analyzer) : null;
|
|
581
|
+
const stats = {
|
|
582
|
+
requests_forwarded: 0,
|
|
583
|
+
skills_injected: 0,
|
|
584
|
+
lessons_extracted: 0,
|
|
585
|
+
started_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
586
|
+
};
|
|
587
|
+
let cachedSkills = [];
|
|
588
|
+
let cacheTimestamp = 0;
|
|
589
|
+
function getSkills() {
|
|
590
|
+
const now = Date.now();
|
|
591
|
+
if (now - cacheTimestamp > SKILL_CACHE_TTL_MS) {
|
|
592
|
+
cachedSkills = store.listApproved();
|
|
593
|
+
cacheTimestamp = now;
|
|
594
|
+
}
|
|
595
|
+
return cachedSkills;
|
|
596
|
+
}
|
|
597
|
+
const server = (0, import_node_http.createServer)(async (req, res) => {
|
|
598
|
+
if (req.url === "/health" && req.method === "GET") {
|
|
599
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
600
|
+
res.end(JSON.stringify({ status: "ok", ...stats }));
|
|
601
|
+
return;
|
|
602
|
+
}
|
|
603
|
+
const isOpenAI = req.url === "/v1/chat/completions";
|
|
604
|
+
const isAnthropic = req.url === "/v1/messages";
|
|
605
|
+
if (req.method !== "POST" || !isOpenAI && !isAnthropic) {
|
|
606
|
+
res.writeHead(404, { "Content-Type": "application/json" });
|
|
607
|
+
res.end(JSON.stringify({ error: "Not found. Use POST /v1/chat/completions or /v1/messages" }));
|
|
608
|
+
return;
|
|
609
|
+
}
|
|
610
|
+
try {
|
|
611
|
+
const rawBody = await readBody(req);
|
|
612
|
+
const body = JSON.parse(rawBody);
|
|
613
|
+
const messages = body.messages;
|
|
614
|
+
if (Array.isArray(messages)) {
|
|
615
|
+
const skills = getSkills().slice(0, config.max_skills_per_call);
|
|
616
|
+
if (skills.length > 0) {
|
|
617
|
+
const skillText = formatSkillsForInjection(skills);
|
|
618
|
+
injectSkillsIntoMessages(messages, skillText);
|
|
619
|
+
stats.skills_injected++;
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
const upstreamUrl = buildUpstreamUrl(config, req.url);
|
|
623
|
+
const upstreamHeaders = buildUpstreamHeaders(config, req.headers);
|
|
624
|
+
const isStreaming = body.stream === true;
|
|
625
|
+
const modifiedBody = JSON.stringify(body);
|
|
626
|
+
const upstreamRes = await fetch(upstreamUrl, {
|
|
627
|
+
method: "POST",
|
|
628
|
+
headers: upstreamHeaders,
|
|
629
|
+
body: modifiedBody
|
|
630
|
+
});
|
|
631
|
+
stats.requests_forwarded++;
|
|
632
|
+
const responseHeaders = {};
|
|
633
|
+
upstreamRes.headers.forEach((value, key) => {
|
|
634
|
+
if (key.toLowerCase() !== "transfer-encoding") {
|
|
635
|
+
responseHeaders[key] = value;
|
|
636
|
+
}
|
|
637
|
+
});
|
|
638
|
+
res.writeHead(upstreamRes.status, responseHeaders);
|
|
639
|
+
if (isStreaming && upstreamRes.body) {
|
|
640
|
+
const reader = upstreamRes.body.getReader();
|
|
641
|
+
try {
|
|
642
|
+
while (true) {
|
|
643
|
+
const { done, value } = await reader.read();
|
|
644
|
+
if (done) break;
|
|
645
|
+
res.write(value);
|
|
646
|
+
}
|
|
647
|
+
} finally {
|
|
648
|
+
res.end();
|
|
649
|
+
}
|
|
650
|
+
if (config.auto_extract && extractor && Array.isArray(messages)) {
|
|
651
|
+
extractor.extract(messages).then(() => {
|
|
652
|
+
stats.lessons_extracted++;
|
|
653
|
+
}).catch(() => {
|
|
654
|
+
});
|
|
655
|
+
}
|
|
78
656
|
} else {
|
|
79
|
-
|
|
657
|
+
const responseBuffer = await upstreamRes.arrayBuffer();
|
|
658
|
+
res.end(Buffer.from(responseBuffer));
|
|
659
|
+
if (config.auto_extract && extractor && Array.isArray(messages)) {
|
|
660
|
+
extractor.extract(messages).then(() => {
|
|
661
|
+
stats.lessons_extracted++;
|
|
662
|
+
}).catch(() => {
|
|
663
|
+
});
|
|
664
|
+
}
|
|
80
665
|
}
|
|
81
666
|
} catch (err) {
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
667
|
+
const safeMessage = err instanceof Error && err.message === "Request body too large" ? "Request body too large" : "Failed to forward request to LLM";
|
|
668
|
+
if (!res.headersSent) {
|
|
669
|
+
res.writeHead(502, { "Content-Type": "application/json" });
|
|
670
|
+
}
|
|
671
|
+
res.end(JSON.stringify({ error: safeMessage }));
|
|
86
672
|
}
|
|
673
|
+
});
|
|
674
|
+
return {
|
|
675
|
+
server,
|
|
676
|
+
stats,
|
|
677
|
+
store,
|
|
678
|
+
trust,
|
|
679
|
+
listen: (port) => {
|
|
680
|
+
const p = port ?? config.port;
|
|
681
|
+
return new Promise((resolve) => {
|
|
682
|
+
server.listen(p, "127.0.0.1", () => resolve());
|
|
683
|
+
});
|
|
684
|
+
},
|
|
685
|
+
close: () => new Promise((resolve) => server.close(() => resolve()))
|
|
686
|
+
};
|
|
687
|
+
}
|
|
688
|
+
function readBody(req) {
|
|
689
|
+
return new Promise((resolve, reject) => {
|
|
690
|
+
const chunks = [];
|
|
691
|
+
let size = 0;
|
|
692
|
+
const MAX_BODY = 10 * 1024 * 1024;
|
|
693
|
+
req.on("data", (chunk) => {
|
|
694
|
+
size += chunk.length;
|
|
695
|
+
if (size > MAX_BODY) {
|
|
696
|
+
req.destroy();
|
|
697
|
+
reject(new Error("Request body too large"));
|
|
698
|
+
return;
|
|
699
|
+
}
|
|
700
|
+
chunks.push(chunk);
|
|
701
|
+
});
|
|
702
|
+
req.on("end", () => resolve(Buffer.concat(chunks).toString("utf-8")));
|
|
703
|
+
req.on("error", reject);
|
|
704
|
+
});
|
|
705
|
+
}
|
|
706
|
+
function buildUpstreamUrl(config, path) {
|
|
707
|
+
const base = config.llm_base_url.replace(/\/+$/, "");
|
|
708
|
+
return `${base}${path}`;
|
|
709
|
+
}
|
|
710
|
+
function buildUpstreamHeaders(config, incomingHeaders) {
|
|
711
|
+
const headers = {
|
|
712
|
+
"Content-Type": "application/json"
|
|
713
|
+
};
|
|
714
|
+
if (config.llm_provider === "anthropic") {
|
|
715
|
+
headers["x-api-key"] = config.llm_api_key;
|
|
716
|
+
headers["anthropic-version"] = "2023-06-01";
|
|
717
|
+
const version = incomingHeaders["anthropic-version"];
|
|
718
|
+
if (typeof version === "string") headers["anthropic-version"] = version;
|
|
719
|
+
const beta = incomingHeaders["anthropic-beta"];
|
|
720
|
+
if (typeof beta === "string") headers["anthropic-beta"] = beta;
|
|
87
721
|
} else {
|
|
88
|
-
|
|
89
|
-
console.log("Set SUPABASE_URL and SUPABASE_KEY to use Supabase, or use --sqlite for local storage.");
|
|
722
|
+
headers["Authorization"] = `Bearer ${config.llm_api_key}`;
|
|
90
723
|
}
|
|
91
|
-
|
|
92
|
-
|
|
724
|
+
const accept = incomingHeaders["accept"];
|
|
725
|
+
if (typeof accept === "string") headers["Accept"] = accept;
|
|
726
|
+
return headers;
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
// src/dashboard/server.ts
|
|
730
|
+
var import_node_http2 = require("http");
|
|
731
|
+
|
|
732
|
+
// src/dashboard/api/handlers.ts
|
|
733
|
+
function createHandlers(deps) {
|
|
734
|
+
const { store, trust, getProxyStats, getState, setState } = deps;
|
|
735
|
+
return {
|
|
736
|
+
// ── Status ──────────────────────────────────────────────────────────
|
|
737
|
+
"GET /api/status": () => ({
|
|
738
|
+
state: getState(),
|
|
739
|
+
skills_count: store.listApproved().length,
|
|
740
|
+
pending_count: store.listPending().length,
|
|
741
|
+
rejected_count: store.listRejected().length,
|
|
742
|
+
proxy: getProxyStats()
|
|
743
|
+
}),
|
|
744
|
+
// ── State toggle ────────────────────────────────────────────────────
|
|
745
|
+
"POST /api/state": (body) => {
|
|
746
|
+
const newState = body?.state;
|
|
747
|
+
if (newState !== "on" && newState !== "off") {
|
|
748
|
+
return { error: 'state must be "on" or "off"' };
|
|
749
|
+
}
|
|
750
|
+
try {
|
|
751
|
+
setState(newState);
|
|
752
|
+
return { state: newState };
|
|
753
|
+
} catch (err) {
|
|
754
|
+
const msg = err instanceof Error ? err.message : "Failed to change state";
|
|
755
|
+
return { error: msg };
|
|
756
|
+
}
|
|
757
|
+
},
|
|
758
|
+
// ── Skills (approved) ───────────────────────────────────────────────
|
|
759
|
+
"GET /api/skills": () => store.listApproved(),
|
|
760
|
+
// ── Pending ─────────────────────────────────────────────────────────
|
|
761
|
+
"GET /api/pending": () => store.listPending(),
|
|
762
|
+
// ── Rejected ────────────────────────────────────────────────────────
|
|
763
|
+
"GET /api/rejected": () => store.listRejected(),
|
|
764
|
+
// ── Approve ─────────────────────────────────────────────────────────
|
|
765
|
+
"POST /api/approve": (body) => {
|
|
766
|
+
const id = body?.id;
|
|
767
|
+
if (!id || typeof id !== "string") return { error: "id required" };
|
|
768
|
+
const ok = store.approve(id);
|
|
769
|
+
return ok ? { ok: true } : { error: "not found" };
|
|
770
|
+
},
|
|
771
|
+
// ── Reject ──────────────────────────────────────────────────────────
|
|
772
|
+
"POST /api/reject": (body) => {
|
|
773
|
+
const id = body?.id;
|
|
774
|
+
if (!id || typeof id !== "string") return { error: "id required" };
|
|
775
|
+
const ok = store.reject(id);
|
|
776
|
+
return ok ? { ok: true } : { error: "not found" };
|
|
777
|
+
},
|
|
778
|
+
// ── Disable (approved → rejected) ───────────────────────────────────
|
|
779
|
+
"POST /api/disable": (body) => {
|
|
780
|
+
const id = body?.id;
|
|
781
|
+
if (!id || typeof id !== "string") return { error: "id required" };
|
|
782
|
+
const ok = store.disable(id);
|
|
783
|
+
return ok ? { ok: true } : { error: "not found" };
|
|
784
|
+
},
|
|
785
|
+
// ── Remove permanently ──────────────────────────────────────────────
|
|
786
|
+
"DELETE /api/skill": (body) => {
|
|
787
|
+
const id = body?.id;
|
|
788
|
+
if (!id || typeof id !== "string") return { error: "id required" };
|
|
789
|
+
const ok = store.remove(id);
|
|
790
|
+
return ok ? { ok: true } : { error: "not found" };
|
|
791
|
+
},
|
|
792
|
+
// ── Trust ───────────────────────────────────────────────────────────
|
|
793
|
+
"GET /api/trust": () => trust.getConfig(),
|
|
794
|
+
"POST /api/trust": (body) => {
|
|
795
|
+
const { agent, level } = body ?? {};
|
|
796
|
+
if (!agent || typeof agent !== "string") return { error: "agent required" };
|
|
797
|
+
if (!["trusted", "pending", "blocked"].includes(level)) return { error: "level must be trusted/pending/blocked" };
|
|
798
|
+
trust.setLevel(agent, level);
|
|
799
|
+
return { ok: true };
|
|
800
|
+
},
|
|
801
|
+
"POST /api/trust/default": (body) => {
|
|
802
|
+
const { level } = body ?? {};
|
|
803
|
+
if (!["trusted", "pending", "blocked"].includes(level)) return { error: "level must be trusted/pending/blocked" };
|
|
804
|
+
trust.setDefault(level);
|
|
805
|
+
return { ok: true };
|
|
806
|
+
},
|
|
807
|
+
// ── Network ─────────────────────────────────────────────────────────
|
|
808
|
+
"GET /api/network": () => {
|
|
809
|
+
const approved = store.listApproved();
|
|
810
|
+
const pending = store.listPending();
|
|
811
|
+
const all = [...approved, ...pending];
|
|
812
|
+
const agents = {};
|
|
813
|
+
for (const skill of all) {
|
|
814
|
+
const id = skill.learned_from;
|
|
815
|
+
if (!agents[id]) {
|
|
816
|
+
agents[id] = { lessons: 0, skills: [], trust: trust.getLevel(id) };
|
|
817
|
+
}
|
|
818
|
+
agents[id].lessons++;
|
|
819
|
+
if (!agents[id].skills.includes(skill.name)) {
|
|
820
|
+
agents[id].skills.push(skill.name);
|
|
821
|
+
}
|
|
822
|
+
}
|
|
823
|
+
return agents;
|
|
824
|
+
},
|
|
825
|
+
// ── Stats ───────────────────────────────────────────────────────────
|
|
826
|
+
"GET /api/stats": () => {
|
|
827
|
+
const counts = trust.getDailyCounts();
|
|
828
|
+
return {
|
|
829
|
+
today_lessons: counts.total,
|
|
830
|
+
today_per_agent: counts.perAgent,
|
|
831
|
+
total_approved: store.listApproved().length,
|
|
832
|
+
total_pending: store.listPending().length,
|
|
833
|
+
total_rejected: store.listRejected().length,
|
|
834
|
+
proxy: getProxyStats()
|
|
835
|
+
};
|
|
836
|
+
}
|
|
837
|
+
};
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
// src/dashboard/ui.ts
|
|
841
|
+
function renderDashboardHTML() {
|
|
842
|
+
return `<!DOCTYPE html>
|
|
843
|
+
<html lang="en">
|
|
844
|
+
<head>
|
|
845
|
+
<meta charset="utf-8">
|
|
846
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
847
|
+
<title>become</title>
|
|
848
|
+
<style>
|
|
849
|
+
:root { --bg: #0a0a0a; --card: #141414; --border: #262626; --text: #e5e5e5; --dim: #737373; --accent: #22d3ee; --green: #22c55e; --red: #ef4444; --amber: #f59e0b; }
|
|
850
|
+
* { box-sizing: border-box; margin: 0; padding: 0; }
|
|
851
|
+
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; background: var(--bg); color: var(--text); line-height: 1.6; }
|
|
852
|
+
.container { max-width: 800px; margin: 0 auto; padding: 20px; }
|
|
853
|
+
h1 { font-size: 24px; font-weight: 600; margin-bottom: 4px; }
|
|
854
|
+
h2 { font-size: 18px; font-weight: 600; margin-bottom: 12px; color: var(--accent); }
|
|
855
|
+
.subtitle { color: var(--dim); font-size: 14px; margin-bottom: 24px; }
|
|
856
|
+
|
|
857
|
+
/* Nav */
|
|
858
|
+
nav { display: flex; gap: 4px; margin-bottom: 24px; border-bottom: 1px solid var(--border); padding-bottom: 8px; }
|
|
859
|
+
nav button { background: none; border: none; color: var(--dim); font-size: 14px; padding: 8px 16px; cursor: pointer; border-radius: 6px 6px 0 0; }
|
|
860
|
+
nav button:hover { color: var(--text); }
|
|
861
|
+
nav button.active { color: var(--accent); border-bottom: 2px solid var(--accent); }
|
|
862
|
+
|
|
863
|
+
/* Status bar */
|
|
864
|
+
.status-bar { display: flex; gap: 16px; align-items: center; margin-bottom: 24px; padding: 12px 16px; background: var(--card); border: 1px solid var(--border); border-radius: 8px; font-size: 13px; }
|
|
865
|
+
.status-dot { width: 8px; height: 8px; border-radius: 50%; }
|
|
866
|
+
.status-dot.on { background: var(--green); }
|
|
867
|
+
.status-dot.off { background: var(--red); }
|
|
868
|
+
.status-label { font-weight: 600; }
|
|
869
|
+
.status-stat { color: var(--dim); }
|
|
870
|
+
|
|
871
|
+
/* Cards */
|
|
872
|
+
.card { background: var(--card); border: 1px solid var(--border); border-radius: 8px; padding: 16px; margin-bottom: 12px; }
|
|
873
|
+
.card-header { display: flex; justify-content: space-between; align-items: flex-start; margin-bottom: 8px; }
|
|
874
|
+
.card-instruction { font-size: 15px; margin-bottom: 8px; }
|
|
875
|
+
.card-meta { font-size: 12px; color: var(--dim); display: flex; gap: 12px; flex-wrap: wrap; }
|
|
876
|
+
.card-meta span { display: inline-flex; align-items: center; gap: 4px; }
|
|
877
|
+
|
|
878
|
+
/* Buttons */
|
|
879
|
+
.btn { border: none; padding: 6px 14px; border-radius: 6px; font-size: 13px; cursor: pointer; font-weight: 500; }
|
|
880
|
+
.btn-approve { background: var(--green); color: #000; }
|
|
881
|
+
.btn-reject { background: var(--red); color: #fff; }
|
|
882
|
+
.btn-disable { background: var(--border); color: var(--text); }
|
|
883
|
+
.btn-trust { background: var(--accent); color: #000; }
|
|
884
|
+
.btn-small { padding: 4px 10px; font-size: 12px; }
|
|
885
|
+
.btn:hover { opacity: 0.85; }
|
|
886
|
+
.btn-group { display: flex; gap: 6px; }
|
|
887
|
+
|
|
888
|
+
/* Badge */
|
|
889
|
+
.badge { font-size: 11px; padding: 2px 8px; border-radius: 10px; font-weight: 500; }
|
|
890
|
+
.badge-trusted { background: rgba(34,197,94,0.15); color: var(--green); }
|
|
891
|
+
.badge-pending { background: rgba(245,158,11,0.15); color: var(--amber); }
|
|
892
|
+
.badge-blocked { background: rgba(239,68,68,0.15); color: var(--red); }
|
|
893
|
+
|
|
894
|
+
/* Empty state */
|
|
895
|
+
.empty { text-align: center; padding: 40px; color: var(--dim); }
|
|
896
|
+
|
|
897
|
+
/* Page sections */
|
|
898
|
+
.page { display: none; }
|
|
899
|
+
.page.active { display: block; }
|
|
900
|
+
|
|
901
|
+
/* Trust settings */
|
|
902
|
+
.trust-row { display: flex; justify-content: space-between; align-items: center; padding: 10px 0; border-bottom: 1px solid var(--border); }
|
|
903
|
+
.trust-row:last-child { border: none; }
|
|
904
|
+
.trust-agent { font-weight: 500; }
|
|
905
|
+
.trust-lessons { font-size: 13px; color: var(--dim); }
|
|
906
|
+
|
|
907
|
+
/* Skill group */
|
|
908
|
+
.skill-group { margin-bottom: 20px; }
|
|
909
|
+
.skill-group-name { font-size: 13px; text-transform: uppercase; color: var(--dim); letter-spacing: 0.5px; margin-bottom: 8px; }
|
|
910
|
+
|
|
911
|
+
/* Toggle */
|
|
912
|
+
.toggle { display: flex; align-items: center; gap: 12px; margin-bottom: 16px; }
|
|
913
|
+
.toggle-switch { width: 44px; height: 24px; background: var(--border); border-radius: 12px; position: relative; cursor: pointer; transition: background 0.2s; }
|
|
914
|
+
.toggle-switch.on { background: var(--green); }
|
|
915
|
+
.toggle-switch::after { content: ''; position: absolute; width: 18px; height: 18px; background: white; border-radius: 50%; top: 3px; left: 3px; transition: transform 0.2s; }
|
|
916
|
+
.toggle-switch.on::after { transform: translateX(20px); }
|
|
917
|
+
</style>
|
|
918
|
+
</head>
|
|
919
|
+
<body>
|
|
920
|
+
<div class="container">
|
|
921
|
+
<h1>become</h1>
|
|
922
|
+
<p class="subtitle">agent-to-agent learning</p>
|
|
93
923
|
|
|
94
|
-
|
|
924
|
+
<div class="status-bar" id="status-bar">
|
|
925
|
+
<div class="status-dot" id="status-dot"></div>
|
|
926
|
+
<span class="status-label" id="status-label">Loading...</span>
|
|
927
|
+
<span class="status-stat" id="status-skills"></span>
|
|
928
|
+
<span class="status-stat" id="status-pending"></span>
|
|
929
|
+
</div>
|
|
95
930
|
|
|
96
|
-
|
|
931
|
+
<nav>
|
|
932
|
+
<button class="active" onclick="showPage('pending',this)">Pending</button>
|
|
933
|
+
<button onclick="showPage('skills',this)">Active Skills</button>
|
|
934
|
+
<button onclick="showPage('network',this)">Network</button>
|
|
935
|
+
<button onclick="showPage('settings',this)">Settings</button>
|
|
936
|
+
</nav>
|
|
97
937
|
|
|
98
|
-
|
|
99
|
-
|
|
938
|
+
<!-- Pending Page -->
|
|
939
|
+
<div id="page-pending" class="page active"></div>
|
|
100
940
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
941
|
+
<!-- Skills Page -->
|
|
942
|
+
<div id="page-skills" class="page"></div>
|
|
943
|
+
|
|
944
|
+
<!-- Network Page -->
|
|
945
|
+
<div id="page-network" class="page"></div>
|
|
946
|
+
|
|
947
|
+
<!-- Settings Page -->
|
|
948
|
+
<div id="page-settings" class="page"></div>
|
|
949
|
+
</div>
|
|
950
|
+
|
|
951
|
+
<script>
|
|
952
|
+
const API = '';
|
|
953
|
+
|
|
954
|
+
async function api(method, path, body) {
|
|
955
|
+
const opts = { method, headers: { 'Content-Type': 'application/json' } };
|
|
956
|
+
if (body) opts.body = JSON.stringify(body);
|
|
957
|
+
const res = await fetch(API + path, opts);
|
|
958
|
+
return res.json();
|
|
959
|
+
}
|
|
960
|
+
|
|
961
|
+
function showPage(name, btn) {
|
|
962
|
+
document.querySelectorAll('.page').forEach(p => p.classList.remove('active'));
|
|
963
|
+
document.getElementById('page-' + name).classList.add('active');
|
|
964
|
+
document.querySelectorAll('nav button').forEach(b => b.classList.remove('active'));
|
|
965
|
+
if (btn) btn.classList.add('active');
|
|
966
|
+
if (name === 'pending') loadPending();
|
|
967
|
+
if (name === 'skills') loadSkills();
|
|
968
|
+
if (name === 'network') loadNetwork();
|
|
969
|
+
if (name === 'settings') loadSettings();
|
|
970
|
+
}
|
|
971
|
+
|
|
972
|
+
// \u2500\u2500 Status Bar \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
973
|
+
async function loadStatus() {
|
|
974
|
+
const s = await api('GET', '/api/status');
|
|
975
|
+
const dot = document.getElementById('status-dot');
|
|
976
|
+
const label = document.getElementById('status-label');
|
|
977
|
+
dot.className = 'status-dot ' + s.state;
|
|
978
|
+
label.textContent = s.state.toUpperCase();
|
|
979
|
+
document.getElementById('status-skills').textContent = s.skills_count + ' skills';
|
|
980
|
+
document.getElementById('status-pending').textContent = s.pending_count + ' pending';
|
|
981
|
+
}
|
|
982
|
+
|
|
983
|
+
// \u2500\u2500 Pending Page \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
984
|
+
async function loadPending() {
|
|
985
|
+
const lessons = await api('GET', '/api/pending');
|
|
986
|
+
const el = document.getElementById('page-pending');
|
|
987
|
+
if (lessons.length === 0) {
|
|
988
|
+
el.innerHTML = '<div class="empty">No pending lessons. Your agent will learn as it talks to other agents.</div>';
|
|
989
|
+
return;
|
|
990
|
+
}
|
|
991
|
+
el.innerHTML = '<h2>Pending Review (' + lessons.length + ')</h2>' +
|
|
992
|
+
lessons.map(l => renderPendingCard(l)).join('');
|
|
993
|
+
}
|
|
994
|
+
|
|
995
|
+
function renderPendingCard(l) {
|
|
996
|
+
return '<div class="card" id="card-' + l.id + '">' +
|
|
997
|
+
'<div class="card-instruction">' + esc(l.instruction) + '</div>' +
|
|
998
|
+
'<div class="card-meta">' +
|
|
999
|
+
'<span>From: <strong>' + esc(l.learned_from) + '</strong></span>' +
|
|
1000
|
+
'<span>Source: ' + esc(l.source) + '</span>' +
|
|
1001
|
+
'<span>Confidence: ' + (l.confidence * 100).toFixed(0) + '%</span>' +
|
|
1002
|
+
'<span>Skill: ' + esc(l.name) + '</span>' +
|
|
1003
|
+
'</div>' +
|
|
1004
|
+
'<div style="margin-top:12px" class="btn-group">' +
|
|
1005
|
+
'<button class="btn btn-approve" onclick="doApprove(\\''+l.id+'\\')">Approve</button>' +
|
|
1006
|
+
'<button class="btn btn-reject" onclick="doReject(\\''+l.id+'\\')">Reject</button>' +
|
|
1007
|
+
'<button class="btn btn-trust btn-small" onclick="doTrustAgent(\\''+esc(l.learned_from)+'\\')">Trust Agent</button>' +
|
|
1008
|
+
'</div>' +
|
|
1009
|
+
'</div>';
|
|
1010
|
+
}
|
|
1011
|
+
|
|
1012
|
+
async function doApprove(id) {
|
|
1013
|
+
await api('POST', '/api/approve', { id });
|
|
1014
|
+
document.getElementById('card-' + id)?.remove();
|
|
1015
|
+
loadStatus();
|
|
1016
|
+
}
|
|
1017
|
+
|
|
1018
|
+
async function doReject(id) {
|
|
1019
|
+
await api('POST', '/api/reject', { id });
|
|
1020
|
+
document.getElementById('card-' + id)?.remove();
|
|
1021
|
+
loadStatus();
|
|
1022
|
+
}
|
|
1023
|
+
|
|
1024
|
+
async function doTrustAgent(agent) {
|
|
1025
|
+
await api('POST', '/api/trust', { agent, level: 'trusted' });
|
|
1026
|
+
loadPending();
|
|
1027
|
+
loadStatus();
|
|
1028
|
+
}
|
|
1029
|
+
|
|
1030
|
+
// \u2500\u2500 Skills Page \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
1031
|
+
async function loadSkills() {
|
|
1032
|
+
const skills = await api('GET', '/api/skills');
|
|
1033
|
+
const el = document.getElementById('page-skills');
|
|
1034
|
+
if (skills.length === 0) {
|
|
1035
|
+
el.innerHTML = '<div class="empty">No active skills yet. Approve pending lessons to activate them.</div>';
|
|
1036
|
+
return;
|
|
1037
|
+
}
|
|
1038
|
+
|
|
1039
|
+
// Group by skill name
|
|
1040
|
+
const groups = {};
|
|
1041
|
+
for (const s of skills) {
|
|
1042
|
+
if (!groups[s.name]) groups[s.name] = [];
|
|
1043
|
+
groups[s.name].push(s);
|
|
1044
|
+
}
|
|
1045
|
+
|
|
1046
|
+
let html = '<h2>Active Skills (' + skills.length + ')</h2>';
|
|
1047
|
+
for (const [name, items] of Object.entries(groups)) {
|
|
1048
|
+
html += '<div class="skill-group"><div class="skill-group-name">' + esc(name) + '</div>';
|
|
1049
|
+
for (const s of items) {
|
|
1050
|
+
html += '<div class="card" id="card-' + s.id + '">' +
|
|
1051
|
+
'<div class="card-header"><div class="card-instruction">' + esc(s.instruction) + '</div>' +
|
|
1052
|
+
'<button class="btn btn-disable btn-small" onclick="doDisable(\\''+s.id+'\\')">Disable</button></div>' +
|
|
1053
|
+
'<div class="card-meta"><span>From: ' + esc(s.learned_from) + '</span><span>Source: ' + esc(s.source) + '</span></div>' +
|
|
1054
|
+
'</div>';
|
|
1055
|
+
}
|
|
1056
|
+
html += '</div>';
|
|
1057
|
+
}
|
|
1058
|
+
el.innerHTML = html;
|
|
1059
|
+
}
|
|
1060
|
+
|
|
1061
|
+
async function doDisable(id) {
|
|
1062
|
+
await api('POST', '/api/disable', { id });
|
|
1063
|
+
document.getElementById('card-' + id)?.remove();
|
|
1064
|
+
loadStatus();
|
|
1065
|
+
}
|
|
1066
|
+
|
|
1067
|
+
// \u2500\u2500 Network Page \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
1068
|
+
async function loadNetwork() {
|
|
1069
|
+
const network = await api('GET', '/api/network');
|
|
1070
|
+
const el = document.getElementById('page-network');
|
|
1071
|
+
const entries = Object.entries(network);
|
|
1072
|
+
if (entries.length === 0) {
|
|
1073
|
+
el.innerHTML = '<div class="empty">No agents have taught yours yet.</div>';
|
|
1074
|
+
return;
|
|
1075
|
+
}
|
|
1076
|
+
|
|
1077
|
+
let html = '<h2>Who Taught Your Agent</h2>';
|
|
1078
|
+
entries.sort((a, b) => b[1].lessons - a[1].lessons);
|
|
1079
|
+
for (const [agent, data] of entries) {
|
|
1080
|
+
const d = data;
|
|
1081
|
+
const badgeClass = 'badge badge-' + d.trust;
|
|
1082
|
+
html += '<div class="card"><div class="trust-row">' +
|
|
1083
|
+
'<div><div class="trust-agent">' + esc(agent) + ' <span class="' + badgeClass + '">' + d.trust + '</span></div>' +
|
|
1084
|
+
'<div class="trust-lessons">' + d.lessons + ' lesson(s): ' + d.skills.map(esc).join(', ') + '</div></div>' +
|
|
1085
|
+
'<div class="btn-group">' +
|
|
1086
|
+
(d.trust !== 'trusted' ? '<button class="btn btn-trust btn-small" onclick="setTrust(\\''+esc(agent)+'\\',\\'trusted\\')">Trust</button>' : '') +
|
|
1087
|
+
(d.trust !== 'blocked' ? '<button class="btn btn-reject btn-small" onclick="setTrust(\\''+esc(agent)+'\\',\\'blocked\\')">Block</button>' : '') +
|
|
1088
|
+
(d.trust !== 'pending' ? '<button class="btn btn-disable btn-small" onclick="setTrust(\\''+esc(agent)+'\\',\\'pending\\')">Reset</button>' : '') +
|
|
1089
|
+
'</div>' +
|
|
1090
|
+
'</div></div>';
|
|
1091
|
+
}
|
|
1092
|
+
el.innerHTML = html;
|
|
1093
|
+
}
|
|
1094
|
+
|
|
1095
|
+
async function setTrust(agent, level) {
|
|
1096
|
+
await api('POST', '/api/trust', { agent, level });
|
|
1097
|
+
loadNetwork();
|
|
1098
|
+
}
|
|
1099
|
+
|
|
1100
|
+
// \u2500\u2500 Settings Page \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
1101
|
+
async function loadSettings() {
|
|
1102
|
+
const status = await api('GET', '/api/status');
|
|
1103
|
+
const trust = await api('GET', '/api/trust');
|
|
1104
|
+
const stats = await api('GET', '/api/stats');
|
|
1105
|
+
|
|
1106
|
+
const el = document.getElementById('page-settings');
|
|
1107
|
+
el.innerHTML = '<h2>Settings</h2>' +
|
|
1108
|
+
'<div class="card">' +
|
|
1109
|
+
'<div class="toggle">' +
|
|
1110
|
+
'<div class="toggle-switch ' + status.state + '" id="toggle-state" onclick="toggleState()"></div>' +
|
|
1111
|
+
'<span>Proxy is <strong>' + status.state.toUpperCase() + '</strong></span>' +
|
|
1112
|
+
'</div>' +
|
|
1113
|
+
'</div>' +
|
|
1114
|
+
'<div class="card">' +
|
|
1115
|
+
'<h2 style="margin-bottom:8px">Default Trust</h2>' +
|
|
1116
|
+
'<div class="card-meta" style="margin-bottom:12px">What happens when a new agent teaches your agent</div>' +
|
|
1117
|
+
'<div class="btn-group">' +
|
|
1118
|
+
'<button class="btn ' + (trust.default === 'pending' ? 'btn-approve' : 'btn-disable') + '" onclick="setDefaultTrust(\\'pending\\')">Pending (review)</button>' +
|
|
1119
|
+
'<button class="btn ' + (trust.default === 'trusted' ? 'btn-approve' : 'btn-disable') + '" onclick="setDefaultTrust(\\'trusted\\')">Auto-approve</button>' +
|
|
1120
|
+
'<button class="btn ' + (trust.default === 'blocked' ? 'btn-approve' : 'btn-disable') + '" onclick="setDefaultTrust(\\'blocked\\')">Block all</button>' +
|
|
1121
|
+
'</div>' +
|
|
1122
|
+
'</div>' +
|
|
1123
|
+
'<div class="card">' +
|
|
1124
|
+
'<h2 style="margin-bottom:8px">Stats</h2>' +
|
|
1125
|
+
'<div class="card-meta" style="flex-direction:column;gap:4px">' +
|
|
1126
|
+
'<span>Approved: ' + stats.total_approved + '</span>' +
|
|
1127
|
+
'<span>Pending: ' + stats.total_pending + '</span>' +
|
|
1128
|
+
'<span>Rejected: ' + stats.total_rejected + '</span>' +
|
|
1129
|
+
'<span>Lessons today: ' + stats.today_lessons + '</span>' +
|
|
1130
|
+
'</div>' +
|
|
1131
|
+
'</div>' +
|
|
1132
|
+
'<div class="card">' +
|
|
1133
|
+
'<h2 style="margin-bottom:8px;color:var(--red)">Danger Zone</h2>' +
|
|
1134
|
+
'<div class="btn-group">' +
|
|
1135
|
+
'<button class="btn btn-reject" onclick="if(confirm(\\'Clear all skills?\\'))clearAll()">Clear All Skills</button>' +
|
|
1136
|
+
'</div>' +
|
|
1137
|
+
'</div>';
|
|
1138
|
+
}
|
|
1139
|
+
|
|
1140
|
+
async function toggleState() {
|
|
1141
|
+
const el = document.getElementById('toggle-state');
|
|
1142
|
+
const newState = el.classList.contains('on') ? 'off' : 'on';
|
|
1143
|
+
await api('POST', '/api/state', { state: newState });
|
|
1144
|
+
loadStatus();
|
|
1145
|
+
loadSettings();
|
|
1146
|
+
}
|
|
1147
|
+
|
|
1148
|
+
async function setDefaultTrust(level) {
|
|
1149
|
+
await api('POST', '/api/trust/default', { level });
|
|
1150
|
+
loadSettings();
|
|
1151
|
+
}
|
|
1152
|
+
|
|
1153
|
+
async function clearAll() {
|
|
1154
|
+
const skills = await api('GET', '/api/skills');
|
|
1155
|
+
for (const s of skills) {
|
|
1156
|
+
await api('DELETE', '/api/skill', { id: s.id });
|
|
1157
|
+
}
|
|
1158
|
+
loadStatus();
|
|
1159
|
+
loadSettings();
|
|
1160
|
+
}
|
|
1161
|
+
|
|
1162
|
+
function esc(s) {
|
|
1163
|
+
if (!s) return '';
|
|
1164
|
+
return String(s).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"').replace(/'/g,''');
|
|
1165
|
+
}
|
|
1166
|
+
|
|
1167
|
+
// Init
|
|
1168
|
+
loadStatus();
|
|
1169
|
+
loadPending();
|
|
1170
|
+
setInterval(loadStatus, 10000);
|
|
1171
|
+
</script>
|
|
1172
|
+
</body>
|
|
1173
|
+
</html>`;
|
|
1174
|
+
}
|
|
1175
|
+
|
|
1176
|
+
// src/dashboard/server.ts
|
|
1177
|
+
function createDashboardServer(deps) {
|
|
1178
|
+
const handlers = createHandlers(deps);
|
|
1179
|
+
const html = renderDashboardHTML();
|
|
1180
|
+
const server = (0, import_node_http2.createServer)(async (req, res) => {
|
|
1181
|
+
const origin = req.headers.origin ?? "";
|
|
1182
|
+
if (origin && /^https?:\/\/(localhost|127\.0\.0\.1)(:\d+)?$/.test(origin)) {
|
|
1183
|
+
res.setHeader("Access-Control-Allow-Origin", origin);
|
|
1184
|
+
}
|
|
1185
|
+
res.setHeader("Access-Control-Allow-Methods", "GET, POST, DELETE, OPTIONS");
|
|
1186
|
+
res.setHeader("Access-Control-Allow-Headers", "Content-Type");
|
|
1187
|
+
if (req.method === "OPTIONS") {
|
|
1188
|
+
res.writeHead(204);
|
|
1189
|
+
res.end();
|
|
1190
|
+
return;
|
|
1191
|
+
}
|
|
1192
|
+
if (req.url === "/" && req.method === "GET") {
|
|
1193
|
+
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
|
|
1194
|
+
res.end(html);
|
|
1195
|
+
return;
|
|
1196
|
+
}
|
|
1197
|
+
const key = `${req.method} ${req.url}`;
|
|
1198
|
+
const handler = handlers[key];
|
|
1199
|
+
if (!handler) {
|
|
1200
|
+
res.writeHead(404, { "Content-Type": "application/json" });
|
|
1201
|
+
res.end(JSON.stringify({ error: "Not found" }));
|
|
1202
|
+
return;
|
|
1203
|
+
}
|
|
1204
|
+
try {
|
|
1205
|
+
let body;
|
|
1206
|
+
if (req.method === "POST" || req.method === "DELETE") {
|
|
1207
|
+
const raw = await readBody2(req);
|
|
1208
|
+
if (raw) {
|
|
1209
|
+
try {
|
|
1210
|
+
body = JSON.parse(raw);
|
|
1211
|
+
} catch {
|
|
1212
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
1213
|
+
res.end(JSON.stringify({ error: "Invalid JSON body" }));
|
|
1214
|
+
return;
|
|
1215
|
+
}
|
|
1216
|
+
}
|
|
1217
|
+
}
|
|
1218
|
+
const result = await handler(body);
|
|
1219
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
1220
|
+
res.end(JSON.stringify(result));
|
|
1221
|
+
} catch (err) {
|
|
1222
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
1223
|
+
res.end(JSON.stringify({ error: "Internal server error" }));
|
|
1224
|
+
}
|
|
1225
|
+
});
|
|
1226
|
+
return {
|
|
1227
|
+
server,
|
|
1228
|
+
listen: (port) => {
|
|
1229
|
+
return new Promise((resolve) => {
|
|
1230
|
+
server.listen(port, "127.0.0.1", () => resolve());
|
|
1231
|
+
});
|
|
1232
|
+
},
|
|
1233
|
+
close: () => new Promise((resolve) => server.close(() => resolve()))
|
|
1234
|
+
};
|
|
1235
|
+
}
|
|
1236
|
+
function readBody2(req) {
|
|
1237
|
+
return new Promise((resolve, reject) => {
|
|
1238
|
+
const chunks = [];
|
|
1239
|
+
let size = 0;
|
|
1240
|
+
req.on("data", (chunk) => {
|
|
1241
|
+
size += chunk.length;
|
|
1242
|
+
if (size > 1024 * 1024) {
|
|
1243
|
+
req.destroy();
|
|
1244
|
+
reject(new Error("Too large"));
|
|
1245
|
+
return;
|
|
1246
|
+
}
|
|
1247
|
+
chunks.push(chunk);
|
|
1248
|
+
});
|
|
1249
|
+
req.on("end", () => resolve(Buffer.concat(chunks).toString("utf-8")));
|
|
1250
|
+
req.on("error", reject);
|
|
1251
|
+
});
|
|
1252
|
+
}
|
|
1253
|
+
|
|
1254
|
+
// src/cli/adapter/openclaw.ts
|
|
1255
|
+
var import_node_fs4 = require("fs");
|
|
1256
|
+
var import_node_path4 = require("path");
|
|
1257
|
+
var import_node_os2 = require("os");
|
|
1258
|
+
var import_node_child_process = require("child_process");
|
|
1259
|
+
var OPENCLAW_CONFIG = (0, import_node_path4.join)((0, import_node_os2.homedir)(), ".openclaw", "openclaw.json");
|
|
1260
|
+
var BACKUP_PATH = (0, import_node_path4.join)((0, import_node_os2.homedir)(), ".become", "state", "original_openclaw.json");
|
|
1261
|
+
function patchOpenClaw(config) {
|
|
1262
|
+
if (!(0, import_node_fs4.existsSync)(OPENCLAW_CONFIG)) {
|
|
1263
|
+
throw new Error(`OpenClaw config not found at ${OPENCLAW_CONFIG}`);
|
|
1264
|
+
}
|
|
1265
|
+
const raw = (0, import_node_fs4.readFileSync)(OPENCLAW_CONFIG, "utf-8");
|
|
1266
|
+
const clawConfig = JSON.parse(raw);
|
|
1267
|
+
(0, import_node_fs4.mkdirSync)((0, import_node_path4.join)((0, import_node_os2.homedir)(), ".become", "state"), { recursive: true });
|
|
1268
|
+
(0, import_node_fs4.writeFileSync)(BACKUP_PATH, raw, "utf-8");
|
|
1269
|
+
if (!clawConfig.models) clawConfig.models = {};
|
|
1270
|
+
if (!clawConfig.models.providers) clawConfig.models.providers = {};
|
|
1271
|
+
clawConfig.models.providers.become = {
|
|
1272
|
+
api: "anthropic-messages",
|
|
1273
|
+
baseUrl: `http://127.0.0.1:${config.proxy_port}`,
|
|
1274
|
+
apiKey: config.llm_api_key
|
|
1275
|
+
};
|
|
1276
|
+
if (clawConfig.agents?.defaults?.model?.primary) {
|
|
1277
|
+
const original = clawConfig.agents.defaults.model.primary;
|
|
1278
|
+
clawConfig.models.providers.become._originalModel = original;
|
|
1279
|
+
const modelId = original.includes("/") ? original.split("/").slice(1).join("/") : original;
|
|
1280
|
+
clawConfig.agents.defaults.model.primary = `become/${modelId}`;
|
|
1281
|
+
}
|
|
1282
|
+
(0, import_node_fs4.writeFileSync)(OPENCLAW_CONFIG, JSON.stringify(clawConfig, null, 2), "utf-8");
|
|
1283
|
+
try {
|
|
1284
|
+
(0, import_node_child_process.execSync)("openclaw gateway restart", { stdio: "pipe", timeout: 15e3 });
|
|
1285
|
+
} catch {
|
|
1286
|
+
console.log("Warning: Could not restart OpenClaw gateway. Restart it manually: openclaw gateway restart");
|
|
1287
|
+
}
|
|
1288
|
+
}
|
|
1289
|
+
function restoreOpenClaw() {
|
|
1290
|
+
if (!(0, import_node_fs4.existsSync)(BACKUP_PATH)) {
|
|
1291
|
+
throw new Error("No backup found. Was become ever turned on?");
|
|
1292
|
+
}
|
|
1293
|
+
const backup = (0, import_node_fs4.readFileSync)(BACKUP_PATH, "utf-8");
|
|
1294
|
+
(0, import_node_fs4.writeFileSync)(OPENCLAW_CONFIG, backup, "utf-8");
|
|
1295
|
+
try {
|
|
1296
|
+
(0, import_node_child_process.execSync)("openclaw gateway restart", { stdio: "pipe", timeout: 15e3 });
|
|
1297
|
+
} catch {
|
|
1298
|
+
console.log("Warning: Could not restart OpenClaw gateway. Restart it manually: openclaw gateway restart");
|
|
1299
|
+
}
|
|
1300
|
+
}
|
|
1301
|
+
|
|
1302
|
+
// src/cli/adapter/ironclaw.ts
|
|
1303
|
+
var import_node_fs5 = require("fs");
|
|
1304
|
+
var import_node_path5 = require("path");
|
|
1305
|
+
var import_node_os3 = require("os");
|
|
1306
|
+
var import_node_child_process2 = require("child_process");
|
|
1307
|
+
var IRONCLAW_ENV = (0, import_node_path5.join)((0, import_node_os3.homedir)(), ".ironclaw", ".env");
|
|
1308
|
+
var BACKUP_PATH2 = (0, import_node_path5.join)((0, import_node_os3.homedir)(), ".become", "state", "original_ironclaw.env");
|
|
1309
|
+
function patchIronClaw(config) {
|
|
1310
|
+
if (!(0, import_node_fs5.existsSync)(IRONCLAW_ENV)) {
|
|
1311
|
+
throw new Error(`IronClaw .env not found at ${IRONCLAW_ENV}`);
|
|
1312
|
+
}
|
|
1313
|
+
(0, import_node_fs5.mkdirSync)((0, import_node_path5.join)((0, import_node_os3.homedir)(), ".become", "state"), { recursive: true });
|
|
1314
|
+
(0, import_node_fs5.copyFileSync)(IRONCLAW_ENV, BACKUP_PATH2);
|
|
1315
|
+
patchDotEnv(IRONCLAW_ENV, {
|
|
1316
|
+
LLM_BASE_URL: `http://127.0.0.1:${config.proxy_port}/v1`
|
|
107
1317
|
});
|
|
1318
|
+
try {
|
|
1319
|
+
(0, import_node_child_process2.execSync)("ironclaw service restart", { stdio: "pipe", timeout: 15e3 });
|
|
1320
|
+
} catch {
|
|
1321
|
+
console.log("Warning: Could not restart IronClaw. Restart it manually: ironclaw service restart");
|
|
1322
|
+
}
|
|
1323
|
+
}
|
|
1324
|
+
function restoreIronClaw() {
|
|
1325
|
+
if (!(0, import_node_fs5.existsSync)(BACKUP_PATH2)) {
|
|
1326
|
+
throw new Error("No backup found. Was become ever turned on?");
|
|
1327
|
+
}
|
|
1328
|
+
(0, import_node_fs5.copyFileSync)(BACKUP_PATH2, IRONCLAW_ENV);
|
|
1329
|
+
try {
|
|
1330
|
+
(0, import_node_child_process2.execSync)("ironclaw service restart", { stdio: "pipe", timeout: 15e3 });
|
|
1331
|
+
} catch {
|
|
1332
|
+
console.log("Warning: Could not restart IronClaw. Restart it manually: ironclaw service restart");
|
|
1333
|
+
}
|
|
1334
|
+
}
|
|
1335
|
+
function patchDotEnv(path, vars) {
|
|
1336
|
+
let content = (0, import_node_fs5.readFileSync)(path, "utf-8");
|
|
1337
|
+
for (const [key, value] of Object.entries(vars)) {
|
|
1338
|
+
const regex = new RegExp(`^${key}=.*$`, "m");
|
|
1339
|
+
if (regex.test(content)) {
|
|
1340
|
+
content = content.replace(regex, `${key}=${value}`);
|
|
1341
|
+
} else {
|
|
1342
|
+
content += `
|
|
1343
|
+
${key}=${value}`;
|
|
1344
|
+
}
|
|
1345
|
+
}
|
|
1346
|
+
(0, import_node_fs5.writeFileSync)(path, content, "utf-8");
|
|
1347
|
+
}
|
|
108
1348
|
|
|
109
|
-
|
|
1349
|
+
// src/cli/adapter/nanoclaw.ts
|
|
1350
|
+
var import_node_fs6 = require("fs");
|
|
1351
|
+
var import_node_path6 = require("path");
|
|
1352
|
+
var import_node_os4 = require("os");
|
|
1353
|
+
var import_node_child_process3 = require("child_process");
|
|
1354
|
+
var BACKUP_PATH3 = (0, import_node_path6.join)((0, import_node_os4.homedir)(), ".become", "state", "original_nanoclaw.env");
|
|
1355
|
+
function patchNanoClaw(config) {
|
|
1356
|
+
const envPath = findNanoClawEnv();
|
|
1357
|
+
if (!envPath) {
|
|
1358
|
+
throw new Error("Could not find NanoClaw .env. Set ANTHROPIC_BASE_URL manually to http://127.0.0.1:" + config.proxy_port);
|
|
1359
|
+
}
|
|
1360
|
+
(0, import_node_fs6.mkdirSync)((0, import_node_path6.join)((0, import_node_os4.homedir)(), ".become", "state"), { recursive: true });
|
|
1361
|
+
(0, import_node_fs6.copyFileSync)(envPath, BACKUP_PATH3);
|
|
1362
|
+
patchDotEnv2(envPath, {
|
|
1363
|
+
ANTHROPIC_BASE_URL: `http://127.0.0.1:${config.proxy_port}`
|
|
1364
|
+
});
|
|
1365
|
+
restartNanoClaw();
|
|
1366
|
+
}
|
|
1367
|
+
function restoreNanoClaw() {
|
|
1368
|
+
const envPath = findNanoClawEnv();
|
|
1369
|
+
if (!(0, import_node_fs6.existsSync)(BACKUP_PATH3) || !envPath) {
|
|
1370
|
+
throw new Error("No backup found. Was become ever turned on?");
|
|
1371
|
+
}
|
|
1372
|
+
(0, import_node_fs6.copyFileSync)(BACKUP_PATH3, envPath);
|
|
1373
|
+
restartNanoClaw();
|
|
1374
|
+
}
|
|
1375
|
+
function findNanoClawEnv() {
|
|
1376
|
+
const candidates = [
|
|
1377
|
+
(0, import_node_path6.join)((0, import_node_os4.homedir)(), ".nanoclaw", ".env"),
|
|
1378
|
+
(0, import_node_path6.join)((0, import_node_os4.homedir)(), ".config", "nanoclaw", ".env")
|
|
1379
|
+
];
|
|
1380
|
+
const plistPath = (0, import_node_path6.join)((0, import_node_os4.homedir)(), "Library", "LaunchAgents", "ai.nanoclaw.agent.plist");
|
|
1381
|
+
if ((0, import_node_fs6.existsSync)(plistPath)) {
|
|
1382
|
+
try {
|
|
1383
|
+
const plist = (0, import_node_fs6.readFileSync)(plistPath, "utf-8");
|
|
1384
|
+
const match = plist.match(/<string>([^<]*\.env)<\/string>/);
|
|
1385
|
+
if (match) candidates.unshift(match[1]);
|
|
1386
|
+
} catch {
|
|
1387
|
+
}
|
|
1388
|
+
}
|
|
1389
|
+
for (const path of candidates) {
|
|
1390
|
+
if ((0, import_node_fs6.existsSync)(path)) return path;
|
|
1391
|
+
}
|
|
1392
|
+
return null;
|
|
1393
|
+
}
|
|
1394
|
+
function restartNanoClaw() {
|
|
1395
|
+
try {
|
|
1396
|
+
(0, import_node_child_process3.execSync)("launchctl kickstart -k gui/$(id -u)/ai.nanoclaw.agent", { stdio: "pipe", timeout: 15e3 });
|
|
1397
|
+
} catch {
|
|
1398
|
+
try {
|
|
1399
|
+
(0, import_node_child_process3.execSync)("systemctl --user restart nanoclaw", { stdio: "pipe", timeout: 15e3 });
|
|
1400
|
+
} catch {
|
|
1401
|
+
console.log("Warning: Could not restart NanoClaw. Restart it manually.");
|
|
1402
|
+
}
|
|
1403
|
+
}
|
|
1404
|
+
}
|
|
1405
|
+
function patchDotEnv2(path, vars) {
|
|
1406
|
+
let content = (0, import_node_fs6.readFileSync)(path, "utf-8");
|
|
1407
|
+
for (const [key, value] of Object.entries(vars)) {
|
|
1408
|
+
const regex = new RegExp(`^${key}=.*$`, "m");
|
|
1409
|
+
if (regex.test(content)) {
|
|
1410
|
+
content = content.replace(regex, `${key}=${value}`);
|
|
1411
|
+
} else {
|
|
1412
|
+
content += `
|
|
1413
|
+
${key}=${value}`;
|
|
1414
|
+
}
|
|
1415
|
+
}
|
|
1416
|
+
(0, import_node_fs6.writeFileSync)(path, content, "utf-8");
|
|
1417
|
+
}
|
|
1418
|
+
|
|
1419
|
+
// src/cli/commands.ts
|
|
1420
|
+
async function start() {
|
|
1421
|
+
const config = loadConfig();
|
|
1422
|
+
const baseDir = getBecomeDir();
|
|
1423
|
+
const proxyConfig = {
|
|
1424
|
+
port: config.proxy_port,
|
|
1425
|
+
llm_base_url: config.llm_base_url,
|
|
1426
|
+
llm_api_key: config.llm_api_key,
|
|
1427
|
+
llm_provider: config.llm_provider,
|
|
1428
|
+
baseDir,
|
|
1429
|
+
max_skills_per_call: config.max_skills_per_call,
|
|
1430
|
+
auto_extract: config.auto_extract
|
|
1431
|
+
};
|
|
1432
|
+
const proxy = createProxyServer(proxyConfig);
|
|
1433
|
+
await proxy.listen();
|
|
1434
|
+
const dashboard = createDashboardServer({
|
|
1435
|
+
store: proxy.store,
|
|
1436
|
+
trust: proxy.trust,
|
|
1437
|
+
getProxyStats: () => proxy.stats,
|
|
1438
|
+
getState: () => {
|
|
1439
|
+
try {
|
|
1440
|
+
return loadConfig().state;
|
|
1441
|
+
} catch {
|
|
1442
|
+
return "off";
|
|
1443
|
+
}
|
|
1444
|
+
},
|
|
1445
|
+
setState: (state) => {
|
|
1446
|
+
try {
|
|
1447
|
+
if (state === "on") turnOn();
|
|
1448
|
+
else turnOff();
|
|
1449
|
+
} catch (e) {
|
|
1450
|
+
console.error("State change failed:", e);
|
|
1451
|
+
}
|
|
1452
|
+
}
|
|
1453
|
+
});
|
|
1454
|
+
await dashboard.listen(config.dashboard_port);
|
|
1455
|
+
const approved = proxy.store.listApproved().length;
|
|
1456
|
+
const pending = proxy.store.listPending().length;
|
|
1457
|
+
const trustConfig = proxy.trust.getConfig();
|
|
1458
|
+
console.log(`
|
|
1459
|
+
become proxy running on localhost:${config.proxy_port}`);
|
|
1460
|
+
console.log(`become dashboard at http://localhost:${config.dashboard_port}`);
|
|
1461
|
+
console.log(`
|
|
1462
|
+
Skills loaded: ${approved} approved, ${pending} pending`);
|
|
1463
|
+
console.log(`Trust rules: ${trustConfig.trusted.length} trusted, ${trustConfig.blocked.length} blocked`);
|
|
1464
|
+
if (config.state === "on") {
|
|
1465
|
+
console.log("\nProxy is ACTIVE \u2014 your agent is learning from other agents.");
|
|
1466
|
+
} else {
|
|
1467
|
+
console.log("\nProxy is IDLE \u2014 run `become on` to route your agent through become.");
|
|
1468
|
+
}
|
|
1469
|
+
console.log("Use `become off` to disconnect. Ctrl+C to stop.\n");
|
|
1470
|
+
const shutdown = async () => {
|
|
1471
|
+
console.log("\nShutting down...");
|
|
1472
|
+
await Promise.all([proxy.close(), dashboard.close()]);
|
|
1473
|
+
process.exit(0);
|
|
1474
|
+
};
|
|
1475
|
+
process.on("SIGINT", shutdown);
|
|
1476
|
+
process.on("SIGTERM", shutdown);
|
|
1477
|
+
}
|
|
1478
|
+
function turnOn() {
|
|
1479
|
+
const config = loadConfig();
|
|
1480
|
+
console.log(`
|
|
1481
|
+
Patching ${config.agent_type} config...`);
|
|
1482
|
+
console.log(` baseUrl: ${config.llm_base_url} \u2192 localhost:${config.proxy_port}`);
|
|
1483
|
+
switch (config.agent_type) {
|
|
1484
|
+
case "openclaw":
|
|
1485
|
+
patchOpenClaw(config);
|
|
1486
|
+
break;
|
|
1487
|
+
case "ironclaw":
|
|
1488
|
+
patchIronClaw(config);
|
|
1489
|
+
break;
|
|
1490
|
+
case "nanoclaw":
|
|
1491
|
+
patchNanoClaw(config);
|
|
1492
|
+
break;
|
|
1493
|
+
case "generic":
|
|
1494
|
+
console.log(`
|
|
1495
|
+
Set these env vars in your agent's config:`);
|
|
1496
|
+
console.log(` OPENAI_BASE_URL=http://127.0.0.1:${config.proxy_port}/v1`);
|
|
1497
|
+
console.log(` ANTHROPIC_BASE_URL=http://127.0.0.1:${config.proxy_port}`);
|
|
1498
|
+
console.log(`Then restart your agent.
|
|
110
1499
|
`);
|
|
1500
|
+
break;
|
|
1501
|
+
}
|
|
1502
|
+
config.state = "on";
|
|
1503
|
+
saveConfig(config);
|
|
1504
|
+
console.log("\nbecome is ON. Your agent is now learning from other agents.\n");
|
|
1505
|
+
}
|
|
1506
|
+
function turnOff() {
|
|
1507
|
+
const config = loadConfig();
|
|
1508
|
+
console.log(`
|
|
1509
|
+
Restoring ${config.agent_type} config...`);
|
|
1510
|
+
console.log(` baseUrl: localhost:${config.proxy_port} \u2192 ${config.llm_base_url}`);
|
|
1511
|
+
switch (config.agent_type) {
|
|
1512
|
+
case "openclaw":
|
|
1513
|
+
restoreOpenClaw();
|
|
1514
|
+
break;
|
|
1515
|
+
case "ironclaw":
|
|
1516
|
+
restoreIronClaw();
|
|
1517
|
+
break;
|
|
1518
|
+
case "nanoclaw":
|
|
1519
|
+
restoreNanoClaw();
|
|
1520
|
+
break;
|
|
1521
|
+
case "generic":
|
|
1522
|
+
console.log(`
|
|
1523
|
+
Restore your original env vars and restart your agent.
|
|
1524
|
+
`);
|
|
1525
|
+
break;
|
|
1526
|
+
}
|
|
1527
|
+
config.state = "off";
|
|
1528
|
+
saveConfig(config);
|
|
1529
|
+
console.log("\nbecome is OFF. Your agent talks directly to the LLM.");
|
|
1530
|
+
console.log("Learned skills are preserved \u2014 they'll be injected when you turn become back on.\n");
|
|
1531
|
+
}
|
|
1532
|
+
function showStatus() {
|
|
1533
|
+
const config = loadConfig();
|
|
1534
|
+
const baseDir = getBecomeDir();
|
|
1535
|
+
const store = new FileSkillStore({ baseDir });
|
|
1536
|
+
const trust = new TrustManager(baseDir);
|
|
1537
|
+
const approved = store.listApproved().length;
|
|
1538
|
+
const pending = store.listPending().length;
|
|
1539
|
+
const rejected = store.listRejected().length;
|
|
1540
|
+
const trustConfig = trust.getConfig();
|
|
1541
|
+
const counts = trust.getDailyCounts();
|
|
1542
|
+
console.log(`
|
|
1543
|
+
State: ${config.state.toUpperCase()}`);
|
|
1544
|
+
console.log(`Proxy: localhost:${config.proxy_port}`);
|
|
1545
|
+
console.log(`Dashboard: localhost:${config.dashboard_port}`);
|
|
1546
|
+
console.log(`
|
|
1547
|
+
Skills: ${approved} approved, ${pending} pending, ${rejected} rejected`);
|
|
1548
|
+
console.log(`Trust: ${trustConfig.trusted.length} trusted, ${trustConfig.blocked.length} blocked`);
|
|
1549
|
+
console.log(`Today: ${counts.total} lessons extracted
|
|
1550
|
+
`);
|
|
1551
|
+
}
|
|
1552
|
+
|
|
1553
|
+
// src/cli/init.ts
|
|
1554
|
+
var command = process.argv[2];
|
|
1555
|
+
async function main() {
|
|
1556
|
+
switch (command) {
|
|
1557
|
+
case "setup":
|
|
1558
|
+
await runSetup();
|
|
1559
|
+
break;
|
|
1560
|
+
case "start":
|
|
1561
|
+
await start();
|
|
1562
|
+
break;
|
|
1563
|
+
case "on":
|
|
1564
|
+
turnOn();
|
|
1565
|
+
break;
|
|
1566
|
+
case "off":
|
|
1567
|
+
turnOff();
|
|
1568
|
+
break;
|
|
1569
|
+
case "status":
|
|
1570
|
+
showStatus();
|
|
1571
|
+
break;
|
|
1572
|
+
case "init":
|
|
1573
|
+
console.log("The `init` command is no longer needed. Use `become setup` instead.");
|
|
1574
|
+
break;
|
|
1575
|
+
default:
|
|
1576
|
+
console.log(`
|
|
1577
|
+
become \u2014 agent-to-agent learning
|
|
1578
|
+
|
|
1579
|
+
Usage:
|
|
1580
|
+
become setup Set up become (interactive wizard)
|
|
1581
|
+
become start Start the proxy server
|
|
1582
|
+
become on Route your agent through become
|
|
1583
|
+
become off Disconnect \u2014 agent talks directly to LLM
|
|
1584
|
+
become status Show current status
|
|
1585
|
+
`);
|
|
1586
|
+
}
|
|
111
1587
|
}
|
|
112
|
-
main().catch(
|
|
1588
|
+
main().catch((err) => {
|
|
1589
|
+
console.error(err.message);
|
|
1590
|
+
process.exit(1);
|
|
1591
|
+
});
|
|
1592
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
1593
|
+
0 && (module.exports = {
|
|
1594
|
+
runSetup,
|
|
1595
|
+
showStatus,
|
|
1596
|
+
start,
|
|
1597
|
+
turnOff,
|
|
1598
|
+
turnOn
|
|
1599
|
+
});
|
|
113
1600
|
//# sourceMappingURL=cli.cjs.map
|