@openclawcity/become 1.0.13 → 1.0.19

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,187 @@ 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}`);
167
+ const originalUrl = readSafe(ORIGINAL_URL_PATH);
168
+ const patchInfo = readSafe(PATCHED_PROVIDER_PATH);
169
+ if (!originalUrl || !patchInfo) {
170
+ cleanState();
171
+ return;
169
172
  }
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;
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");
182
+ }
183
+ }
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
+ }
177
191
  }
178
192
  }
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;
193
+ cleanLegacy();
194
+ cleanState();
195
+ restartGateway();
196
+ }
197
+ function cleanLegacy() {
198
+ if ((0, import_node_fs2.existsSync)(OPENCLAW_CONFIG)) {
199
+ try {
200
+ const config = parseConfig((0, import_node_fs2.readFileSync)(OPENCLAW_CONFIG, "utf-8"));
201
+ let changed = false;
202
+ if (config.models?.providers?.become) {
203
+ delete config.models.providers.become;
204
+ changed = true;
189
205
  }
190
- } else {
191
- if (config.agents?.defaults?.model) {
192
- config.agents.defaults.model.primary = originalModel;
206
+ const primary = config.agents?.defaults?.model?.primary ?? "";
207
+ if (primary.startsWith("become/")) {
208
+ config.agents.defaults.model.primary = "openrouter/" + primary.slice("become/".length);
209
+ changed = true;
210
+ }
211
+ for (const prov of Object.values(config.models?.providers ?? {})) {
212
+ if (prov && typeof prov === "object" && "_originalModel" in prov) {
213
+ delete prov._originalModel;
214
+ changed = true;
215
+ }
193
216
  }
217
+ if (changed) (0, import_node_fs2.writeFileSync)(OPENCLAW_CONFIG, JSON.stringify(config, null, 2), "utf-8");
218
+ } catch {
194
219
  }
195
220
  }
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;
221
+ try {
222
+ const clawConfig = parseConfig((0, import_node_fs2.readFileSync)(OPENCLAW_CONFIG, "utf-8"));
223
+ const modelsJsonPath = getModelsJsonPath(clawConfig);
224
+ if (modelsJsonPath && (0, import_node_fs2.existsSync)(modelsJsonPath)) {
225
+ const models = JSON.parse((0, import_node_fs2.readFileSync)(modelsJsonPath, "utf-8"));
226
+ let changed = false;
227
+ if (models.providers?.become) {
228
+ delete models.providers.become;
229
+ changed = true;
230
+ }
231
+ if (changed) (0, import_node_fs2.writeFileSync)(modelsJsonPath, JSON.stringify(models, null, 2), "utf-8");
202
232
  }
233
+ } catch {
203
234
  }
204
- (0, import_node_fs2.writeFileSync)(OPENCLAW_CONFIG, JSON.stringify(config, null, 2), "utf-8");
205
- restartGateway();
206
235
  }
207
236
  function listOpenClawAgents() {
208
237
  if (!(0, import_node_fs2.existsSync)(OPENCLAW_CONFIG)) return [];
209
238
  try {
210
- const config = parseOpenClawConfig((0, import_node_fs2.readFileSync)(OPENCLAW_CONFIG, "utf-8"));
239
+ const config = parseConfig((0, import_node_fs2.readFileSync)(OPENCLAW_CONFIG, "utf-8"));
211
240
  const agents = config.agents?.list ?? [];
212
- const defaultModel = unbecome(config.agents?.defaults?.model?.primary ?? "unknown");
241
+ const defaultModel = config.agents?.defaults?.model?.primary ?? "unknown";
213
242
  if (agents.length === 0) {
214
243
  return [{ id: "_defaults", model: defaultModel }];
215
244
  }
216
245
  return agents.map((a) => ({
217
246
  id: a.id,
218
- model: unbecome(a.model ?? defaultModel)
247
+ model: a.model ?? defaultModel
219
248
  }));
220
249
  } catch {
221
250
  return [];
222
251
  }
223
252
  }
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;
253
+ function getModelsJsonPath(clawConfig) {
254
+ const agentList = clawConfig.agents?.list ?? [];
255
+ const defaultAgent = agentList.find((a) => a.default) ?? agentList[0];
256
+ if (defaultAgent?.agentDir) {
257
+ return (0, import_node_path2.join)(defaultAgent.agentDir.replace("~", (0, import_node_os2.homedir)()), "models.json");
258
+ }
259
+ const mainPath = (0, import_node_path2.join)((0, import_node_os2.homedir)(), ".openclaw", "agents", "main", "agent", "models.json");
260
+ if ((0, import_node_fs2.existsSync)(mainPath)) return mainPath;
261
+ return null;
229
262
  }
230
- function parseOpenClawConfig(raw) {
263
+ function parseConfig(raw) {
231
264
  const stripped = raw.replace(/\/\/.*$/gm, "").replace(/\/\*[\s\S]*?\*\//g, "").replace(/,\s*([\]}])/g, "$1");
232
265
  return JSON.parse(stripped);
233
266
  }
234
- function readStateFile(path) {
267
+ function readSafe(path) {
235
268
  try {
236
269
  return (0, import_node_fs2.existsSync)(path) ? (0, import_node_fs2.readFileSync)(path, "utf-8").trim() : "";
237
270
  } catch {
238
271
  return "";
239
272
  }
240
273
  }
274
+ function cleanState() {
275
+ for (const f of [ORIGINAL_URL_PATH, PATCHED_PROVIDER_PATH]) {
276
+ try {
277
+ (0, import_node_fs2.writeFileSync)(f, "", "utf-8");
278
+ } catch {
279
+ }
280
+ }
281
+ }
241
282
  function restartGateway() {
242
283
  console.log("Restarting OpenClaw gateway...");
243
284
  try {
@@ -323,6 +364,11 @@ Proxy port (default 30001): `);
323
364
  }
