@openclawcity/become 1.0.13 → 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.cjs CHANGED
@@ -98,146 +98,147 @@ var import_node_path2 = require("path");
98
98
  var import_node_os2 = require("os");
99
99
  var import_node_child_process = require("child_process");
100
100
  var OPENCLAW_CONFIG = (0, import_node_path2.join)((0, import_node_os2.homedir)(), ".openclaw", "openclaw.json");
101
- var BACKUP_PATH = (0, import_node_path2.join)((0, import_node_os2.homedir)(), ".become", "state", "original_openclaw.json");
102
- var ORIGINAL_MODEL_PATH = (0, import_node_path2.join)((0, import_node_os2.homedir)(), ".become", "state", "original_model.txt");
103
- var PATCHED_AGENT_PATH = (0, import_node_path2.join)((0, import_node_os2.homedir)(), ".become", "state", "patched_agent.txt");
104
- function patchOpenClaw(config, agentId) {
101
+ var STATE_DIR = (0, import_node_path2.join)((0, import_node_os2.homedir)(), ".become", "state");
102
+ var BACKUP_PATH = (0, import_node_path2.join)(STATE_DIR, "original_openclaw.json");
103
+ var ORIGINAL_URL_PATH = (0, import_node_path2.join)(STATE_DIR, "original_base_url.txt");
104
+ var PATCHED_PROVIDER_PATH = (0, import_node_path2.join)(STATE_DIR, "patched_provider.txt");
105
+ function patchOpenClaw(config) {
105
106
  if (!(0, import_node_fs2.existsSync)(OPENCLAW_CONFIG)) {
106
107
  throw new Error(`OpenClaw config not found at ${OPENCLAW_CONFIG}`);
107
108
  }
108
109
  const raw = (0, import_node_fs2.readFileSync)(OPENCLAW_CONFIG, "utf-8");
109
- const clawConfig = parseOpenClawConfig(raw);
110
- if (clawConfig.models?.providers?.become) {
111
- console.log("become is already connected. Run `become off` first to disconnect.");
112
- return;
110
+ const clawConfig = parseConfig(raw);
111
+ (0, import_node_fs2.mkdirSync)(STATE_DIR, { recursive: true });
112
+ const primaryModel = clawConfig.agents?.defaults?.model?.primary ?? "";
113
+ if (!primaryModel) {
114
+ throw new Error("No default model configured in openclaw.json (agents.defaults.model.primary)");
115
+ }
116
+ const providerName = primaryModel.split("/")[0];
117
+ if (!providerName) {
118
+ throw new Error(`Cannot determine provider from model: ${primaryModel}`);
119
+ }
120
+ if ((0, import_node_fs2.existsSync)(ORIGINAL_URL_PATH)) {
121
+ const existingUrl = (0, import_node_fs2.readFileSync)(ORIGINAL_URL_PATH, "utf-8").trim();
122
+ if (existingUrl) {
123
+ console.log("become is already connected. Run `become off` first to disconnect.");
124
+ return;
125
+ }
126
+ }
127
+ const modelsJsonPath = getModelsJsonPath(clawConfig);
128
+ let modelsConfig = null;
129
+ let modelsSource = "openclaw.json";
130
+ if (modelsJsonPath && (0, import_node_fs2.existsSync)(modelsJsonPath)) {
131
+ modelsConfig = JSON.parse((0, import_node_fs2.readFileSync)(modelsJsonPath, "utf-8"));
132
+ modelsSource = "models.json";
133
+ }
134
+ let provider = null;
135
+ let providerLocation = null;
136
+ if (modelsConfig?.providers?.[providerName]) {
137
+ provider = modelsConfig.providers[providerName];
138
+ providerLocation = modelsConfig.providers;
139
+ } else if (clawConfig.models?.providers?.[providerName]) {
140
+ provider = clawConfig.models.providers[providerName];
141
+ providerLocation = clawConfig.models.providers;
142
+ modelsSource = "openclaw.json";
143
+ }
144
+ if (!provider) {
145
+ throw new Error(
146
+ `Provider "${providerName}" not found in models.json or openclaw.json. Your model is "${primaryModel}" which needs a "${providerName}" provider.`
147
+ );
148
+ }
149
+ const originalUrl = provider.baseUrl;
150
+ if (!originalUrl) {
151
+ throw new Error(`Provider "${providerName}" has no baseUrl`);
113
152
  }
114
- (0, import_node_fs2.mkdirSync)((0, import_node_path2.join)((0, import_node_os2.homedir)(), ".become", "state"), { recursive: true });
115
153
  (0, import_node_fs2.writeFileSync)(BACKUP_PATH, raw, "utf-8");
116
- const agents = clawConfig.agents?.list ?? [];
117
- let originalModel;
118
- let patchedAgentId;
119
- if (agents.length > 0 && agentId) {
120
- const agent = agents.find((a) => a.id === agentId);
121
- if (!agent) {
122
- throw new Error(`Agent "${agentId}" not found in agents.list. Available: ${agents.map((a) => a.id).join(", ")}`);
123
- }
124
- originalModel = agent.model ?? clawConfig.agents?.defaults?.model?.primary ?? "";
125
- patchedAgentId = agentId;
126
- const modelId2 = stripProvider(originalModel);
127
- if (!modelId2) {
128
- throw new Error("No model configured for this agent. Set a model in openclaw.json first.");
129
- }
130
- agent.model = `become/${modelId2}`;
154
+ (0, import_node_fs2.writeFileSync)(ORIGINAL_URL_PATH, originalUrl, "utf-8");
155
+ (0, import_node_fs2.writeFileSync)(PATCHED_PROVIDER_PATH, `${providerName}:${modelsSource}`, "utf-8");
156
+ provider.baseUrl = `http://127.0.0.1:${config.proxy_port}`;
157
+ if (modelsSource === "models.json" && modelsJsonPath) {
158
+ (0, import_node_fs2.writeFileSync)(modelsJsonPath, JSON.stringify(modelsConfig, null, 2), "utf-8");
131
159
  } else {
132
- originalModel = clawConfig.agents?.defaults?.model?.primary ?? "";
133
- patchedAgentId = "_defaults";
134
- const modelId2 = stripProvider(originalModel);
135
- if (!modelId2) {
136
- throw new Error("No default model configured. Set agents.defaults.model.primary in openclaw.json first.");
137
- }
138
- if (!clawConfig.agents) clawConfig.agents = {};
139
- if (!clawConfig.agents.defaults) clawConfig.agents.defaults = {};
140
- if (!clawConfig.agents.defaults.model) clawConfig.agents.defaults.model = {};
141
- clawConfig.agents.defaults.model.primary = `become/${modelId2}`;
142
- }
143
- (0, import_node_fs2.writeFileSync)(ORIGINAL_MODEL_PATH, originalModel, "utf-8");
144
- (0, import_node_fs2.writeFileSync)(PATCHED_AGENT_PATH, patchedAgentId, "utf-8");
145
- const modelId = stripProvider(originalModel);
146
- if (!clawConfig.models) clawConfig.models = {};
147
- if (!clawConfig.models.providers) clawConfig.models.providers = {};
148
- clawConfig.models.providers.become = {
149
- api: config.llm_provider === "openai" || config.llm_provider === "openrouter" ? "openai-completions" : "anthropic-messages",
150
- baseUrl: `http://127.0.0.1:${config.proxy_port}`,
151
- apiKey: config.llm_api_key,
152
- models: [
153
- { id: modelId, name: `${modelId} via become` }
154
- ]
155
- };
156
- (0, import_node_fs2.writeFileSync)(OPENCLAW_CONFIG, JSON.stringify(clawConfig, null, 2), "utf-8");
157
- console.log("Restarting OpenClaw gateway...");
158
- try {
159
- (0, import_node_child_process.execSync)("openclaw gateway restart", { stdio: "pipe", timeout: 15e3 });
160
- console.log("OpenClaw gateway restarted. Your agent is now routing through become.");
161
- } catch {
162
- console.log("\n*** OpenClaw gateway needs a manual restart to pick up the new config. ***");
163
- console.log("*** Run: openclaw gateway restart ***\n");
160
+ (0, import_node_fs2.writeFileSync)(OPENCLAW_CONFIG, JSON.stringify(clawConfig, null, 2), "utf-8");
164
161
  }
162
+ console.log(` provider: ${providerName}`);
163
+ console.log(` baseUrl: ${originalUrl} -> localhost:${config.proxy_port}`);
164
+ restartGateway();
165
165
  }
166
166
  function restoreOpenClaw() {
167
- if (!(0, import_node_fs2.existsSync)(OPENCLAW_CONFIG)) {
168
- throw new Error(`OpenClaw config not found at ${OPENCLAW_CONFIG}`);
169
- }
170
- if ((0, import_node_fs2.existsSync)(BACKUP_PATH)) {
171
- const backup = (0, import_node_fs2.readFileSync)(BACKUP_PATH, "utf-8");
172
- const backupConfig = parseOpenClawConfig(backup);
173
- if (!backupConfig.models?.providers?.become) {
174
- (0, import_node_fs2.writeFileSync)(OPENCLAW_CONFIG, backup, "utf-8");
175
- restartGateway();
176
- return;
177
- }
167
+ const originalUrl = readSafe(ORIGINAL_URL_PATH);
168
+ const patchInfo = readSafe(PATCHED_PROVIDER_PATH);
169
+ if (!originalUrl || !patchInfo) {
170
+ cleanState();
171
+ return;
178
172
  }
179
- const raw = (0, import_node_fs2.readFileSync)(OPENCLAW_CONFIG, "utf-8");
180
- const config = parseOpenClawConfig(raw);
181
- const patchedAgentId = readStateFile(PATCHED_AGENT_PATH);
182
- const originalModel = readStateFile(ORIGINAL_MODEL_PATH);
183
- if (originalModel) {
184
- const agents = config.agents?.list ?? [];
185
- if (patchedAgentId && patchedAgentId !== "_defaults") {
186
- const agent = agents.find((a) => a.id === patchedAgentId);
187
- if (agent) {
188
- agent.model = originalModel;
189
- }
190
- } else {
191
- if (config.agents?.defaults?.model) {
192
- config.agents.defaults.model.primary = originalModel;
173
+ const [providerName, source] = patchInfo.split(":");
174
+ if (source === "models.json") {
175
+ const clawConfig = parseConfig((0, import_node_fs2.readFileSync)(OPENCLAW_CONFIG, "utf-8"));
176
+ const modelsJsonPath = getModelsJsonPath(clawConfig);
177
+ if (modelsJsonPath && (0, import_node_fs2.existsSync)(modelsJsonPath)) {
178
+ const modelsConfig = JSON.parse((0, import_node_fs2.readFileSync)(modelsJsonPath, "utf-8"));
179
+ if (modelsConfig.providers?.[providerName]) {
180
+ modelsConfig.providers[providerName].baseUrl = originalUrl;
181
+ (0, import_node_fs2.writeFileSync)(modelsJsonPath, JSON.stringify(modelsConfig, null, 2), "utf-8");
193
182
  }
194
183
  }
195
- }
196
- if (config.models?.providers?.become) {
197
- delete config.models.providers.become;
198
- }
199
- for (const provider of Object.values(config.models?.providers ?? {})) {
200
- if (provider && typeof provider === "object" && "_originalModel" in provider) {
201
- delete provider._originalModel;
184
+ } else {
185
+ if ((0, import_node_fs2.existsSync)(OPENCLAW_CONFIG)) {
186
+ const clawConfig = parseConfig((0, import_node_fs2.readFileSync)(OPENCLAW_CONFIG, "utf-8"));
187
+ if (clawConfig.models?.providers?.[providerName]) {
188
+ clawConfig.models.providers[providerName].baseUrl = originalUrl;
189
+ (0, import_node_fs2.writeFileSync)(OPENCLAW_CONFIG, JSON.stringify(clawConfig, null, 2), "utf-8");
190
+ }
202
191
  }
203
192
  }
204
- (0, import_node_fs2.writeFileSync)(OPENCLAW_CONFIG, JSON.stringify(config, null, 2), "utf-8");
193
+ cleanState();
205
194
  restartGateway();
206
195
  }
207
196
  function listOpenClawAgents() {
208
197
  if (!(0, import_node_fs2.existsSync)(OPENCLAW_CONFIG)) return [];
209
198
  try {
210
- const config = parseOpenClawConfig((0, import_node_fs2.readFileSync)(OPENCLAW_CONFIG, "utf-8"));
199
+ const config = parseConfig((0, import_node_fs2.readFileSync)(OPENCLAW_CONFIG, "utf-8"));
211
200
  const agents = config.agents?.list ?? [];
212
- const defaultModel = unbecome(config.agents?.defaults?.model?.primary ?? "unknown");
201
+ const defaultModel = config.agents?.defaults?.model?.primary ?? "unknown";
213
202
  if (agents.length === 0) {
214
203
  return [{ id: "_defaults", model: defaultModel }];
215
204
  }
216
205
  return agents.map((a) => ({
217
206
  id: a.id,
218
- model: unbecome(a.model ?? defaultModel)
207
+ model: a.model ?? defaultModel
219
208
  }));
220
209
  } catch {
221
210
  return [];
222
211
  }
223
212
  }
224
- function stripProvider(model) {
225
- return model.includes("/") ? model.split("/").slice(1).join("/") : model;
226
- }
227
- function unbecome(model) {
228
- return model.startsWith("become/") ? model.replace("become/", "") : model;
213
+ function getModelsJsonPath(clawConfig) {
214
+ const agentList = clawConfig.agents?.list ?? [];
215
+ const defaultAgent = agentList.find((a) => a.default) ?? agentList[0];
216
+ if (defaultAgent?.agentDir) {
217
+ return (0, import_node_path2.join)(defaultAgent.agentDir.replace("~", (0, import_node_os2.homedir)()), "models.json");
218
+ }
219
+ const mainPath = (0, import_node_path2.join)((0, import_node_os2.homedir)(), ".openclaw", "agents", "main", "agent", "models.json");
220
+ if ((0, import_node_fs2.existsSync)(mainPath)) return mainPath;
221
+ return null;
229
222
  }
230
- function parseOpenClawConfig(raw) {
223
+ function parseConfig(raw) {
231
224
  const stripped = raw.replace(/\/\/.*$/gm, "").replace(/\/\*[\s\S]*?\*\//g, "").replace(/,\s*([\]}])/g, "$1");
232
225
  return JSON.parse(stripped);
233
226
  }
234
- function readStateFile(path) {
227
+ function readSafe(path) {
235
228
  try {
236
229
  return (0, import_node_fs2.existsSync)(path) ? (0, import_node_fs2.readFileSync)(path, "utf-8").trim() : "";
237
230
  } catch {
238
231
  return "";
239
232
  }
240
233
  }
234
+ function cleanState() {
235
+ for (const f of [ORIGINAL_URL_PATH, PATCHED_PROVIDER_PATH]) {
236
+ try {
237
+ (0, import_node_fs2.writeFileSync)(f, "", "utf-8");
238
+ } catch {
239
+ }
240
+ }
241
+ }
241
242
  function restartGateway() {
242
243
  console.log("Restarting OpenClaw gateway...");
243
244
  try {
@@ -323,6 +324,11 @@ Proxy port (default 30001): `);
323
324
  }
324
325
  }
325
326
 
327
+ // src/cli/commands.ts
328
+ var import_node_fs7 = require("fs");
329
+ var import_node_path7 = require("path");
330
+ var import_node_os5 = require("os");
331
+
326
332
  // src/proxy/server.ts
327
333
  var import_node_http = require("http");
328
334
 
@@ -749,7 +755,7 @@ Rules:
749
755
 
750
756
  // src/proxy/server.ts
751
757
  var SKILL_CACHE_TTL_MS = 5e3;
752
- function createProxyServer(config, analyzer) {
758
+ function createProxyServer(config, analyzer, overrideUpstreamUrl) {
753
759
  const store = new FileSkillStore({ baseDir: config.baseDir });
754
760
  const trust = new TrustManager(config.baseDir);
755
761
  const extractor = analyzer ? new LessonExtractor(store, trust, analyzer) : null;
@@ -796,7 +802,7 @@ function createProxyServer(config, analyzer) {
796
802
  stats.skills_injected++;
797
803
  }
798
804
  }
799
- const upstreamUrl = buildUpstreamUrl(config, req.url);
805
+ const upstreamUrl = buildUpstreamUrl(overrideUpstreamUrl ?? config.llm_base_url, req.url);
800
806
  const upstreamHeaders = buildUpstreamHeaders(config, req.headers);
801
807
  const isStreaming = body.stream === true;
802
808
  const modifiedBody = JSON.stringify(body);
@@ -880,8 +886,8 @@ function readBody(req) {
880
886
  req.on("error", reject);
881
887
  });
882
888
  }
883
- function buildUpstreamUrl(config, path) {
884
- const base = config.llm_base_url.replace(/\/+$/, "");
889
+ function buildUpstreamUrl(baseUrl, path) {
890
+ const base = baseUrl.replace(/\/+$/, "");
885
891
  return `${base}${path}`;
886
892
  }
887
893
  function buildUpstreamHeaders(config, incomingHeaders) {
@@ -1433,49 +1439,88 @@ var import_node_fs5 = require("fs");
1433
1439
  var import_node_path5 = require("path");
1434
1440
  var import_node_os3 = require("os");
1435
1441
  var import_node_child_process2 = require("child_process");
1436
- var IRONCLAW_ENV = (0, import_node_path5.join)((0, import_node_os3.homedir)(), ".ironclaw", ".env");
1442
+ var IRONCLAW_ENV = (0, import_node_path5.join)(process.env.IRONCLAW_BASE_DIR ?? (0, import_node_path5.join)((0, import_node_os3.homedir)(), ".ironclaw"), ".env");
1437
1443
  var BACKUP_PATH2 = (0, import_node_path5.join)((0, import_node_os3.homedir)(), ".become", "state", "original_ironclaw.env");
1438
1444
  function patchIronClaw(config) {
1439
1445
  if (!(0, import_node_fs5.existsSync)(IRONCLAW_ENV)) {
1440
1446
  throw new Error(`IronClaw .env not found at ${IRONCLAW_ENV}`);
1441
1447
  }
1448
+ if ((0, import_node_fs5.existsSync)(BACKUP_PATH2)) {
1449
+ console.log("become is already connected to IronClaw. Run `become off` first.");
1450
+ return;
1451
+ }
1442
1452
  (0, import_node_fs5.mkdirSync)((0, import_node_path5.join)((0, import_node_os3.homedir)(), ".become", "state"), { recursive: true });
1443
1453
  (0, import_node_fs5.copyFileSync)(IRONCLAW_ENV, BACKUP_PATH2);
1444
- patchDotEnv(IRONCLAW_ENV, {
1445
- LLM_BASE_URL: `http://127.0.0.1:${config.proxy_port}/v1`
1446
- });
1447
- console.log("Restarting IronClaw...");
1448
- try {
1449
- (0, import_node_child_process2.execSync)("ironclaw service restart", { stdio: "pipe", timeout: 15e3 });
1450
- console.log("IronClaw restarted.");
1451
- } catch {
1452
- console.log("\n*** IronClaw needs a manual restart. ***");
1453
- console.log("*** Run: ironclaw service restart ***\n");
1454
+ const content = (0, import_node_fs5.readFileSync)(IRONCLAW_ENV, "utf-8");
1455
+ const backendMatch = content.match(/^LLM_BACKEND=(.+)$/m);
1456
+ const backend = backendMatch?.[1]?.trim().toLowerCase() ?? "openai_compatible";
1457
+ const proxyUrl = `http://127.0.0.1:${config.proxy_port}`;
1458
+ const vars = {};
1459
+ switch (backend) {
1460
+ case "anthropic":
1461
+ vars["ANTHROPIC_BASE_URL"] = proxyUrl;
1462
+ break;
1463
+ case "ollama":
1464
+ vars["OLLAMA_BASE_URL"] = proxyUrl;
1465
+ break;
1466
+ case "nearai":
1467
+ case "near_ai":
1468
+ case "near":
1469
+ vars["NEARAI_BASE_URL"] = proxyUrl;
1470
+ break;
1471
+ default:
1472
+ vars["LLM_BASE_URL"] = proxyUrl;
1473
+ break;
1454
1474
  }
1475
+ patchDotEnv(IRONCLAW_ENV, vars);
1476
+ console.log(` backend: ${backend}`);
1477
+ console.log(` patched: ${Object.keys(vars).join(", ")} -> localhost:${config.proxy_port}`);
1478
+ restartIronClaw();
1455
1479
  }
1456
1480
  function restoreIronClaw() {
1457
1481
  if (!(0, import_node_fs5.existsSync)(BACKUP_PATH2)) {
1458
- throw new Error("No backup found. Was become ever turned on?");
1482
+ return;
1459
1483
  }
1460
1484
  (0, import_node_fs5.copyFileSync)(BACKUP_PATH2, IRONCLAW_ENV);
1485
+ try {
1486
+ (0, import_node_fs5.unlinkSync)(BACKUP_PATH2);
1487
+ } catch {
1488
+ }
1489
+ restartIronClaw();
1490
+ }
1491
+ function restartIronClaw() {
1461
1492
  console.log("Restarting IronClaw...");
1462
1493
  try {
1463
- (0, import_node_child_process2.execSync)("ironclaw service restart", { stdio: "pipe", timeout: 15e3 });
1494
+ (0, import_node_child_process2.execSync)("ironclaw service stop", { stdio: "pipe", timeout: 1e4 });
1495
+ (0, import_node_child_process2.execSync)("ironclaw service start", { stdio: "pipe", timeout: 1e4 });
1464
1496
  console.log("IronClaw restarted.");
1497
+ return;
1498
+ } catch {
1499
+ }
1500
+ try {
1501
+ (0, import_node_child_process2.execSync)("launchctl kickstart -k gui/$(id -u)/com.ironclaw.daemon", { stdio: "pipe", timeout: 1e4 });
1502
+ console.log("IronClaw restarted via launchctl.");
1503
+ return;
1504
+ } catch {
1505
+ }
1506
+ try {
1507
+ (0, import_node_child_process2.execSync)("systemctl --user restart ironclaw", { stdio: "pipe", timeout: 1e4 });
1508
+ console.log("IronClaw restarted via systemd.");
1509
+ return;
1465
1510
  } catch {
1466
- console.log("\n*** IronClaw needs a manual restart. ***");
1467
- console.log("*** Run: ironclaw service restart ***\n");
1468
1511
  }
1512
+ console.log("\n*** IronClaw needs a manual restart. ***");
1513
+ console.log("*** Run: ironclaw service stop && ironclaw service start ***\n");
1469
1514
  }
1470
1515
  function patchDotEnv(path, vars) {
1471
- let content = (0, import_node_fs5.readFileSync)(path, "utf-8");
1516
+ let content = (0, import_node_fs5.existsSync)(path) ? (0, import_node_fs5.readFileSync)(path, "utf-8") : "";
1472
1517
  for (const [key, value] of Object.entries(vars)) {
1473
1518
  const regex = new RegExp(`^${key}=.*$`, "m");
1474
1519
  if (regex.test(content)) {
1475
1520
  content = content.replace(regex, `${key}=${value}`);
1476
1521
  } else {
1477
- content += `
1478
- ${key}=${value}`;
1522
+ content = content.trimEnd() + (content.length > 0 ? "\n" : "") + `${key}=${value}
1523
+ `;
1479
1524
  }
1480
1525
  }
1481
1526
  (0, import_node_fs5.writeFileSync)(path, content, "utf-8");
@@ -1487,40 +1532,88 @@ var import_node_path6 = require("path");
1487
1532
  var import_node_os4 = require("os");
1488
1533
  var import_node_child_process3 = require("child_process");
1489
1534
  var BACKUP_PATH3 = (0, import_node_path6.join)((0, import_node_os4.homedir)(), ".become", "state", "original_nanoclaw.env");
1535
+ var PATCHED_ENV_PATH_FILE = (0, import_node_path6.join)((0, import_node_os4.homedir)(), ".become", "state", "nanoclaw_env_path.txt");
1536
+ var NANOCLAW_URL_VAR = "ANTHROPIC_BASE_URL";
1490
1537
  function patchNanoClaw(config) {
1491
1538
  const envPath = findNanoClawEnv();
1492
1539
  if (!envPath) {
1493
- throw new Error("Could not find NanoClaw .env. Set ANTHROPIC_BASE_URL manually to http://127.0.0.1:" + config.proxy_port);
1540
+ throw new Error(
1541
+ `Could not find NanoClaw .env file.
1542
+ NanoClaw stores .env in its project root (where you cloned it).
1543
+ Set ${NANOCLAW_URL_VAR}=http://127.0.0.1:${config.proxy_port} manually in your NanoClaw .env file.`
1544
+ );
1545
+ }
1546
+ if ((0, import_node_fs6.existsSync)(BACKUP_PATH3)) {
1547
+ console.log("become is already connected to NanoClaw. Run `become off` first.");
1548
+ return;
1494
1549
  }
1495
1550
  (0, import_node_fs6.mkdirSync)((0, import_node_path6.join)((0, import_node_os4.homedir)(), ".become", "state"), { recursive: true });
1496
1551
  (0, import_node_fs6.copyFileSync)(envPath, BACKUP_PATH3);
1552
+ (0, import_node_fs6.writeFileSync)(PATCHED_ENV_PATH_FILE, envPath, "utf-8");
1497
1553
  patchDotEnv2(envPath, {
1498
- ANTHROPIC_BASE_URL: `http://127.0.0.1:${config.proxy_port}`
1554
+ [NANOCLAW_URL_VAR]: `http://127.0.0.1:${config.proxy_port}`
1499
1555
  });
1556
+ console.log(` env file: ${envPath}`);
1557
+ console.log(` patched: ${NANOCLAW_URL_VAR} -> localhost:${config.proxy_port}`);
1500
1558
  restartNanoClaw();
1501
1559
  }
1502
1560
  function restoreNanoClaw() {
1503
- const envPath = findNanoClawEnv();
1504
- if (!(0, import_node_fs6.existsSync)(BACKUP_PATH3) || !envPath) {
1505
- throw new Error("No backup found. Was become ever turned on?");
1561
+ if (!(0, import_node_fs6.existsSync)(BACKUP_PATH3)) {
1562
+ return;
1563
+ }
1564
+ let envPath = null;
1565
+ if ((0, import_node_fs6.existsSync)(PATCHED_ENV_PATH_FILE)) {
1566
+ envPath = (0, import_node_fs6.readFileSync)(PATCHED_ENV_PATH_FILE, "utf-8").trim();
1567
+ }
1568
+ if (!envPath) {
1569
+ envPath = findNanoClawEnv();
1570
+ }
1571
+ if (!envPath) {
1572
+ console.log("Warning: Cannot find NanoClaw .env to restore. Backup is at " + BACKUP_PATH3);
1573
+ return;
1506
1574
  }
1507
1575
  (0, import_node_fs6.copyFileSync)(BACKUP_PATH3, envPath);
1576
+ try {
1577
+ (0, import_node_fs6.unlinkSync)(BACKUP_PATH3);
1578
+ } catch {
1579
+ }
1580
+ try {
1581
+ (0, import_node_fs6.unlinkSync)(PATCHED_ENV_PATH_FILE);
1582
+ } catch {
1583
+ }
1508
1584
  restartNanoClaw();
1509
1585
  }
1510
1586
  function findNanoClawEnv() {
1511
- const candidates = [
1512
- (0, import_node_path6.join)((0, import_node_os4.homedir)(), ".nanoclaw", ".env"),
1513
- (0, import_node_path6.join)((0, import_node_os4.homedir)(), ".config", "nanoclaw", ".env")
1514
- ];
1515
- const plistPath = (0, import_node_path6.join)((0, import_node_os4.homedir)(), "Library", "LaunchAgents", "ai.nanoclaw.agent.plist");
1587
+ const candidates = [];
1588
+ const plistPath = (0, import_node_path6.join)((0, import_node_os4.homedir)(), "Library", "LaunchAgents", "com.nanoclaw.plist");
1516
1589
  if ((0, import_node_fs6.existsSync)(plistPath)) {
1517
1590
  try {
1518
1591
  const plist = (0, import_node_fs6.readFileSync)(plistPath, "utf-8");
1519
- const match = plist.match(/<string>([^<]*\.env)<\/string>/);
1520
- if (match) candidates.unshift(match[1]);
1592
+ const match = plist.match(/<key>WorkingDirectory<\/key>\s*<string>([^<]+)<\/string>/);
1593
+ if (match) candidates.push((0, import_node_path6.join)(match[1], ".env"));
1594
+ } catch {
1595
+ }
1596
+ }
1597
+ const userUnit = (0, import_node_path6.join)((0, import_node_os4.homedir)(), ".config", "systemd", "user", "nanoclaw.service");
1598
+ if ((0, import_node_fs6.existsSync)(userUnit)) {
1599
+ try {
1600
+ const unit = (0, import_node_fs6.readFileSync)(userUnit, "utf-8");
1601
+ const match = unit.match(/WorkingDirectory=(.+)/);
1602
+ if (match) candidates.push((0, import_node_path6.join)(match[1].trim(), ".env"));
1603
+ } catch {
1604
+ }
1605
+ }
1606
+ const rootUnit = "/etc/systemd/system/nanoclaw.service";
1607
+ if ((0, import_node_fs6.existsSync)(rootUnit)) {
1608
+ try {
1609
+ const unit = (0, import_node_fs6.readFileSync)(rootUnit, "utf-8");
1610
+ const match = unit.match(/WorkingDirectory=(.+)/);
1611
+ if (match) candidates.push((0, import_node_path6.join)(match[1].trim(), ".env"));
1521
1612
  } catch {
1522
1613
  }
1523
1614
  }
1615
+ candidates.push((0, import_node_path6.join)((0, import_node_os4.homedir)(), "nanoclaw", ".env"));
1616
+ candidates.push("/opt/nanoclaw/.env");
1524
1617
  for (const path of candidates) {
1525
1618
  if ((0, import_node_fs6.existsSync)(path)) return path;
1526
1619
  }
@@ -1528,35 +1621,207 @@ function findNanoClawEnv() {
1528
1621
  }
1529
1622
  function restartNanoClaw() {
1530
1623
  console.log("Restarting NanoClaw...");
1531
- try {
1532
- (0, import_node_child_process3.execSync)("launchctl kickstart -k gui/$(id -u)/ai.nanoclaw.agent", { stdio: "pipe", timeout: 15e3 });
1533
- console.log("NanoClaw restarted.");
1534
- } catch {
1624
+ const plistPath = (0, import_node_path6.join)((0, import_node_os4.homedir)(), "Library", "LaunchAgents", "com.nanoclaw.plist");
1625
+ if ((0, import_node_fs6.existsSync)(plistPath)) {
1535
1626
  try {
1536
- (0, import_node_child_process3.execSync)("systemctl --user restart nanoclaw", { stdio: "pipe", timeout: 15e3 });
1627
+ (0, import_node_child_process3.execSync)(`launchctl unload "${plistPath}"`, { stdio: "pipe", timeout: 1e4 });
1628
+ (0, import_node_child_process3.execSync)(`launchctl load "${plistPath}"`, { stdio: "pipe", timeout: 1e4 });
1537
1629
  console.log("NanoClaw restarted.");
1630
+ return;
1538
1631
  } catch {
1539
- console.log("\n*** NanoClaw needs a manual restart. ***");
1540
- console.log("*** macOS: launchctl kickstart -k gui/$(id -u)/ai.nanoclaw.agent ***");
1541
- console.log("*** Linux: systemctl --user restart nanoclaw ***\n");
1542
1632
  }
1543
1633
  }
1634
+ try {
1635
+ (0, import_node_child_process3.execSync)("systemctl --user restart nanoclaw", { stdio: "pipe", timeout: 1e4 });
1636
+ console.log("NanoClaw restarted.");
1637
+ return;
1638
+ } catch {
1639
+ }
1640
+ console.log("\n*** NanoClaw needs a manual restart. ***");
1641
+ console.log("*** macOS: launchctl unload ~/Library/LaunchAgents/com.nanoclaw.plist && launchctl load ~/Library/LaunchAgents/com.nanoclaw.plist ***");
1642
+ console.log("*** Linux: systemctl --user restart nanoclaw ***\n");
1544
1643
  }
1545
1644
  function patchDotEnv2(path, vars) {
1546
- let content = (0, import_node_fs6.readFileSync)(path, "utf-8");
1645
+ let content = (0, import_node_fs6.existsSync)(path) ? (0, import_node_fs6.readFileSync)(path, "utf-8") : "";
1547
1646
  for (const [key, value] of Object.entries(vars)) {
1548
1647
  const regex = new RegExp(`^${key}=.*$`, "m");
1549
1648
  if (regex.test(content)) {
1550
1649
  content = content.replace(regex, `${key}=${value}`);
1551
1650
  } else {
1552
- content += `
1553
- ${key}=${value}`;
1651
+ content = content.trimEnd() + (content.length > 0 ? "\n" : "") + `${key}=${value}
1652
+ `;
1554
1653
  }
1555
1654
  }
1556
1655
  (0, import_node_fs6.writeFileSync)(path, content, "utf-8");
1557
1656
  }
1558
1657
 
1658
+ // src/adapters/llm.ts
1659
+ var DEFAULT_TIMEOUT_MS = 6e4;
1660
+ var OpenAIAdapter = class {
1661
+ apiKey;
1662
+ baseUrl;
1663
+ defaultModel;
1664
+ constructor(config) {
1665
+ if (!config.apiKey) throw new Error("OpenAI API key is required");
1666
+ this.apiKey = config.apiKey;
1667
+ this.baseUrl = (config.baseUrl ?? "https://api.openai.com").replace(/\/+$/, "");
1668
+ this.defaultModel = config.model ?? "gpt-4o-mini";
1669
+ }
1670
+ async complete(prompt, opts) {
1671
+ const response = await this.request({
1672
+ model: opts?.model ?? this.defaultModel,
1673
+ messages: [{ role: "user", content: prompt }],
1674
+ max_tokens: opts?.maxTokens ?? 2e3,
1675
+ temperature: opts?.temperature ?? 0.7
1676
+ }, opts?.timeoutMs);
1677
+ return response.choices?.[0]?.message?.content ?? "";
1678
+ }
1679
+ async json(prompt, opts) {
1680
+ const response = await this.request({
1681
+ model: opts?.model ?? this.defaultModel,
1682
+ messages: [{ role: "user", content: prompt }],
1683
+ max_tokens: opts?.maxTokens ?? 2e3,
1684
+ temperature: opts?.temperature ?? 0.3,
1685
+ response_format: { type: "json_object" }
1686
+ }, opts?.timeoutMs);
1687
+ const text = response.choices?.[0]?.message?.content ?? "{}";
1688
+ return JSON.parse(text);
1689
+ }
1690
+ async request(body, timeoutMs) {
1691
+ const res = await fetch(`${this.baseUrl}/v1/chat/completions`, {
1692
+ method: "POST",
1693
+ headers: {
1694
+ "Content-Type": "application/json",
1695
+ "Authorization": `Bearer ${this.apiKey}`
1696
+ },
1697
+ body: JSON.stringify(body),
1698
+ signal: AbortSignal.timeout(timeoutMs ?? DEFAULT_TIMEOUT_MS)
1699
+ });
1700
+ if (!res.ok) {
1701
+ const text = await res.text().catch(() => "unknown error");
1702
+ throw new Error(`OpenAI API error ${res.status}: ${text.slice(0, 200)}`);
1703
+ }
1704
+ return res.json();
1705
+ }
1706
+ };
1707
+ var AnthropicAdapter = class {
1708
+ apiKey;
1709
+ defaultModel;
1710
+ constructor(config) {
1711
+ if (!config.apiKey) throw new Error("Anthropic API key is required");
1712
+ this.apiKey = config.apiKey;
1713
+ this.defaultModel = config.model ?? "claude-sonnet-4-20250514";
1714
+ }
1715
+ async complete(prompt, opts) {
1716
+ const res = await fetch("https://api.anthropic.com/v1/messages", {
1717
+ method: "POST",
1718
+ headers: {
1719
+ "Content-Type": "application/json",
1720
+ "x-api-key": this.apiKey,
1721
+ "anthropic-version": "2023-06-01"
1722
+ },
1723
+ body: JSON.stringify({
1724
+ model: opts?.model ?? this.defaultModel,
1725
+ max_tokens: opts?.maxTokens ?? 2e3,
1726
+ messages: [{ role: "user", content: prompt }]
1727
+ }),
1728
+ signal: AbortSignal.timeout(opts?.timeoutMs ?? DEFAULT_TIMEOUT_MS)
1729
+ });
1730
+ if (!res.ok) {
1731
+ const text = await res.text().catch(() => "unknown error");
1732
+ throw new Error(`Anthropic API error ${res.status}: ${text.slice(0, 200)}`);
1733
+ }
1734
+ const data = await res.json();
1735
+ return data.content?.[0]?.text ?? "";
1736
+ }
1737
+ async json(prompt, opts) {
1738
+ const text = await this.complete(
1739
+ `${prompt}
1740
+
1741
+ Respond with valid JSON only, no other text.`,
1742
+ { ...opts, temperature: opts?.temperature ?? 0.3 }
1743
+ );
1744
+ try {
1745
+ return JSON.parse(text.trim());
1746
+ } catch {
1747
+ const match = text.match(/\{[\s\S]*?\}(?=\s*$|\s*[^}\]])/);
1748
+ const arrMatch = text.match(/\[[\s\S]*?\](?=\s*$|\s*[^}\]])/);
1749
+ const candidate = match?.[0] ?? arrMatch?.[0];
1750
+ if (!candidate) throw new Error("No JSON found in response");
1751
+ return JSON.parse(candidate);
1752
+ }
1753
+ }
1754
+ };
1755
+ var OllamaAdapter = class {
1756
+ baseUrl;
1757
+ defaultModel;
1758
+ constructor(config) {
1759
+ this.baseUrl = (config?.baseUrl ?? "http://localhost:11434").replace(/\/+$/, "");
1760
+ this.defaultModel = config?.model ?? "llama3.1";
1761
+ }
1762
+ async complete(prompt, opts) {
1763
+ const res = await fetch(`${this.baseUrl}/api/generate`, {
1764
+ method: "POST",
1765
+ headers: { "Content-Type": "application/json" },
1766
+ body: JSON.stringify({
1767
+ model: opts?.model ?? this.defaultModel,
1768
+ prompt,
1769
+ stream: false,
1770
+ options: {
1771
+ num_predict: opts?.maxTokens ?? 2e3,
1772
+ temperature: opts?.temperature ?? 0.7
1773
+ }
1774
+ }),
1775
+ signal: AbortSignal.timeout(opts?.timeoutMs ?? DEFAULT_TIMEOUT_MS)
1776
+ });
1777
+ if (!res.ok) {
1778
+ const text = await res.text().catch(() => "unknown error");
1779
+ throw new Error(`Ollama error ${res.status}: ${text.slice(0, 200)}`);
1780
+ }
1781
+ const data = await res.json();
1782
+ return data.response ?? "";
1783
+ }
1784
+ async json(prompt, opts) {
1785
+ const res = await fetch(`${this.baseUrl}/api/generate`, {
1786
+ method: "POST",
1787
+ headers: { "Content-Type": "application/json" },
1788
+ body: JSON.stringify({
1789
+ model: opts?.model ?? this.defaultModel,
1790
+ prompt: `${prompt}
1791
+
1792
+ Respond with valid JSON only.`,
1793
+ stream: false,
1794
+ format: "json",
1795
+ options: {
1796
+ num_predict: opts?.maxTokens ?? 2e3,
1797
+ temperature: opts?.temperature ?? 0.3
1798
+ }
1799
+ }),
1800
+ signal: AbortSignal.timeout(opts?.timeoutMs ?? DEFAULT_TIMEOUT_MS)
1801
+ });
1802
+ if (!res.ok) {
1803
+ const text = await res.text().catch(() => "unknown error");
1804
+ throw new Error(`Ollama error ${res.status}: ${text.slice(0, 200)}`);
1805
+ }
1806
+ const data = await res.json();
1807
+ return JSON.parse(data.response ?? "{}");
1808
+ }
1809
+ };
1810
+
1559
1811
  // src/cli/commands.ts
1812
+ function createAnalyzer(config) {
1813
+ switch (config.llm_provider) {
1814
+ case "anthropic":
1815
+ return { analyze: (p) => new AnthropicAdapter({ apiKey: config.llm_api_key }).complete(p) };
1816
+ case "ollama":
1817
+ return { analyze: (p) => new OllamaAdapter({ baseUrl: config.llm_base_url }).complete(p) };
1818
+ case "openai":
1819
+ case "openrouter":
1820
+ case "custom":
1821
+ default:
1822
+ return { analyze: (p) => new OpenAIAdapter({ apiKey: config.llm_api_key, baseUrl: config.llm_base_url }).complete(p) };
1823
+ }
1824
+ }
1560
1825
  async function start() {
1561
1826
  const config = loadConfig();
1562
1827
  const baseDir = getBecomeDir();
@@ -1569,7 +1834,14 @@ async function start() {
1569
1834
  max_skills_per_call: config.max_skills_per_call,
1570
1835
  auto_extract: config.auto_extract
1571
1836
  };
1572
- const proxy = createProxyServer(proxyConfig);
1837
+ const originalUrlPath = (0, import_node_path7.join)((0, import_node_os5.homedir)(), ".become", "state", "original_base_url.txt");
1838
+ let originalUpstreamUrl;
1839
+ if ((0, import_node_fs7.existsSync)(originalUrlPath)) {
1840
+ const saved = (0, import_node_fs7.readFileSync)(originalUrlPath, "utf-8").trim();
1841
+ if (saved) originalUpstreamUrl = saved;
1842
+ }
1843
+ const analyzer = createAnalyzer(config);
1844
+ const proxy = createProxyServer(proxyConfig, analyzer, originalUpstreamUrl);
1573
1845
  await proxy.listen();
1574
1846
  const dashboard = createDashboardServer({
1575
1847
  store: proxy.store,
@@ -1653,7 +1925,7 @@ Patching ${config.agent_type} config...`);
1653
1925
  console.log(` baseUrl: ${config.llm_base_url} \u2192 localhost:${config.proxy_port}`);
1654
1926
  switch (config.agent_type) {
1655
1927
  case "openclaw":
1656
- patchOpenClaw(config, config.openclaw_agent_id);
1928
+ patchOpenClaw(config);
1657
1929
  break;
1658
1930
  case "ironclaw":
1659
1931
  patchIronClaw(config);
@@ -1722,21 +1994,21 @@ Skills: ${approved} approved, ${pending} pending, ${rejected} rejected`);
1722
1994
  }
1723
1995
 
1724
1996
  // src/cli/init.ts
1725
- var import_node_fs7 = require("fs");
1726
- var import_node_path7 = require("path");
1997
+ var import_node_fs8 = require("fs");
1998
+ var import_node_path8 = require("path");
1727
1999
  var import_node_url = require("url");
1728
2000
  var import_meta = {};
1729
2001
  var command = process.argv[2];
1730
2002
  var VERSION = "unknown";
1731
2003
  try {
1732
- const dir = (0, import_node_path7.dirname)((0, import_node_url.fileURLToPath)(import_meta.url));
1733
- const pkgPath = (0, import_node_path7.join)(dir, "..", "package.json");
1734
- VERSION = JSON.parse((0, import_node_fs7.readFileSync)(pkgPath, "utf-8")).version;
2004
+ const dir = (0, import_node_path8.dirname)((0, import_node_url.fileURLToPath)(import_meta.url));
2005
+ const pkgPath = (0, import_node_path8.join)(dir, "..", "package.json");
2006
+ VERSION = JSON.parse((0, import_node_fs8.readFileSync)(pkgPath, "utf-8")).version;
1735
2007
  } catch {
1736
2008
  try {
1737
- const dir = (0, import_node_path7.dirname)((0, import_node_url.fileURLToPath)(import_meta.url));
1738
- const pkgPath = (0, import_node_path7.join)(dir, "..", "..", "package.json");
1739
- VERSION = JSON.parse((0, import_node_fs7.readFileSync)(pkgPath, "utf-8")).version;
2009
+ const dir = (0, import_node_path8.dirname)((0, import_node_url.fileURLToPath)(import_meta.url));
2010
+ const pkgPath = (0, import_node_path8.join)(dir, "..", "..", "package.json");
2011
+ VERSION = JSON.parse((0, import_node_fs8.readFileSync)(pkgPath, "utf-8")).version;
1740
2012
  } catch {
1741
2013
  }
1742
2014
  }