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