324
365
  }
325
366
 
367
+ // src/cli/commands.ts
368
+ var import_node_fs7 = require("fs");
369
+ var import_node_path7 = require("path");
370
+ var import_node_os5 = require("os");
371
+
326
372
  // src/proxy/server.ts
327
373
  var import_node_http = require("http");
328
374
 
@@ -749,7 +795,7 @@ Rules:
749
795
 
750
796
  // src/proxy/server.ts
751
797
  var SKILL_CACHE_TTL_MS = 5e3;
752
- function createProxyServer(config, analyzer) {
798
+ function createProxyServer(config, analyzer, overrideUpstreamUrl) {
753
799
  const store = new FileSkillStore({ baseDir: config.baseDir });
754
800
  const trust = new TrustManager(config.baseDir);
755
801
  const extractor = analyzer ? new LessonExtractor(store, trust, analyzer) : null;
@@ -796,7 +842,7 @@ function createProxyServer(config, analyzer) {
796
842
  stats.skills_injected++;
797
843
  }
798
844
  }
799
- const upstreamUrl = buildUpstreamUrl(config, req.url);
845
+ const upstreamUrl = buildUpstreamUrl(overrideUpstreamUrl ?? config.llm_base_url, req.url);
800
846
  const upstreamHeaders = buildUpstreamHeaders(config, req.headers);
801
847
  const isStreaming = body.stream === true;
802
848
  const modifiedBody = JSON.stringify(body);
@@ -880,8 +926,8 @@ function readBody(req) {
880
926
  req.on("error", reject);
881
927
  });
882
928
  }
