boaclaw 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/bin/boaclaw.js ADDED
@@ -0,0 +1,292 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * boaclaw — BOA setup for Claude Code & Factory
5
+ *
6
+ * Usage:
7
+ * boaclaw <API_KEY> — setup with key
8
+ * boaclaw — interactive prompt
9
+ * boaclaw --restore — remove all vars & restore Factory config
10
+ */
11
+
12
+ import { readFileSync, writeFileSync, copyFileSync, appendFileSync, existsSync, rmSync, mkdirSync } from "fs";
13
+ import { join } from "path";
14
+ import { homedir, platform } from "os";
15
+ import { execSync } from "child_process";
16
+ import { createInterface } from "readline";
17
+
18
+ // ─── Helpers ─────────────────────────────────────────────────────────
19
+
20
+ function ask(question) {
21
+ const rl = createInterface({ input: process.stdin, output: process.stderr, terminal: true });
22
+ return new Promise((resolve) =>
23
+ rl.question(question, (answer) => { rl.close(); resolve(answer.trim()); })
24
+ );
25
+ }
26
+
27
+ const out = (msg = "") => process.stdout.write(msg + "\n");
28
+ const err = (msg = "") => process.stderr.write(msg + "\n");
29
+ const log = (msg) => err(`\x1b[36m>\x1b[0m ${msg}`);
30
+ const ok = (msg) => err(`\x1b[32m✔\x1b[0m ${msg}`);
31
+ const warn = (msg) => err(`\x1b[33m!\x1b[0m ${msg}`);
32
+ const fail = (msg) => err(`\x1b[31m✖\x1b[0m ${msg}`);
33
+
34
+ const OS = platform();
35
+ const HOME = homedir();
36
+ const BACKUP_DIR = join(HOME, ".boaclaw");
37
+ const MARKER_FILE = join(BACKUP_DIR, "pending");
38
+ const BACKUP_FILE = join(BACKUP_DIR, "backup.json");
39
+
40
+ const BASE_URL = "https://api.bayofassets.com/";
41
+
42
+ const VAR_KEYS = [
43
+ "ANTHROPIC_BASE_URL",
44
+ "ANTHROPIC_AUTH_TOKEN",
45
+ "ANTHROPIC_DEFAULT_HAIKU_MODEL",
46
+ "ANTHROPIC_DEFAULT_SONNET_MODEL",
47
+ "ANTHROPIC_DEFAULT_OPUS_MODEL",
48
+ ];
49
+
50
+ // ─── Banner ──────────────────────────────────────────────────────────
51
+
52
+ out("");
53
+ out("\x1b[1m boaclaw\x1b[0m \u2014 BOA setup for Claude Code & Factory");
54
+ out(" \x1b[2m\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\x1b[0m");
55
+ out("");
56
+
57
+ // ─── Arg parsing ─────────────────────────────────────────────────────
58
+
59
+ const args = process.argv.slice(2);
60
+ const restore = args.includes("--restore");
61
+ const keyArg = args.find(a => !a.startsWith("-") && a.length > 8);
62
+ const apiKey = !restore ? (process.env.BOA_API_KEY || keyArg) : null;
63
+
64
+ // ─── First-run detection ──────────────────────────────────────────────
65
+
66
+ const isNpx = (process.env.npm_execpath || "").includes("npx") || process.argv[1].includes("_npx");
67
+ const isFirstRun = !isNpx && existsSync(MARKER_FILE);
68
+
69
+ // ════════════════════════════════════════════════════════════════════
70
+ // RESTORE MODE
71
+ // ════════════════════════════════════════════════════════════════════
72
+
73
+ if (restore) {
74
+ log("Removing boaclaw environment variables...");
75
+
76
+ if (OS === "win32") {
77
+ for (const key of VAR_KEYS) {
78
+ try { execSync(`reg delete "HKLM\\SYSTEM\\CurrentControlSet\\Control\\Session Manager\\Environment" /v "${key}" /f`, { stdio: "ignore" }); } catch {}
79
+ try { execSync(`reg delete "HKCU\\Environment" /v "${key}" /f`, { stdio: "ignore" }); } catch {}
80
+ ok(`Removed ${key}`);
81
+ }
82
+ } else {
83
+ const marker = "# --- BOA / Claude Code env ---";
84
+ const endMarker = "# --- end BOA ---";
85
+ const rcFiles = ["/etc/environment", "/etc/zshenv", "/etc/bash.bashrc",
86
+ join(HOME, ".bashrc"), join(HOME, ".zshrc")];
87
+ for (const rc of rcFiles) {
88
+ if (!existsSync(rc)) continue;
89
+ const content = readFileSync(rc, "utf-8");
90
+ if (!content.includes(marker)) continue;
91
+ const regex = new RegExp(
92
+ `\\n?${marker.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}[\\s\\S]*?${endMarker.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\n?`, "g"
93
+ );
94
+ writeFileSync(rc, content.replace(regex, ""), "utf-8");
95
+ ok(`Cleaned ${rc}`);
96
+ }
97
+ }
98
+
99
+ // Restore Factory settings from backup if available
100
+ const factoryPath = join(HOME, ".factory", "settings.json");
101
+ if (existsSync(BACKUP_FILE)) {
102
+ const backup = JSON.parse(readFileSync(BACKUP_FILE, "utf-8"));
103
+ if (backup.factory && existsSync(factoryPath)) {
104
+ log("Restoring Factory settings...");
105
+ writeFileSync(factoryPath, JSON.stringify(backup.factory, null, 2), "utf-8");
106
+ ok("Factory settings restored.");
107
+ }
108
+ } else {
109
+ warn("No backup found — env vars removed, Factory settings unchanged.");
110
+ }
111
+
112
+ try { rmSync(BACKUP_FILE); } catch {}
113
+
114
+ out("");
115
+ ok("Restore complete! Open a new terminal to apply changes.");
116
+ out("");
117
+ process.exit(0);
118
+ }
119
+
120
+ // ════════════════════════════════════════════════════════════════════
121
+ // SETUP MODE
122
+ // ════════════════════════════════════════════════════════════════════
123
+
124
+ if (!apiKey && isFirstRun) {
125
+ out(" \x1b[33m\u26a1 Setup required!\x1b[0m Run with your BOA API key:");
126
+ out("");
127
+ out(" \x1b[36mboaclaw\x1b[0m \x1b[33m<your-api-key>\x1b[0m");
128
+ out("");
129
+ out(" Get your key at \x1b[4mhttps://bayofassets.com\x1b[0m");
130
+ out("");
131
+ process.exit(0);
132
+ }
133
+
134
+ let key = apiKey;
135
+ if (!key) {
136
+ try { key = await ask(" Enter your BOA API key: "); }
137
+ catch (e) { fail(e.message); process.exit(1); }
138
+ }
139
+ if (!key) { fail("API key cannot be empty."); process.exit(1); }
140
+
141
+ err("");
142
+
143
+ // ─── Backup Factory config ────────────────────────────────────────────
144
+
145
+ mkdirSync(BACKUP_DIR, { recursive: true });
146
+
147
+ const backup = { factory: null };
148
+ const factoryPath = join(HOME, ".factory", "settings.json");
149
+
150
+ if (existsSync(factoryPath)) {
151
+ let raw = readFileSync(factoryPath, "utf-8");
152
+ if (raw.charCodeAt(0) === 0xfeff) raw = raw.slice(1);
153
+ backup.factory = JSON.parse(raw.trim() || "{}");
154
+ }
155
+
156
+ writeFileSync(BACKUP_FILE, JSON.stringify(backup, null, 2), "utf-8");
157
+ ok(`Backup saved to ${BACKUP_FILE}`);
158
+
159
+ // ─── Step 1: Add models to Factory ──────────────────────────────────
160
+
161
+ if (existsSync(factoryPath)) {
162
+ log("Adding models to Factory...");
163
+
164
+ copyFileSync(factoryPath, `${factoryPath}.backup`);
165
+
166
+ let raw = readFileSync(factoryPath, "utf-8");
167
+ if (raw.charCodeAt(0) === 0xfeff) raw = raw.slice(1);
168
+ const settings = JSON.parse(raw.trim() || "{}");
169
+
170
+ const newModels = [
171
+ { model: "claude-sonnet-4-6-20250929", baseUrl: BASE_URL, provider: "anthropic" },
172
+ { model: "claude-opus-4-6-thinking", baseUrl: BASE_URL, provider: "anthropic" },
173
+ { model: "claude-haiku-4-5-20251001", baseUrl: BASE_URL, provider: "anthropic" },
174
+ { model: "gemini-3.1-pro", baseUrl: BASE_URL + "v1", provider: "openai" },
175
+ { model: "gemini-3-flash", baseUrl: BASE_URL + "v1", provider: "openai" },
176
+ { model: "gpt-5.1", baseUrl: BASE_URL + "v1", provider: "openai" },
177
+ { model: "gpt-5.1-codex", baseUrl: BASE_URL + "v1", provider: "openai" },
178
+ { model: "gpt-5.1-codex-max", baseUrl: BASE_URL + "v1", provider: "openai" },
179
+ { model: "gpt-5.2", baseUrl: BASE_URL + "v1", provider: "openai" },
180
+ { model: "gpt-5.2-codex", baseUrl: BASE_URL + "v1", provider: "openai" },
181
+ { model: "gpt-5.3-codex", baseUrl: BASE_URL + "v1", provider: "openai" },
182
+ ];
183
+
184
+ const existing = settings.customModels || [];
185
+ const existingNames = new Set(existing.map((m) => m.model));
186
+ let maxIndex = existing.reduce((max, m) => Math.max(max, m.index ?? -1), -1);
187
+ let nextIndex = maxIndex + 1;
188
+ let added = 0, updated = 0;
189
+
190
+ for (const m of newModels) {
191
+ if (existingNames.has(m.model)) {
192
+ for (const e of existing) {
193
+ if (e.model === m.model) { e.apiKey = key; e.baseUrl = m.baseUrl; e.provider = m.provider; }
194
+ }
195
+ updated++;
196
+ } else {
197
+ existing.push({ model: m.model, id: `custom:${m.model}-${nextIndex}`, index: nextIndex,
198
+ baseUrl: m.baseUrl, apiKey: key, displayName: m.model, noImageSupport: false, provider: m.provider });
199
+ nextIndex++; added++;
200
+ }
201
+ }
202
+
203
+ settings.customModels = existing;
204
+ writeFileSync(factoryPath, JSON.stringify(settings, null, 2), "utf-8");
205
+ ok(`Factory: ${added} added, ${updated} updated (${existing.length} total)`);
206
+ } else {
207
+ warn("Factory settings not found — skipped.");
208
+ }
209
+
210
+ // ─── Step 2: Set env vars (system-wide, fallback to user) ─────────────
211
+
212
+ log("Setting environment variables system-wide...");
213
+
214
+ const vars = {
215
+ ANTHROPIC_BASE_URL: BASE_URL,
216
+ ANTHROPIC_AUTH_TOKEN: key,
217
+ ANTHROPIC_DEFAULT_HAIKU_MODEL: "claude-haiku-4-5-20251001",
218
+ ANTHROPIC_DEFAULT_SONNET_MODEL: "claude-sonnet-4-6-20250929",
219
+ ANTHROPIC_DEFAULT_OPUS_MODEL: "claude-opus-4-6-thinking",
220
+ };
221
+
222
+ if (OS === "win32") {
223
+ try {
224
+ for (const [k, v] of Object.entries(vars)) {
225
+ execSync(
226
+ `reg add "HKLM\\SYSTEM\\CurrentControlSet\\Control\\Session Manager\\Environment" /v "${k}" /t REG_SZ /d "${v}" /f`,
227
+ { stdio: "ignore" }
228
+ );
229
+ }
230
+ ok("Env vars written system-wide (HKLM \u2014 all users).");
231
+ } catch {
232
+ warn("No admin rights \u2014 setting for current user only (HKCU).");
233
+ for (const [k, v] of Object.entries(vars)) {
234
+ execSync(`reg add "HKCU\\Environment" /v "${k}" /t REG_SZ /d "${v}" /f`, { stdio: "ignore" });
235
+ }
236
+ ok("Env vars written to HKCU\\Environment.");
237
+ }
238
+ } else {
239
+ const marker = "# --- BOA / Claude Code env ---";
240
+ const endMarker = "# --- end BOA ---";
241
+ const block = ["", marker, ...Object.entries(vars).map(([k, v]) => `export ${k}="${v}"`), endMarker, ""].join("\n");
242
+
243
+ const systemFiles = OS === "darwin"
244
+ ? ["/etc/zshenv", "/etc/bashrc"]
245
+ : ["/etc/environment", "/etc/bash.bashrc", "/etc/zshenv"];
246
+ const userFiles = [join(HOME, ".bashrc"), join(HOME, ".zshrc")];
247
+
248
+ const writeToRc = (rc) => {
249
+ if (!existsSync(rc)) return false;
250
+ const content = readFileSync(rc, "utf-8");
251
+ if (content.includes(marker)) {
252
+ const regex = new RegExp(
253
+ `\\n?${marker.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}[\\s\\S]*?${endMarker.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\n?`, "g"
254
+ );
255
+ writeFileSync(rc, content.replace(regex, block), "utf-8");
256
+ } else {
257
+ appendFileSync(rc, block, "utf-8");
258
+ }
259
+ return true;
260
+ };
261
+
262
+ let written = [];
263
+ let systemOk = false;
264
+ for (const rc of systemFiles) {
265
+ try { if (writeToRc(rc)) { written.push(rc); systemOk = true; } } catch {}
266
+ }
267
+
268
+ if (systemOk) {
269
+ ok(`Env vars written system-wide: ${written.join(", ")}`);
270
+ } else {
271
+ warn("No write access to system files \u2014 writing to user profile.");
272
+ for (const rc of userFiles) { if (writeToRc(rc)) written.push(rc); }
273
+ if (written.length === 0) { appendFileSync(join(HOME, ".bashrc"), block, "utf-8"); written.push(join(HOME, ".bashrc")); }
274
+ ok(`Env vars written to: ${written.join(", ")}`);
275
+ }
276
+ }
277
+
278
+ // ─── Done ────────────────────────────────────────────────────────────
279
+
280
+ try { rmSync(MARKER_FILE); } catch {}
281
+
282
+ out("");
283
+ out(" \x1b[1mEnvironment variables set:\x1b[0m");
284
+ for (const [k, v] of Object.entries(vars)) {
285
+ const display = k === "ANTHROPIC_AUTH_TOKEN" ? v.slice(0, 8) + "..." : v;
286
+ out(` ${k} = ${display}`);
287
+ }
288
+ out("");
289
+ ok("All done! Open a new terminal, then launch Claude Code.");
290
+ out("");
291
+ out(" \x1b[2mTo undo: boaclaw --restore\x1b[0m");
292
+ out("");
@@ -0,0 +1,30 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { join } from "path";
4
+ import { homedir } from "os";
5
+ import { writeFileSync, mkdirSync } from "fs";
6
+
7
+ const markerDir = join(homedir(), ".boaclaw");
8
+ const markerFile = join(markerDir, "pending");
9
+
10
+ try {
11
+ mkdirSync(markerDir, { recursive: true });
12
+ writeFileSync(markerFile, "1", "utf-8");
13
+ } catch {}
14
+
15
+ const apiKey = process.env.BOA_API_KEY;
16
+ if (apiKey) {
17
+ await import("./boaclaw.js");
18
+ process.exit(0);
19
+ }
20
+
21
+ const w = (msg = "") => process.stdout.write(msg + "\n");
22
+
23
+ w();
24
+ w(" \x1b[1mboaclaw\x1b[0m \u2014 BOA setup for Claude Code & Factory");
25
+ w(" \x1b[2m\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\x1b[0m");
26
+ w();
27
+ w(" \x1b[32m\u2714 Installed!\x1b[0m Now run:");
28
+ w();
29
+ w(" \x1b[36mboaclaw\x1b[0m \x1b[33m<your-boa-api-key>\x1b[0m");
30
+ w();
package/package.json ADDED
@@ -0,0 +1,30 @@
1
+ {
2
+ "name": "boaclaw",
3
+ "version": "1.0.0",
4
+ "description": "BOA setup for Claude Code and Factory",
5
+ "type": "module",
6
+ "bin": {
7
+ "boaclaw": "bin/boaclaw.js"
8
+ },
9
+ "scripts": {
10
+ "postinstall": "node bin/postinstall.mjs"
11
+ },
12
+ "files": [
13
+ "bin/boaclaw.js",
14
+ "bin/postinstall.mjs"
15
+ ],
16
+ "keywords": [
17
+ "claude",
18
+ "claude-code",
19
+ "boa",
20
+ "bayofassets",
21
+ "factory",
22
+ "ai",
23
+ "llm",
24
+ "setup"
25
+ ],
26
+ "license": "MIT",
27
+ "engines": {
28
+ "node": ">=18"
29
+ }
30
+ }