@openclawcity/become 1.0.12 → 1.0.18

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/dist/cli.js CHANGED
@@ -59,146 +59,147 @@ import { join as join2 } from "path";
59
59
  import { homedir as homedir2 } from "os";
60
60
  import { execSync } from "child_process";
61
61
  var OPENCLAW_CONFIG = join2(homedir2(), ".openclaw", "openclaw.json");
62
- var BACKUP_PATH = join2(homedir2(), ".become", "state", "original_openclaw.json");
63
- var ORIGINAL_MODEL_PATH = join2(homedir2(), ".become", "state", "original_model.txt");
64
- var PATCHED_AGENT_PATH = join2(homedir2(), ".become", "state", "patched_agent.txt");
65
- function patchOpenClaw(config, agentId) {
62
+ var STATE_DIR = join2(homedir2(), ".become", "state");
63
+ var BACKUP_PATH = join2(STATE_DIR, "original_openclaw.json");
64
+ var ORIGINAL_URL_PATH = join2(STATE_DIR, "original_base_url.txt");
65
+ var PATCHED_PROVIDER_PATH = join2(STATE_DIR, "patched_provider.txt");
66
+ function patchOpenClaw(config) {
66
67
  if (!existsSync2(OPENCLAW_CONFIG)) {
67
68
  throw new Error(`OpenClaw config not found at ${OPENCLAW_CONFIG}`);
68
69
  }
69
70
  const raw = readFileSync2(OPENCLAW_CONFIG, "utf-8");
70
- const clawConfig = parseOpenClawConfig(raw);
71
- if (clawConfig.models?.providers?.become) {
72
- console.log("become is already connected. Run `become off` first to disconnect.");
73
- return;
71
+ const clawConfig = parseConfig(raw);
72
+ mkdirSync2(STATE_DIR, { recursive: true });
73
+ const primaryModel = clawConfig.agents?.defaults?.model?.primary ?? "";
74
+ if (!primaryModel) {
75
+ throw new Error("No default model configured in openclaw.json (agents.defaults.model.primary)");
76
+ }
77
+ const providerName = primaryModel.split("/")[0];
78
+ if (!providerName) {
79
+ throw new Error(`Cannot determine provider from model: ${primaryModel}`);
80
+ }
81
+ if (existsSync2(ORIGINAL_URL_PATH)) {
82
+ const existingUrl = readFileSync2(ORIGINAL_URL_PATH, "utf-8").trim();
83
+ if (existingUrl) {
84
+ console.log("become is already connected. Run `become off` first to disconnect.");
85
+ return;
86
+ }
87
+ }
88
+ const modelsJsonPath = getModelsJsonPath(clawConfig);
89
+ let modelsConfig = null;
90
+ let modelsSource = "openclaw.json";
91
+ if (modelsJsonPath && existsSync2(modelsJsonPath)) {
92
+ modelsConfig = JSON.parse(readFileSync2(modelsJsonPath, "utf-8"));
93
+ modelsSource = "models.json";
94
+ }
95
+ let provider = null;
96
+ let providerLocation = null;
97
+ if (modelsConfig?.providers?.[providerName]) {
98
+ provider = modelsConfig.providers[providerName];
99
+ providerLocation = modelsConfig.providers;
100
+ } else if (clawConfig.models?.providers?.[providerName]) {
101
+ provider = clawConfig.models.providers[providerName];
102
+ providerLocation = clawConfig.models.providers;
103
+ modelsSource = "openclaw.json";
104
+ }
105
+ if (!provider) {
106
+ throw new Error(
107
+ `Provider "${providerName}" not found in models.json or openclaw.json. Your model is "${primaryModel}" which needs a "${providerName}" provider.`
108
+ );
109
+ }
110
+ const originalUrl = provider.baseUrl;
111
+ if (!originalUrl) {
112
+ throw new Error(`Provider "${providerName}" has no baseUrl`);
74
113
  }
75
- mkdirSync2(join2(homedir2(), ".become", "state"), { recursive: true });
76
114
  writeFileSync2(BACKUP_PATH, raw, "utf-8");
77
- const agents = clawConfig.agents?.list ?? [];
78
- let originalModel;
79
- let patchedAgentId;
80
- if (agents.length > 0 && agentId) {
81
- const agent = agents.find((a) => a.id === agentId);
82
- if (!agent) {
83
- throw new Error(`Agent "${agentId}" not found in agents.list. Available: ${agents.map((a) => a.id).join(", ")}`);
84
- }
85
- originalModel = agent.model ?? clawConfig.agents?.defaults?.model?.primary ?? "";
86
- patchedAgentId = agentId;
87
- const modelId2 = stripProvider(originalModel);
88
- if (!modelId2) {
89
- throw new Error("No model configured for this agent. Set a model in openclaw.json first.");
90
- }
91
- agent.model = `become/${modelId2}`;
115
+ writeFileSync2(ORIGINAL_URL_PATH, originalUrl, "utf-8");
116
+ writeFileSync2(PATCHED_PROVIDER_PATH, `${providerName}:${modelsSource}`, "utf-8");
117
+ provider.baseUrl = `http://127.0.0.1:${config.proxy_port}`;
118
+ if (modelsSource === "models.json" && modelsJsonPath) {
119
+ writeFileSync2(modelsJsonPath, JSON.stringify(modelsConfig, null, 2), "utf-8");
92
120
  } else {
93
- originalModel = clawConfig.agents?.defaults?.model?.primary ?? "";
94
- patchedAgentId = "_defaults";
95
- const modelId2 = stripProvider(originalModel);
96
- if (!modelId2) {
97
- throw new Error("No default model configured. Set agents.defaults.model.primary in openclaw.json first.");
98
- }
99
- if (!clawConfig.agents) clawConfig.agents = {};
100
- if (!clawConfig.agents.defaults) clawConfig.agents.defaults = {};
101
- if (!clawConfig.agents.defaults.model) clawConfig.agents.defaults.model = {};
102
- clawConfig.agents.defaults.model.primary = `become/${modelId2}`;
103
- }
104
- writeFileSync2(ORIGINAL_MODEL_PATH, originalModel, "utf-8");
105
- writeFileSync2(PATCHED_AGENT_PATH, patchedAgentId, "utf-8");
106
- const modelId = stripProvider(originalModel);
107
- if (!clawConfig.models) clawConfig.models = {};
108
- if (!clawConfig.models.providers) clawConfig.models.providers = {};
109
- clawConfig.models.providers.become = {
110
- api: config.llm_provider === "openai" || config.llm_provider === "openrouter" ? "openai-completions" : "anthropic-messages",
111
- baseUrl: `http://127.0.0.1:${config.proxy_port}`,
112
- apiKey: config.llm_api_key,
113
- models: [
114
- { id: modelId, name: `${modelId} via become` }
115
- ]
116
- };
117
- writeFileSync2(OPENCLAW_CONFIG, JSON.stringify(clawConfig, null, 2), "utf-8");
118
- console.log("Restarting OpenClaw gateway...");
119
- try {
120
- execSync("openclaw gateway restart", { stdio: "pipe", timeout: 15e3 });
121
- console.log("OpenClaw gateway restarted. Your agent is now routing through become.");
122
- } catch {
123
- console.log("\n*** OpenClaw gateway needs a manual restart to pick up the new config. ***");
124
- console.log("*** Run: openclaw gateway restart ***\n");
121
+ writeFileSync2(OPENCLAW_CONFIG, JSON.stringify(clawConfig, null, 2), "utf-8");
125
122
  }
123
+ console.log(` provider: ${providerName}`);
124
+ console.log(` baseUrl: ${originalUrl} -> localhost:${config.proxy_port}`);
125
+ restartGateway();
126
126
  }
127
127
  function restoreOpenClaw() {
128
- if (!existsSync2(OPENCLAW_CONFIG)) {
129
- throw new Error(`OpenClaw config not found at ${OPENCLAW_CONFIG}`);
130
- }
131
- if (existsSync2(BACKUP_PATH)) {
132
- const backup = readFileSync2(BACKUP_PATH, "utf-8");
133
- const backupConfig = parseOpenClawConfig(backup);
134
- if (!backupConfig.models?.providers?.become) {
135
- writeFileSync2(OPENCLAW_CONFIG, backup, "utf-8");
136
- restartGateway();
137
- return;
138
- }
128
+ const originalUrl = readSafe(ORIGINAL_URL_PATH);
129
+ const patchInfo = readSafe(PATCHED_PROVIDER_PATH);
130
+ if (!originalUrl || !patchInfo) {
131
+ cleanState();
132
+ return;
139
133
  }
140
- const raw = readFileSync2(OPENCLAW_CONFIG, "utf-8");
141
- const config = parseOpenClawConfig(raw);
142
- const patchedAgentId = readStateFile(PATCHED_AGENT_PATH);
143
- const originalModel = readStateFile(ORIGINAL_MODEL_PATH);
144
- if (originalModel) {
145
- const agents = config.agents?.list ?? [];
146
- if (patchedAgentId && patchedAgentId !== "_defaults") {
147
- const agent = agents.find((a) => a.id === patchedAgentId);
148
- if (agent) {
149
- agent.model = originalModel;
150
- }
151
- } else {
152
- if (config.agents?.defaults?.model) {
153
- config.agents.defaults.model.primary = originalModel;
134
+ const [providerName, source] = patchInfo.split(":");
135
+ if (source === "models.json") {
136
+ const clawConfig = parseConfig(readFileSync2(OPENCLAW_CONFIG, "utf-8"));
137
+ const modelsJsonPath = getModelsJsonPath(clawConfig);
138
+ if (modelsJsonPath && existsSync2(modelsJsonPath)) {
139
+ const modelsConfig = JSON.parse(readFileSync2(modelsJsonPath, "utf-8"));
140
+ if (modelsConfig.providers?.[providerName]) {
141
+ modelsConfig.providers[providerName].baseUrl = originalUrl;
142
+ writeFileSync2(modelsJsonPath, JSON.stringify(modelsConfig, null, 2), "utf-8");
154
143
  }
155
144
  }
156
- }
157
- if (config.models?.providers?.become) {
158
- delete config.models.providers.become;
159
- }
160
- for (const provider of Object.values(config.models?.providers ?? {})) {
161
- if (provider && typeof provider === "object" && "_originalModel" in provider) {
162
- delete provider._originalModel;
145
+ } else {
146
+ if (existsSync2(OPENCLAW_CONFIG)) {
147
+ const clawConfig = parseConfig(readFileSync2(OPENCLAW_CONFIG, "utf-8"));
148
+ if (clawConfig.models?.providers?.[providerName]) {
149
+ clawConfig.models.providers[providerName].baseUrl = originalUrl;
150
+ writeFileSync2(OPENCLAW_CONFIG, JSON.stringify(clawConfig, null, 2), "utf-8");
151
+ }
163
152
  }
164
153
  }
165
- writeFileSync2(OPENCLAW_CONFIG, JSON.stringify(config, null, 2), "utf-8");
154
+ cleanState();
166
155
  restartGateway();
167
156
  }
168
157
  function listOpenClawAgents() {
169
158
  if (!existsSync2(OPENCLAW_CONFIG)) return [];
170
159
  try {
171
- const config = parseOpenClawConfig(readFileSync2(OPENCLAW_CONFIG, "utf-8"));
160
+ const config = parseConfig(readFileSync2(OPENCLAW_CONFIG, "utf-8"));
172
161
  const agents = config.agents?.list ?? [];
173
- const defaultModel = unbecome(config.agents?.defaults?.model?.primary ?? "unknown");
162
+ const defaultModel = config.agents?.defaults?.model?.primary ?? "unknown";
174
163
  if (agents.length === 0) {
175
164
  return [{ id: "_defaults", model: defaultModel }];
176
165
  }
177
166
  return agents.map((a) => ({
178
167
  id: a.id,
179
- model: unbecome(a.model ?? defaultModel)
168
+ model: a.model ?? defaultModel
180
169
  }));
181
170
  } catch {
182
171
  return [];
183
172
  }
184
173
  }
185
- function stripProvider(model) {
186
- return model.includes("/") ? model.split("/").slice(1).join("/") : model;
187
- }
188
- function unbecome(model) {
189
- return model.startsWith("become/") ? model.replace("become/", "") : model;
174
+ function getModelsJsonPath(clawConfig) {
175
+ const agentList = clawConfig.agents?.list ?? [];
176
+ const defaultAgent = agentList.find((a) => a.default) ?? agentList[0];
177
+ if (defaultAgent?.agentDir) {
178
+ return join2(defaultAgent.agentDir.replace("~", homedir2()), "models.json");
179
+ }
180
+ const mainPath = join2(homedir2(), ".openclaw", "agents", "main", "agent", "models.json");
181
+ if (existsSync2(mainPath)) return mainPath;
182
+ return null;
190
183
  }
191
- function parseOpenClawConfig(raw) {
184
+ function parseConfig(raw) {
192
185
  const stripped = raw.replace(/\/\/.*$/gm, "").replace(/\/\*[\s\S]*?\*\//g, "").replace(/,\s*([\]}])/g, "$1");
193
186
  return JSON.parse(stripped);
194
187
  }
195
- function readStateFile(path) {
188
+ function readSafe(path) {
196
189
  try {
197
190
  return existsSync2(path) ? readFileSync2(path, "utf-8").trim() : "";
198
191
  } catch {
199
192
  return "";
200
193
  }
201
194
  }
195
+ function cleanState() {
196
+ for (const f of [ORIGINAL_URL_PATH, PATCHED_PROVIDER_PATH]) {
197
+ try {
198
+ writeFileSync2(f, "", "utf-8");
199
+ } catch {
200
+ }
201
+ }
202
+ }
202
203
  function restartGateway() {
203
204
  console.log("Restarting OpenClaw gateway...");
204
205
  try {
@@ -284,6 +285,11 @@ Proxy port (default 30001): `);
284
285
  }
285
286
  }
286
287
 
288
+ // src/cli/commands.ts
289
+ import { readFileSync as readFileSync7, existsSync as existsSync7 } from "fs";
290
+ import { join as join7 } from "path";
291
+ import { homedir as homedir5 } from "os";
292
+
287
293
  // src/proxy/server.ts
288
294
  import { createServer } from "http";
289
295
 
@@ -710,7 +716,7 @@ Rules:
710
716
 
711
717
  // src/proxy/server.ts
712
718
  var SKILL_CACHE_TTL_MS = 5e3;
713
- function createProxyServer(config, analyzer) {
719
+ function createProxyServer(config, analyzer, overrideUpstreamUrl) {
714
720
  const store = new FileSkillStore({ baseDir: config.baseDir });
715
721
  const trust = new TrustManager(config.baseDir);
716
722
  const extractor = analyzer ? new LessonExtractor(store, trust, analyzer) : null;
@@ -757,7 +763,7 @@ function createProxyServer(config, analyzer) {
757
763
  stats.skills_injected++;
758
764
  }
759
765
  }
760
- const upstreamUrl = buildUpstreamUrl(config, req.url);
766
+ const upstreamUrl = buildUpstreamUrl(overrideUpstreamUrl ?? config.llm_base_url, req.url);
761
767
  const upstreamHeaders = buildUpstreamHeaders(config, req.headers);
762
768
  const isStreaming = body.stream === true;
763
769
  const modifiedBody = JSON.stringify(body);
@@ -841,8 +847,8 @@ function readBody(req) {
841
847
  req.on("error", reject);
842
848
  });
843
849
  }
844
- function buildUpstreamUrl(config, path) {
845
- const base = config.llm_base_url.replace(/\/+$/, "");
850
+ function buildUpstreamUrl(baseUrl, path) {
851
+ const base = baseUrl.replace(/\/+$/, "");
846
852
  return `${base}${path}`;
847
853
  }
848
854
  function buildUpstreamHeaders(config, incomingHeaders) {
@@ -1390,98 +1396,185 @@ function readBody2(req) {
1390
1396
  }
1391
1397
 
1392
1398
  // src/cli/adapter/ironclaw.ts
1393
- import { readFileSync as readFileSync5, writeFileSync as writeFileSync5, existsSync as existsSync5, mkdirSync as mkdirSync5, copyFileSync } from "fs";
1399
+ import { readFileSync as readFileSync5, writeFileSync as writeFileSync5, existsSync as existsSync5, mkdirSync as mkdirSync5, copyFileSync, unlinkSync as unlinkSync2 } from "fs";
1394
1400
  import { join as join5 } from "path";
1395
1401
  import { homedir as homedir3 } from "os";
1396
1402
  import { execSync as execSync2 } from "child_process";
1397
- var IRONCLAW_ENV = join5(homedir3(), ".ironclaw", ".env");
1403
+ var IRONCLAW_ENV = join5(process.env.IRONCLAW_BASE_DIR ?? join5(homedir3(), ".ironclaw"), ".env");
1398
1404
  var BACKUP_PATH2 = join5(homedir3(), ".become", "state", "original_ironclaw.env");
1399
1405
  function patchIronClaw(config) {
1400
1406
  if (!existsSync5(IRONCLAW_ENV)) {
1401
1407
  throw new Error(`IronClaw .env not found at ${IRONCLAW_ENV}`);
1402
1408
  }
1409
+ if (existsSync5(BACKUP_PATH2)) {
1410
+ console.log("become is already connected to IronClaw. Run `become off` first.");
1411
+ return;
1412
+ }
1403
1413
  mkdirSync5(join5(homedir3(), ".become", "state"), { recursive: true });
1404
1414
  copyFileSync(IRONCLAW_ENV, BACKUP_PATH2);
1405
- patchDotEnv(IRONCLAW_ENV, {
1406
- LLM_BASE_URL: `http://127.0.0.1:${config.proxy_port}/v1`
1407
- });
1408
- console.log("Restarting IronClaw...");
1409
- try {
1410
- execSync2("ironclaw service restart", { stdio: "pipe", timeout: 15e3 });
1411
- console.log("IronClaw restarted.");
1412
- } catch {
1413
- console.log("\n*** IronClaw needs a manual restart. ***");
1414
- console.log("*** Run: ironclaw service restart ***\n");
1415
+ const content = readFileSync5(IRONCLAW_ENV, "utf-8");
1416
+ const backendMatch = content.match(/^LLM_BACKEND=(.+)$/m);
1417
+ const backend = backendMatch?.[1]?.trim().toLowerCase() ?? "openai_compatible";
1418
+ const proxyUrl = `http://127.0.0.1:${config.proxy_port}`;
1419
+ const vars = {};
1420
+ switch (backend) {
1421
+ case "anthropic":
1422
+ vars["ANTHROPIC_BASE_URL"] = proxyUrl;
1423
+ break;
1424
+ case "ollama":
1425
+ vars["OLLAMA_BASE_URL"] = proxyUrl;
1426
+ break;
1427
+ case "nearai":
1428
+ case "near_ai":
1429
+ case "near":
1430
+ vars["NEARAI_BASE_URL"] = proxyUrl;
1431
+ break;
1432
+ default:
1433
+ vars["LLM_BASE_URL"] = proxyUrl;
1434
+ break;
1415
1435
  }
1436
+ patchDotEnv(IRONCLAW_ENV, vars);
1437
+ console.log(` backend: ${backend}`);
1438
+ console.log(` patched: ${Object.keys(vars).join(", ")} -> localhost:${config.proxy_port}`);
1439
+ restartIronClaw();
1416
1440
  }
1417
1441
  function restoreIronClaw() {
1418
1442
  if (!existsSync5(BACKUP_PATH2)) {
1419
- throw new Error("No backup found. Was become ever turned on?");
1443
+ return;
1420
1444
  }
1421
1445
  copyFileSync(BACKUP_PATH2, IRONCLAW_ENV);
1446
+ try {
1447
+ unlinkSync2(BACKUP_PATH2);
1448
+ } catch {
1449
+ }
1450
+ restartIronClaw();
1451
+ }
1452
+ function restartIronClaw() {
1422
1453
  console.log("Restarting IronClaw...");
1423
1454
  try {
1424
- execSync2("ironclaw service restart", { stdio: "pipe", timeout: 15e3 });
1455
+ execSync2("ironclaw service stop", { stdio: "pipe", timeout: 1e4 });
1456
+ execSync2("ironclaw service start", { stdio: "pipe", timeout: 1e4 });
1425
1457
  console.log("IronClaw restarted.");
1458
+ return;
1459
+ } catch {
1460
+ }
1461
+ try {
1462
+ execSync2("launchctl kickstart -k gui/$(id -u)/com.ironclaw.daemon", { stdio: "pipe", timeout: 1e4 });
1463
+ console.log("IronClaw restarted via launchctl.");
1464
+ return;
1465
+ } catch {
1466
+ }
1467
+ try {
1468
+ execSync2("systemctl --user restart ironclaw", { stdio: "pipe", timeout: 1e4 });
1469
+ console.log("IronClaw restarted via systemd.");
1470
+ return;
1426
1471
  } catch {
1427
- console.log("\n*** IronClaw needs a manual restart. ***");
1428
- console.log("*** Run: ironclaw service restart ***\n");
1429
1472
  }
1473
+ console.log("\n*** IronClaw needs a manual restart. ***");
1474
+ console.log("*** Run: ironclaw service stop && ironclaw service start ***\n");
1430
1475
  }
1431
1476
  function patchDotEnv(path, vars) {
1432
- let content = readFileSync5(path, "utf-8");
1477
+ let content = existsSync5(path) ? readFileSync5(path, "utf-8") : "";
1433
1478
  for (const [key, value] of Object.entries(vars)) {
1434
1479
  const regex = new RegExp(`^${key}=.*$`, "m");
1435
1480
  if (regex.test(content)) {
1436
1481
  content = content.replace(regex, `${key}=${value}`);
1437
1482
  } else {
1438
- content += `
1439
- ${key}=${value}`;
1483
+ content = content.trimEnd() + (content.length > 0 ? "\n" : "") + `${key}=${value}
1484
+ `;
1440
1485
  }
1441
1486
  }
1442
1487
  writeFileSync5(path, content, "utf-8");
1443
1488
  }
1444
1489
 
1445
1490
  // src/cli/adapter/nanoclaw.ts
1446
- import { readFileSync as readFileSync6, writeFileSync as writeFileSync6, existsSync as existsSync6, mkdirSync as mkdirSync6, copyFileSync as copyFileSync2 } from "fs";
1491
+ import { readFileSync as readFileSync6, writeFileSync as writeFileSync6, existsSync as existsSync6, mkdirSync as mkdirSync6, copyFileSync as copyFileSync2, unlinkSync as unlinkSync3 } from "fs";
1447
1492
  import { join as join6 } from "path";
1448
1493
  import { homedir as homedir4 } from "os";
1449
1494
  import { execSync as execSync3 } from "child_process";
1450
1495
  var BACKUP_PATH3 = join6(homedir4(), ".become", "state", "original_nanoclaw.env");
1496
+ var PATCHED_ENV_PATH_FILE = join6(homedir4(), ".become", "state", "nanoclaw_env_path.txt");
1497
+ var NANOCLAW_URL_VAR = "ANTHROPIC_BASE_URL";
1451
1498
  function patchNanoClaw(config) {
1452
1499
  const envPath = findNanoClawEnv();
1453
1500
  if (!envPath) {
1454
- throw new Error("Could not find NanoClaw .env. Set ANTHROPIC_BASE_URL manually to http://127.0.0.1:" + config.proxy_port);
1501
+ throw new Error(
1502
+ `Could not find NanoClaw .env file.
1503
+ NanoClaw stores .env in its project root (where you cloned it).
1504
+ Set ${NANOCLAW_URL_VAR}=http://127.0.0.1:${config.proxy_port} manually in your NanoClaw .env file.`
1505
+ );
1506
+ }
1507
+ if (existsSync6(BACKUP_PATH3)) {
1508
+ console.log("become is already connected to NanoClaw. Run `become off` first.");
1509
+ return;
1455
1510
  }
1456
1511
  mkdirSync6(join6(homedir4(), ".become", "state"), { recursive: true });
1457
1512
  copyFileSync2(envPath, BACKUP_PATH3);
1513
+ writeFileSync6(PATCHED_ENV_PATH_FILE, envPath, "utf-8");
1458
1514
  patchDotEnv2(envPath, {
1459
- ANTHROPIC_BASE_URL: `http://127.0.0.1:${config.proxy_port}`
1515
+ [NANOCLAW_URL_VAR]: `http://127.0.0.1:${config.proxy_port}`
1460
1516
  });
1517
+ console.log(` env file: ${envPath}`);
1518
+ console.log(` patched: ${NANOCLAW_URL_VAR} -> localhost:${config.proxy_port}`);
1461
1519
  restartNanoClaw();
1462
1520
  }
1463
1521
  function restoreNanoClaw() {
1464
- const envPath = findNanoClawEnv();
1465
- if (!existsSync6(BACKUP_PATH3) || !envPath) {
1466
- throw new Error("No backup found. Was become ever turned on?");
1522
+ if (!existsSync6(BACKUP_PATH3)) {
1523
+ return;
1524
+ }
1525
+ let envPath = null;
1526
+ if (existsSync6(PATCHED_ENV_PATH_FILE)) {
1527
+ envPath = readFileSync6(PATCHED_ENV_PATH_FILE, "utf-8").trim();
1528
+ }
1529
+ if (!envPath) {
1530
+ envPath = findNanoClawEnv();
1531
+ }
1532
+ if (!envPath) {
1533
+ console.log("Warning: Cannot find NanoClaw .env to restore. Backup is at " + BACKUP_PATH3);
1534
+ return;
1467
1535
  }
1468
1536
  copyFileSync2(BACKUP_PATH3, envPath);
1537
+ try {
1538
+ unlinkSync3(BACKUP_PATH3);
1539
+ } catch {
1540
+ }
1541
+ try {
1542
+ unlinkSync3(PATCHED_ENV_PATH_FILE);
1543
+ } catch {
1544
+ }
1469
1545
  restartNanoClaw();
1470
1546
  }
1471
1547
  function findNanoClawEnv() {
1472
- const candidates = [
1473
- join6(homedir4(), ".nanoclaw", ".env"),
1474
- join6(homedir4(), ".config", "nanoclaw", ".env")
1475
- ];
1476
- const plistPath = join6(homedir4(), "Library", "LaunchAgents", "ai.nanoclaw.agent.plist");
1548
+ const candidates = [];
1549
+ const plistPath = join6(homedir4(), "Library", "LaunchAgents", "com.nanoclaw.plist");
1477
1550
  if (existsSync6(plistPath)) {
1478
1551
  try {
1479
1552
  const plist = readFileSync6(plistPath, "utf-8");
1480
- const match = plist.match(/<string>([^<]*\.env)<\/string>/);
1481
- if (match) candidates.unshift(match[1]);
1553
+ const match = plist.match(/<key>WorkingDirectory<\/key>\s*<string>([^<]+)<\/string>/);
1554
+ if (match) candidates.push(join6(match[1], ".env"));
1555
+ } catch {
1556
+ }
1557
+ }
1558
+ const userUnit = join6(homedir4(), ".config", "systemd", "user", "nanoclaw.service");
1559
+ if (existsSync6(userUnit)) {
1560
+ try {
1561
+ const unit = readFileSync6(userUnit, "utf-8");
1562
+ const match = unit.match(/WorkingDirectory=(.+)/);
1563
+ if (match) candidates.push(join6(match[1].trim(), ".env"));
1564
+ } catch {
1565
+ }
1566
+ }
1567
+ const rootUnit = "/etc/systemd/system/nanoclaw.service";
1568
+ if (existsSync6(rootUnit)) {
1569
+ try {
1570
+ const unit = readFileSync6(rootUnit, "utf-8");
1571
+ const match = unit.match(/WorkingDirectory=(.+)/);
1572
+ if (match) candidates.push(join6(match[1].trim(), ".env"));
1482
1573
  } catch {
1483
1574
  }
1484
1575
  }
1576
+ candidates.push(join6(homedir4(), "nanoclaw", ".env"));
1577
+ candidates.push("/opt/nanoclaw/.env");
1485
1578
  for (const path of candidates) {
1486
1579
  if (existsSync6(path)) return path;
1487
1580
  }
@@ -1489,35 +1582,207 @@ function findNanoClawEnv() {
1489
1582
  }
1490
1583
  function restartNanoClaw() {
1491
1584
  console.log("Restarting NanoClaw...");
1492
- try {
1493
- execSync3("launchctl kickstart -k gui/$(id -u)/ai.nanoclaw.agent", { stdio: "pipe", timeout: 15e3 });
1494
- console.log("NanoClaw restarted.");
1495
- } catch {
1585
+ const plistPath = join6(homedir4(), "Library", "LaunchAgents", "com.nanoclaw.plist");
1586
+ if (existsSync6(plistPath)) {
1496
1587
  try {
1497
- execSync3("systemctl --user restart nanoclaw", { stdio: "pipe", timeout: 15e3 });
1588
+ execSync3(`launchctl unload "${plistPath}"`, { stdio: "pipe", timeout: 1e4 });
1589
+ execSync3(`launchctl load "${plistPath}"`, { stdio: "pipe", timeout: 1e4 });
1498
1590
  console.log("NanoClaw restarted.");
1591
+ return;
1499
1592
  } catch {
1500
- console.log("\n*** NanoClaw needs a manual restart. ***");
1501
- console.log("*** macOS: launchctl kickstart -k gui/$(id -u)/ai.nanoclaw.agent ***");
1502
- console.log("*** Linux: systemctl --user restart nanoclaw ***\n");
1503
1593
  }
1504
1594
  }
1595
+ try {
1596
+ execSync3("systemctl --user restart nanoclaw", { stdio: "pipe", timeout: 1e4 });
1597
+ console.log("NanoClaw restarted.");
1598
+ return;
1599
+ } catch {
1600
+ }
1601
+ console.log("\n*** NanoClaw needs a manual restart. ***");
1602
+ console.log("*** macOS: launchctl unload ~/Library/LaunchAgents/com.nanoclaw.plist && launchctl load ~/Library/LaunchAgents/com.nanoclaw.plist ***");
1603
+ console.log("*** Linux: systemctl --user restart nanoclaw ***\n");
1505
1604
  }
1506
1605
  function patchDotEnv2(path, vars) {
1507
- let content = readFileSync6(path, "utf-8");
1606
+ let content = existsSync6(path) ? readFileSync6(path, "utf-8") : "";
1508
1607
  for (const [key, value] of Object.entries(vars)) {
1509
1608
  const regex = new RegExp(`^${key}=.*$`, "m");
1510
1609
  if (regex.test(content)) {
1511
1610
  content = content.replace(regex, `${key}=${value}`);
1512
1611
  } else {
1513
- content += `
1514
- ${key}=${value}`;
1612
+ content = content.trimEnd() + (content.length > 0 ? "\n" : "") + `${key}=${value}
1613
+ `;
1515
1614
  }
1516
1615
  }
1517
1616
  writeFileSync6(path, content, "utf-8");
1518
1617
  }
1519
1618
 
1619
+ // src/adapters/llm.ts
1620
+ var DEFAULT_TIMEOUT_MS = 6e4;
1621
+ var OpenAIAdapter = class {
1622
+ apiKey;
1623
+ baseUrl;
1624
+ defaultModel;
1625
+ constructor(config) {
1626
+ if (!config.apiKey) throw new Error("OpenAI API key is required");
1627
+ this.apiKey = config.apiKey;
1628
+ this.baseUrl = (config.baseUrl ?? "https://api.openai.com").replace(/\/+$/, "");
1629
+ this.defaultModel = config.model ?? "gpt-4o-mini";
1630
+ }
1631
+ async complete(prompt, opts) {
1632
+ const response = await this.request({
1633
+ model: opts?.model ?? this.defaultModel,
1634
+ messages: [{ role: "user", content: prompt }],
1635
+ max_tokens: opts?.maxTokens ?? 2e3,
1636
+ temperature: opts?.temperature ?? 0.7
1637
+ }, opts?.timeoutMs);
1638
+ return response.choices?.[0]?.message?.content ?? "";
1639
+ }
1640
+ async json(prompt, opts) {
1641
+ const response = await this.request({
1642
+ model: opts?.model ?? this.defaultModel,
1643
+ messages: [{ role: "user", content: prompt }],
1644
+ max_tokens: opts?.maxTokens ?? 2e3,
1645
+ temperature: opts?.temperature ?? 0.3,
1646
+ response_format: { type: "json_object" }
1647
+ }, opts?.timeoutMs);
1648
+ const text = response.choices?.[0]?.message?.content ?? "{}";
1649
+ return JSON.parse(text);
1650
+ }
1651
+ async request(body, timeoutMs) {
1652
+ const res = await fetch(`${this.baseUrl}/v1/chat/completions`, {
1653
+ method: "POST",
1654
+ headers: {
1655
+ "Content-Type": "application/json",
1656
+ "Authorization": `Bearer ${this.apiKey}`
1657
+ },
1658
+ body: JSON.stringify(body),
1659
+ signal: AbortSignal.timeout(timeoutMs ?? DEFAULT_TIMEOUT_MS)
1660
+ });
1661
+ if (!res.ok) {
1662
+ const text = await res.text().catch(() => "unknown error");
1663
+ throw new Error(`OpenAI API error ${res.status}: ${text.slice(0, 200)}`);
1664
+ }
1665
+ return res.json();
1666
+ }
1667
+ };
1668
+ var AnthropicAdapter = class {
1669
+ apiKey;
1670
+ defaultModel;
1671
+ constructor(config) {
1672
+ if (!config.apiKey) throw new Error("Anthropic API key is required");
1673
+ this.apiKey = config.apiKey;
1674
+ this.defaultModel = config.model ?? "claude-sonnet-4-20250514";
1675
+ }
1676
+ async complete(prompt, opts) {
1677
+ const res = await fetch("https://api.anthropic.com/v1/messages", {
1678
+ method: "POST",
1679
+ headers: {
1680
+ "Content-Type": "application/json",
1681
+ "x-api-key": this.apiKey,
1682
+ "anthropic-version": "2023-06-01"
1683
+ },
1684
+ body: JSON.stringify({
1685
+ model: opts?.model ?? this.defaultModel,
1686
+ max_tokens: opts?.maxTokens ?? 2e3,
1687
+ messages: [{ role: "user", content: prompt }]
1688
+ }),
1689
+ signal: AbortSignal.timeout(opts?.timeoutMs ?? DEFAULT_TIMEOUT_MS)
1690
+ });
1691
+ if (!res.ok) {
1692
+ const text = await res.text().catch(() => "unknown error");
1693
+ throw new Error(`Anthropic API error ${res.status}: ${text.slice(0, 200)}`);
1694
+ }
1695
+ const data = await res.json();
1696
+ return data.content?.[0]?.text ?? "";
1697
+ }
1698
+ async json(prompt, opts) {
1699
+ const text = await this.complete(
1700
+ `${prompt}
1701
+
1702
+ Respond with valid JSON only, no other text.`,
1703
+ { ...opts, temperature: opts?.temperature ?? 0.3 }
1704
+ );
1705
+ try {
1706
+ return JSON.parse(text.trim());
1707
+ } catch {
1708
+ const match = text.match(/\{[\s\S]*?\}(?=\s*$|\s*[^}\]])/);
1709
+ const arrMatch = text.match(/\[[\s\S]*?\](?=\s*$|\s*[^}\]])/);
1710
+ const candidate = match?.[0] ?? arrMatch?.[0];
1711
+ if (!candidate) throw new Error("No JSON found in response");
1712
+ return JSON.parse(candidate);
1713
+ }
1714
+ }
1715
+ };
1716
+ var OllamaAdapter = class {
1717
+ baseUrl;
1718
+ defaultModel;
1719
+ constructor(config) {
1720
+ this.baseUrl = (config?.baseUrl ?? "http://localhost:11434").replace(/\/+$/, "");
1721
+ this.defaultModel = config?.model ?? "llama3.1";
1722
+ }
1723
+ async complete(prompt, opts) {
1724
+ const res = await fetch(`${this.baseUrl}/api/generate`, {
1725
+ method: "POST",
1726
+ headers: { "Content-Type": "application/json" },
1727
+ body: JSON.stringify({
1728
+ model: opts?.model ?? this.defaultModel,
1729
+ prompt,
1730
+ stream: false,
1731
+ options: {
1732
+ num_predict: opts?.maxTokens ?? 2e3,
1733
+ temperature: opts?.temperature ?? 0.7
1734
+ }
1735
+ }),
1736
+ signal: AbortSignal.timeout(opts?.timeoutMs ?? DEFAULT_TIMEOUT_MS)
1737
+ });
1738
+ if (!res.ok) {
1739
+ const text = await res.text().catch(() => "unknown error");
1740
+ throw new Error(`Ollama error ${res.status}: ${text.slice(0, 200)}`);
1741
+ }
1742
+ const data = await res.json();
1743
+ return data.response ?? "";
1744
+ }
1745
+ async json(prompt, opts) {
1746
+ const res = await fetch(`${this.baseUrl}/api/generate`, {
1747
+ method: "POST",
1748
+ headers: { "Content-Type": "application/json" },
1749
+ body: JSON.stringify({
1750
+ model: opts?.model ?? this.defaultModel,
1751
+ prompt: `${prompt}
1752
+
1753
+ Respond with valid JSON only.`,
1754
+ stream: false,
1755
+ format: "json",
1756
+ options: {
1757
+ num_predict: opts?.maxTokens ?? 2e3,
1758
+ temperature: opts?.temperature ?? 0.3
1759
+ }
1760
+ }),
1761
+ signal: AbortSignal.timeout(opts?.timeoutMs ?? DEFAULT_TIMEOUT_MS)
1762
+ });
1763
+ if (!res.ok) {
1764
+ const text = await res.text().catch(() => "unknown error");
1765
+ throw new Error(`Ollama error ${res.status}: ${text.slice(0, 200)}`);
1766
+ }
1767
+ const data = await res.json();
1768
+ return JSON.parse(data.response ?? "{}");
1769
+ }
1770
+ };
1771
+
1520
1772
  // src/cli/commands.ts
1773
+ function createAnalyzer(config) {
1774
+ switch (config.llm_provider) {
1775
+ case "anthropic":
1776
+ return { analyze: (p) => new AnthropicAdapter({ apiKey: config.llm_api_key }).complete(p) };
1777
+ case "ollama":
1778
+ return { analyze: (p) => new OllamaAdapter({ baseUrl: config.llm_base_url }).complete(p) };
1779
+ case "openai":
1780
+ case "openrouter":
1781
+ case "custom":
1782
+ default:
1783
+ return { analyze: (p) => new OpenAIAdapter({ apiKey: config.llm_api_key, baseUrl: config.llm_base_url }).complete(p) };
1784
+ }
1785
+ }
1521
1786
  async function start() {
1522
1787
  const config = loadConfig();
1523
1788
  const baseDir = getBecomeDir();
@@ -1530,7 +1795,14 @@ async function start() {
1530
1795
  max_skills_per_call: config.max_skills_per_call,
1531
1796
  auto_extract: config.auto_extract
1532
1797
  };
1533
- const proxy = createProxyServer(proxyConfig);
1798
+ const originalUrlPath = join7(homedir5(), ".become", "state", "original_base_url.txt");
1799
+ let originalUpstreamUrl;
1800
+ if (existsSync7(originalUrlPath)) {
1801
+ const saved = readFileSync7(originalUrlPath, "utf-8").trim();
1802
+ if (saved) originalUpstreamUrl = saved;
1803
+ }
1804
+ const analyzer = createAnalyzer(config);
1805
+ const proxy = createProxyServer(proxyConfig, analyzer, originalUpstreamUrl);
1534
1806
  await proxy.listen();
1535
1807
  const dashboard = createDashboardServer({
1536
1808
  store: proxy.store,
@@ -1585,15 +1857,24 @@ Connected to: ${config.agent_type}${config.openclaw_agent_id ? ` (agent: ${confi
1585
1857
  console.log(`[become] ${s.requests_forwarded} requests forwarded, ${s.skills_injected} skills injected, ${s.lessons_extracted} lessons extracted`);
1586
1858
  }
1587
1859
  }, 6e4);
1588
- const shutdown = async () => {
1860
+ let shuttingDown = false;
1861
+ const shutdown = () => {
1862
+ if (shuttingDown) {
1863
+ process.exit(0);
1864
+ return;
1865
+ }
1866
+ shuttingDown = true;
1589
1867
  clearInterval(activityInterval);
1590
1868
  console.log("\nShutting down...");
1591
1869
  try {
1592
1870
  turnOff();
1593
1871
  } catch {
1594
1872
  }
1595
- await Promise.all([proxy.close(), dashboard.close()]);
1596
- process.exit(0);
1873
+ proxy.close().catch(() => {
1874
+ });
1875
+ dashboard.close().catch(() => {
1876
+ });
1877
+ setTimeout(() => process.exit(0), 500);
1597
1878
  };
1598
1879
  process.on("SIGINT", shutdown);
1599
1880
  process.on("SIGTERM", shutdown);
@@ -1605,7 +1886,7 @@ Patching ${config.agent_type} config...`);
1605
1886
  console.log(` baseUrl: ${config.llm_base_url} \u2192 localhost:${config.proxy_port}`);
1606
1887
  switch (config.agent_type) {
1607
1888
  case "openclaw":
1608
- patchOpenClaw(config, config.openclaw_agent_id);
1889
+ patchOpenClaw(config);
1609
1890
  break;
1610
1891
  case "ironclaw":
1611
1892
  patchIronClaw(config);
@@ -1674,20 +1955,20 @@ Skills: ${approved} approved, ${pending} pending, ${rejected} rejected`);
1674
1955
  }
1675
1956
 
1676
1957
  // src/cli/init.ts
1677
- import { readFileSync as readFileSync7 } from "fs";
1678
- import { join as join7, dirname as dirname2 } from "path";
1958
+ import { readFileSync as readFileSync8 } from "fs";
1959
+ import { join as join8, dirname as dirname2 } from "path";
1679
1960
  import { fileURLToPath } from "url";
1680
1961
  var command = process.argv[2];
1681
1962
  var VERSION = "unknown";
1682
1963
  try {
1683
1964
  const dir = dirname2(fileURLToPath(import.meta.url));
1684
- const pkgPath = join7(dir, "..", "package.json");
1685
- VERSION = JSON.parse(readFileSync7(pkgPath, "utf-8")).version;
1965
+ const pkgPath = join8(dir, "..", "package.json");
1966
+ VERSION = JSON.parse(readFileSync8(pkgPath, "utf-8")).version;
1686
1967
  } catch {
1687
1968
  try {
1688
1969
  const dir = dirname2(fileURLToPath(import.meta.url));
1689
- const pkgPath = join7(dir, "..", "..", "package.json");
1690
- VERSION = JSON.parse(readFileSync7(pkgPath, "utf-8")).version;
1970
+ const pkgPath = join8(dir, "..", "..", "package.json");
1971
+ VERSION = JSON.parse(readFileSync8(pkgPath, "utf-8")).version;
1691
1972
  } catch {
1692
1973
  }
1693
1974
  }