883
- function buildUpstreamUrl(config, path) {
884
- const base = config.llm_base_url.replace(/\/+$/, "");
929
+ function buildUpstreamUrl(baseUrl, path) {
930
+ const base = baseUrl.replace(/\/+$/, "");
885
931
  return `${base}${path}`;
886
932
  }
887
933
  function buildUpstreamHeaders(config, incomingHeaders) {
@@ -1433,49 +1479,88 @@ var import_node_fs5 = require("fs");
1433
1479
  var import_node_path5 = require("path");
1434
1480
  var import_node_os3 = require("os");
1435
1481
  var import_node_child_process2 = require("child_process");
1436
- var IRONCLAW_ENV = (0, import_node_path5.join)((0, import_node_os3.homedir)(), ".ironclaw", ".env");
1482
+ 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
1483
  var BACKUP_PATH2 = (0, import_node_path5.join)((0, import_node_os3.homedir)(), ".become", "state", "original_ironclaw.env");
1438
1484
  function patchIronClaw(config) {
1439
1485
  if (!(0, import_node_fs5.existsSync)(IRONCLAW_ENV)) {
1440
1486
  throw new Error(`IronClaw .env not found at ${IRONCLAW_ENV}`);
1441
1487
  }
1488
+ if ((0, import_node_fs5.existsSync)(BACKUP_PATH2)) {
1489
+ console.log("become is already connected to IronClaw. Run `become off` first.");
1490
+ return;
1491
+ }
1442
1492
  (0, import_node_fs5.mkdirSync)((0, import_node_path5.join)((0, import_node_os3.homedir)(), ".become", "state"), { recursive: true });
1443
1493
  (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");
1494
+ const content = (0, import_node_fs5.readFileSync)(IRONCLAW_ENV, "utf-8");
1495
+ const backendMatch = content.match(/^LLM_BACKEND=(.+)$/m);
1496
+ const backend = backendMatch?.[1]?.trim().toLowerCase() ?? "openai_compatible";
1497
+ const proxyUrl = `http://127.0.0.1:${config.proxy_port}`;
1498
+ const vars = {};
1499
+ switch (backend) {
1500
+ case "anthropic":
1501
+ vars["ANTHROPIC_BASE_URL"] = proxyUrl;
1502
+ break;
1503
+ case "ollama":
1504
+ vars["OLLAMA_BASE_URL"] = proxyUrl;
1505
+ break;
1506
+ case "nearai":
1507
+ case "near_ai":
1508
+ case "near":
1509
+ vars["NEARAI_BASE_URL"] = proxyUrl;
1510
+ break;
1511
+ default:
1512
+ vars["LLM_BASE_URL"] = proxyUrl;
1513
+ break;
1454
1514
  }
1515
+ patchDotEnv(IRONCLAW_ENV, vars);
1516
+ console.log(` backend: ${backend}`);
1517
+ console.log(` patched: ${Object.keys(vars).join(", ")} -> localhost:${config.proxy_port}`);
1518
+ restartIronClaw();
1455
1519
  }
1456
1520
  function restoreIronClaw() {
1457
1521
  if (!(0, import_node_fs5.existsSync)(BACKUP_PATH2)) {
1458
- throw new Error("No backup found. Was become ever turned on?");
1522
+ return;
1459
1523
  }
1460
1524
  (0, import_node_fs5.copyFileSync)(BACKUP_PATH2, IRONCLAW_ENV);
1525
+ try {
1526
+ (0, import_node_fs5.unlinkSync)(BACKUP_PATH2);
1527
+ } catch {
1528
+ }
1529
+ restartIronClaw();
1530
+ }
1531
+ function restartIronClaw() {
1461
1532
  console.log("Restarting IronClaw...");
1462
1533
  try {
1463
- (0, import_node_child_process2.execSync)("ironclaw service restart", { stdio: "pipe", timeout: 15e3 });
1534
+ (0, import_node_child_process2.execSync)("ironclaw service stop", { stdio: "pipe", timeout: 1e4 });
1535
+ (0, import_node_child_process2.execSync)("ironclaw service start", { stdio: "pipe", timeout: 1e4 });
1464
1536
  console.log("IronClaw restarted.");
1537
+ return;
1538
+ } catch {
1539
+ }
1540
+ try {
1541
+ (0, import_node_child_process2.execSync)("launchctl kickstart -k gui/$(id -u)/com.ironclaw.daemon", { stdio: "pipe", timeout: 1e4 });
1542
+ console.log("IronClaw restarted via launchctl.");
1543
+ return;
1465
1544
  } catch {
1466
- console.log("\n*** IronClaw needs a manual restart. ***");
1467
- console.log("*** Run: ironclaw service restart ***\n");
1468
1545
  }
1546
+ try {
1547
+ (0, import_node_child_process2.execSync)("systemctl --user restart ironclaw", { stdio: "pipe", timeout: 1e4 });
1548
+ console.log("IronClaw restarted via systemd.");
1549
+ return;
1550
+ } catch {
1551
+ }
1552
+ console.log("\n*** IronClaw needs a manual restart. ***");
1553
+ console.log("*** Run: ironclaw service stop && ironclaw service start ***\n");
1469
1554
  }
1470
1555
  function patchDotEnv(path, vars) {
1471
- let content = (0, import_node_fs5.readFileSync)(path, "utf-8");
1556
+ let content = (0, import_node_fs5.existsSync)(path) ? (0, import_node_fs5.readFileSync)(path, "utf-8") : "";
1472
1557
  for (const [key, value] of Object.entries(vars)) {
1473
1558
  const regex = new RegExp(`^${key}=.*$`, "m");
1474
1559
  if (regex.test(content)) {
1475
1560
  content = content.replace(regex, `${key}=${value}`);
1476
1561
  } else {
1477
- content += `
1478
- ${key}=${value}`;
1562
+ content = content.trimEnd() + (content.length > 0 ? "\n" : "") + `${key}=${value}
1563
+ `;
1479
1564
  }
1480
1565
  }
1481
1566
  (0, import_node_fs5.writeFileSync)(path, content, "utf-8");
@@ -1487,40 +1572,88 @@ var import_node_path6 = require("path");
1487
1572
  var import_node_os4 = require("os");
1488
1573
  var import_node_child_process3 = require("child_process");
1489
1574
  var BACKUP_PATH3 = (0, import_node_path6.join)((0, import_node_os4.homedir)(), ".become", "state", "original_nanoclaw.env");
1575
+ var PATCHED_ENV_PATH_FILE = (0, import_node_path6.join)((0, import_node_os4.homedir)(), ".become", "state", "nanoclaw_env_path.txt");
1576
+ var NANOCLAW_URL_VAR = "ANTHROPIC_BASE_URL";
1490
1577
  function patchNanoClaw(config) {
1491
1578
  const envPath = findNanoClawEnv();
1492
1579
  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);
1580
+ throw new Error(
1581
+ `Could not find NanoClaw .env file.
1582
+ NanoClaw stores .env in its project root (where you cloned it).
1583
+ Set ${NANOCLAW_URL_VAR}=http://127.0.0.1:${config.proxy_port} manually in your NanoClaw .env file.`
1584
+ );
1585
+ }
1586
+ if ((0, import_node_fs6.existsSync)(BACKUP_PATH3)) {
1587
+ console.log("become is already connected to NanoClaw. Run `become off` first.");
1588
+ return;
1494
1589
  }
1495
1590
  (0, import_node_fs6.mkdirSync)((0, import_node_path6.join)((0, import_node_os4.homedir)(), ".become", "state"), { recursive: true });
1496
1591
  (0, import_node_fs6.copyFileSync)(envPath, BACKUP_PATH3);
1592
+ (0, import_node_fs6.writeFileSync)(PATCHED_ENV_PATH_FILE, envPath, "utf-8");
1497
1593
  patchDotEnv2(envPath, {
1498
- ANTHROPIC_BASE_URL: `http://127.0.0.1:${config.proxy_port}`
1594
+ [NANOCLAW_URL_VAR]: `http://127.0.0.1:${config.proxy_port}`
1499
1595
  });
1596
+ console.log(` env file: ${envPath}`);
1597
+ console.log(` patched: ${NANOCLAW_URL_VAR} -> localhost:${config.proxy_port}`);
1500
1598
  restartNanoClaw();
1501
1599
  }
1502
1600
  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?");
1601
+ if (!(0, import_node_fs6.existsSync)(BACKUP_PATH3)) {
1602
+ return;
1603
+ }
1604
+ let envPath = null;
1605
+ if ((0, import_node_fs6.existsSync)(PATCHED_ENV_PATH_FILE)) {
1606
+ envPath = (0, import_node_fs6.readFileSync)(PATCHED_ENV_PATH_FILE, "utf-8").trim();
1607
+ }
1608
+ if (!envPath) {
1609
+ envPath = findNanoClawEnv();
1610
+ }
1611
+ if (!envPath) {
1612
+ console.log("Warning: Cannot find NanoClaw .env to restore. Backup is at " + BACKUP_PATH3);
1613
+ return;
1506
1614
  }
1507
1615
  (0, import_node_fs6.copyFileSync)(BACKUP_PATH3, envPath);
1616
+ try {
1617
+ (0, import_node_fs6.unlinkSync)(BACKUP_PATH3);
1618
+ } catch {
1619
+ }
1620
+ try {
1621
+ (0, import_node_fs6.unlinkSync)(PATCHED_ENV_PATH_FILE);
1622
+ } catch {
1623
+ }
1508
1624
  restartNanoClaw();
1509
1625
  }
1510
1626
  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");
1627
+ const candidates = [];
1628
+ const plistPath = (0, import_node_path6.join)((0, import_node_os4.homedir)(), "Library", "LaunchAgents", "com.nanoclaw.plist");
1516
1629
  if ((0, import_node_fs6.existsSync)(plistPath)) {
1517
1630
  try {
1518
1631
  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]);
1632
+ const match = plist.match(/<key>WorkingDirectory<\/key>\s*<string>([^<]+)<\/string>/);
1633
+ if (match) candidates.push((0, import_node_path6.join)(match[1], ".env"));
1521
1634
  } catch {
1522
1635
  }
1523
1636
  }
1637
+ const userUnit = (0, import_node_path6.join)((0, import_node_os4.homedir)(), ".config", "systemd", "user", "nanoclaw.service");
1638
+ if ((0, import_node_fs6.existsSync)(userUnit)) {
1639
+ try {
1640
+ const unit = (0, import_node_fs6.readFileSync)(userUnit, "utf-8");
1641
+ const match = unit.match(/WorkingDirectory=(.+)/);
1642
+ if (match) candidates.push((0, import_node_path6.join)(match[1].trim(), ".env"));
1643
+ } catch {
1644
+ }
1645
+ }
1646
+ const rootUnit = "/etc/systemd/system/nanoclaw.service";
1647
+ if ((0, import_node_fs6.existsSync)(rootUnit)) {
1648
+ try {
1649
+ const unit = (0, import_node_fs6.readFileSync)(rootUnit, "utf-8");
1650
+ const match = unit.match(/WorkingDirectory=(.+)/);
1651
+ if (match) candidates.push((0, import_node_path6.join)(match[1].trim(), ".env"));
1652
+ } catch {
1653
+ }
1654
+ }
1655
+ candidates.push((0, import_node_path6.join)((0, import_node_os4.homedir)(), "nanoclaw", ".env"));
1656
+ candidates.push("/opt/nanoclaw/.env");
1524
1657
  for (const path of candidates) {
1525
1658
  if ((0, import_node_fs6.existsSync)(path)) return path;
1526
1659
  }
@@ -1528,35 +1661,207 @@ function findNanoClawEnv() {
1528
1661
  }
1529
1662
  function restartNanoClaw() {
1530
1663
  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 {
1664
+ const plistPath = (0, import_node_path6.join)((0, import_node_os4.homedir)(), "Library", "LaunchAgents", "com.nanoclaw.plist");
1665
+ if ((0, import_node_fs6.existsSync)(plistPath)) {
1535
1666
  try {
1536
- (0, import_node_child_process3.execSync)("systemctl --user restart nanoclaw", { stdio: "pipe", timeout: 15e3 });
1667
+ (0, import_node_child_process3.execSync)(`launchctl unload "${plistPath}"`, { stdio: "pipe", timeout: 1e4 });
1668
+ (0, import_node_child_process3.execSync)(`launchctl load "${plistPath}"`, { stdio: "pipe", timeout: 1e4 });
1537
1669
  console.log("NanoClaw restarted.");
1670
+ return;
1538
1671
  } 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
1672
  }
1543
1673
  }
1674
+ try {
1675
+ (0, import_node_child_process3.execSync)("systemctl --user restart nanoclaw", { stdio: "pipe", timeout: 1e4 });
1676
+ console.log("NanoClaw restarted.");
1677
+ return;
1678
+ } catch {
1679
+ }
1680
+ console.log("\n*** NanoClaw needs a manual restart. ***");
1681
+ console.log("*** macOS: launchctl unload ~/Library/LaunchAgents/com.nanoclaw.plist && launchctl load ~/Library/LaunchAgents/com.nanoclaw.plist ***");
1682
+ console.log("*** Linux: systemctl --user restart nanoclaw ***\n");
1544
1683
  }
1545
1684
  function patchDotEnv2(path, vars) {
1546
- let content = (0, import_node_fs6.readFileSync)(path, "utf-8");
1685
+ let content = (0, import_node_fs6.existsSync)(path) ? (0, import_node_fs6.readFileSync)(path, "utf-8") : "";
1547
1686
  for (const [key, value] of Object.entries(vars)) {
1548
1687
  const regex = new RegExp(`^${key}=.*$`, "m");
1549
1688
  if (regex.test(content)) {
1550
1689
  content = content.replace(regex, `${key}=${value}`);
1551
1690
  } else {
1552
- content += `
1553
- ${key}=${value}`;
1691
+ content = content.trimEnd() + (content.length > 0 ? "\n" : "") + `${key}=${value}
1692
+ `;
1554
1693
  }
1555
1694
  }
1556
1695
  (0, import_node_fs6.writeFileSync)(path, content, "utf-8");
1557
1696
  }
1558
1697
 
1698
+ // src/adapters/llm.ts
1699
+ var DEFAULT_TIMEOUT_MS = 6e4;
1700
+ var OpenAIAdapter = class {
1701
+ apiKey;
1702
+ baseUrl;
1703
+ defaultModel;
1704
+ constructor(config) {
1705
+ if (!config.apiKey) throw new Error("OpenAI API key is required");
1706
+ this.apiKey = config.apiKey;
1707
+ this.baseUrl = (config.baseUrl ?? "https://api.openai.com").replace(/\/+$/, "");
1708
+ this.defaultModel = config.model ?? "gpt-4o-mini";
1709
+ }
1710
+ async complete(prompt, opts) {
1711
+ const response = await this.request({
1712
+ model: opts?.model ?? this.defaultModel,
1713
+ messages: [{ role: "user", content: prompt }],
1714
+ max_tokens: opts?.maxTokens ?? 2e3,
1715
+ temperature: opts?.temperature ?? 0.7
1716
+ }, opts?.timeoutMs);
1717
+ return response.choices?.[0]?.message?.content ?? "";
1718
+ }
1719
+ async json(prompt, opts) {
1720
+ const response = await this.request({
1721
+ model: opts?.model ?? this.defaultModel,
1722
+ messages: [{ role: "user", content: prompt }],
1723
+ max_tokens: opts?.maxTokens ?? 2e3,
1724
+ temperature: opts?.temperature ?? 0.3,
1725
+ response_format: { type: "json_object" }
1726
+ }, opts?.timeoutMs);
1727
+ const text = response.choices?.[0]?.message?.content ?? "{}";
1728
+ return JSON.parse(text);
1729
+ }
1730
+ async request(body, timeoutMs) {
1731
+ const res = await fetch(`${this.baseUrl}/v1/chat/completions`, {
1732
+ method: "POST",
1733
+ headers: {
1734
+ "Content-Type": "application/json",
1735
+ "Authorization": `Bearer ${this.apiKey}`
1736
+ },
1737
+ body: JSON.stringify(body),
1738
+ signal: AbortSignal.timeout(timeoutMs ?? DEFAULT_TIMEOUT_MS)
1739
+ });
1740
+ if (!res.ok) {
1741
+ const text = await res.text().catch(() => "unknown error");
1742
+ throw new Error(`OpenAI API error ${res.status}: ${text.slice(0, 200)}`);
1743
+ }
1744
+ return res.json();
1745
+ }
1746
+ };
1747
+ var AnthropicAdapter = class {
1748
+ apiKey;
1749
+ defaultModel;
1750
+ constructor(config) {
1751
+ if (!config.apiKey) throw new Error("Anthropic API key is required");
1752
+ this.apiKey = config.apiKey;
1753
+ this.defaultModel = config.model ?? "claude-sonnet-4-20250514";
1754
+ }
1755
+ async complete(prompt, opts) {
1756
+ const res = await fetch("https://api.anthropic.com/v1/messages", {
1757
+ method: "POST",
1758
+ headers: {
1759
+ "Content-Type": "application/json",
1760
+ "x-api-key": this.apiKey,
1761
+ "anthropic-version": "2023-06-01"
1762
+ },
1763
+ body: JSON.stringify({
1764
+ model: opts?.model ?? this.defaultModel,
1765
+ max_tokens: opts?.maxTokens ?? 2e3,
1766
+ messages: [{ role: "user", content: prompt }]
1767
+ }),
1768
+ signal: AbortSignal.timeout(opts?.timeoutMs ?? DEFAULT_TIMEOUT_MS)
1769
+ });
1770
+ if (!res.ok) {
1771
+ const text = await res.text().catch(() => "unknown error");
1772
+ throw new Error(`Anthropic API error ${res.status}: ${text.slice(0, 200)}`);
1773
+ }
1774
+ const data = await res.json();
1775
+ return data.content?.[0]?.text ?? "";
1776
+ }
1777
+ async json(prompt, opts) {
1778
+ const text = await this.complete(
1779
+ `${prompt}
1780
+
1781
+ Respond with valid JSON only, no other text.`,
1782
+ { ...opts, temperature: opts?.temperature ?? 0.3 }
1783
+ );
1784
+ try {
1785
+ return JSON.parse(text.trim());
1786
+ } catch {
1787
+ const match = text.match(/\{[\s\S]*?\}(?=\s*$|\s*[^}\]])/);
1788
+ const arrMatch = text.match(/\[[\s\S]*?\](?=\s*$|\s*[^}\]])/);
1789
+ const candidate = match?.[0] ?? arrMatch?.[0];
1790
+ if (!candidate) throw new Error("No JSON found in response");
1791
+ return JSON.parse(candidate);
1792
+ }
1793
+ }
1794
+ };
1795
+ var OllamaAdapter = class {
1796
+ baseUrl;
1797
+ defaultModel;
1798
+ constructor(config) {
1799
+ this.baseUrl = (config?.baseUrl ?? "http://localhost:11434").replace(/\/+$/, "");
1800
+ this.defaultModel = config?.model ?? "llama3.1";
1801
+ }
1802
+ async complete(prompt, opts) {
1803
+ const res = await fetch(`${this.baseUrl}/api/generate`, {
1804
+ method: "POST",
1805
+ headers: { "Content-Type": "application/json" },
1806
+ body: JSON.stringify({
1807
+ model: opts?.model ?? this.defaultModel,
1808
+ prompt,
1809
+ stream: false,
1810
+ options: {
1811
+ num_predict: opts?.maxTokens ?? 2e3,
1812
+ temperature: opts?.temperature ?? 0.7
1813
+ }
1814
+ }),
1815
+ signal: AbortSignal.timeout(opts?.timeoutMs ?? DEFAULT_TIMEOUT_MS)
1816
+ });
1817
+ if (!res.ok) {
1818
+ const text = await res.text().catch(() => "unknown error");
1819
+ throw new Error(`Ollama error ${res.status}: ${text.slice(0, 200)}`);
1820
+ }
1821
+ const data = await res.json();
1822
+ return data.response ?? "";
1823
+ }
1824
+ async json(prompt, opts) {
1825
+ const res = await fetch(`${this.baseUrl}/api/generate`, {
1826
+ method: "POST",
1827
+ headers: { "Content-Type": "application/json" },
1828
+ body: JSON.stringify({
1829
+ model: opts?.model ?? this.defaultModel,
1830
+ prompt: `${prompt}
1831
+
1832
+ Respond with valid JSON only.`,
1833
+ stream: false,
1834
+ format: "json",
1835
+ options: {
1836
+ num_predict: opts?.maxTokens ?? 2e3,
1837
+ temperature: opts?.temperature ?? 0.3
1838
+ }
1839
+ }),
1840
+ signal: AbortSignal.timeout(opts?.timeoutMs ?? DEFAULT_TIMEOUT_MS)
1841
+ });
1842
+ if (!res.ok) {
1843
+ const text = await res.text().catch(() => "unknown error");
1844
+ throw new Error(`Ollama error ${res.status}: ${text.slice(0, 200)}`);
1845
+ }
1846
+ const data = await res.json();
1847
+ return JSON.parse(data.response ?? "{}");
1848
+ }
1849
+ };
1850
+
1559
1851
  // src/cli/commands.ts
1852
+ function createAnalyzer(config) {
1853
+ switch (config.llm_provider) {
1854
+ case "anthropic":
1855
+ return { analyze: (p) => new AnthropicAdapter({ apiKey: config.llm_api_key }).complete(p) };
1856
+ case "ollama":
1857
+ return { analyze: (p) => new OllamaAdapter({ baseUrl: config.llm_base_url }).complete(p) };
1858
+ case "openai":
1859
+ case "openrouter":
1860
+ case "custom":
1861
+ default:
1862
+ return { analyze: (p) => new OpenAIAdapter({ apiKey: config.llm_api_key, baseUrl: config.llm_base_url }).complete(p) };
1863
+ }
1864
+ }
1560
1865
  async function start() {
1561
1866
  const config = loadConfig();
1562
1867
  const baseDir = getBecomeDir();
@@ -1569,7 +1874,14 @@ async function start() {
1569
1874
  max_skills_per_call: config.max_skills_per_call,
1570
1875
  auto_extract: config.auto_extract
1571
1876
  };
1572
- const proxy = createProxyServer(proxyConfig);
1877
+ const originalUrlPath = (0, import_node_path7.join)((0, import_node_os5.homedir)(), ".become", "state", "original_base_url.txt");
1878
+ let originalUpstreamUrl;
1879
+ if ((0, import_node_fs7.existsSync)(originalUrlPath)) {
1880
+ const saved = (0, import_node_fs7.readFileSync)(originalUrlPath, "utf-8").trim();
1881
+ if (saved) originalUpstreamUrl = saved;
1882
+ }
1883
+ const analyzer = createAnalyzer(config);
1884
+ const proxy = createProxyServer(proxyConfig, analyzer, originalUpstreamUrl);
1573
1885
  await proxy.listen();
1574
1886
  const dashboard = createDashboardServer({
1575
1887
  store: proxy.store,
@@ -1653,7 +1965,7 @@ Patching ${config.agent_type} config...`);
1653
1965
  console.log(` baseUrl: ${config.llm_base_url} \u2192 localhost:${config.proxy_port}`);
1654
1966
  switch (config.agent_type) {
1655
1967
  case "openclaw":
1656
- patchOpenClaw(config, config.openclaw_agent_id);
1968
+ patchOpenClaw(config);
1657
1969
  break;
1658
1970
  case "ironclaw":
1659
1971
  patchIronClaw(config);
@@ -1722,21 +2034,21 @@ Skills: ${approved} approved, ${pending} pending, ${rejected} rejected`);
1722
2034
  }
1723
2035
 
1724
2036
  // src/cli/init.ts
1725
- var import_node_fs7 = require("fs");
1726
- var import_node_path7 = require("path");
2037
+ var import_node_fs8 = require("fs");
2038
+ var import_node_path8 = require("path");
1727
2039
  var import_node_url = require("url");
1728
2040
  var import_meta = {};
1729
2041
  var command = process.argv[2];
1730
2042
  var VERSION = "unknown";
1731
2043
  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;
2044
+ const dir = (0, import_node_path8.dirname)((0, import_node_url.fileURLToPath)(import_meta.url));
2045
+ const pkgPath = (0, import_node_path8.join)(dir, "..", "package.json");
2046
+ VERSION = JSON.parse((0, import_node_fs8.readFileSync)(pkgPath, "utf-8")).version;
1735
2047
  } catch {
1736
2048
  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;
2049
+ const dir = (0, import_node_path8.dirname)((0, import_node_url.fileURLToPath)(import_meta.url));
2050
+ const pkgPath = (0, import_node_path8.join)(dir, "..", "..", "package.json");
2051
+ VERSION = JSON.parse((0, import_node_fs8.readFileSync)(pkgPath, "utf-8")).version;
1740
2052
  } catch {
1741
2053
  }
1742
2054
  }