setclaw 1.2.1 → 1.3.1

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.
Files changed (3) hide show
  1. package/README.md +29 -1
  2. package/bin/setclaw.js +232 -106
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -65,6 +65,23 @@ npm i -g setclaw
65
65
  setclaw <your-quatarly-api-key>
66
66
  ```
67
67
 
68
+ ### What it does
69
+
70
+ - ✔ Backs up your current env vars and Factory config to `~/.setclaw/backup.json`
71
+ - ✔ Adds all 11 models to Factory AI Droid
72
+ - ✔ Sets env vars **system-wide** (all users) — falls back to current user if no admin rights
73
+ - ✔ Works on Windows, macOS, and Linux
74
+
75
+ ### Restore Original Settings
76
+
77
+ Made a mistake or want to undo everything?
78
+
79
+ ```bash
80
+ setclaw --restore
81
+ ```
82
+
83
+ This removes all env vars setclaw set and restores your original Factory config from the backup.
84
+
68
85
  ---
69
86
 
70
87
  ## Using Claude Code
@@ -157,7 +174,18 @@ Then create an account at [app.factory.ai](https://app.factory.ai), run `droid`
157
174
  <details>
158
175
  <summary><b>Can I run it again with a different API key?</b></summary>
159
176
 
160
- Yes. Just run `npx setclaw@latest <new-key>` again — it updates existing models and env vars without creating duplicates. A backup of your Factory settings is saved before any changes.
177
+ Yes. Just run `npx setclaw@latest <new-key>` or `setclaw <new-key>` again — it updates existing models and env vars without creating duplicates. A fresh backup is saved before every run.
178
+
179
+ </details>
180
+
181
+ <details>
182
+ <summary><b>How do I restore my original settings?</b></summary>
183
+
184
+ ```bash
185
+ setclaw --restore
186
+ ```
187
+
188
+ This reverts all env vars to their pre-setclaw values and restores your original Factory config from `~/.setclaw/backup.json`.
161
189
 
162
190
  </details>
163
191
 
package/bin/setclaw.js CHANGED
@@ -4,9 +4,9 @@
4
4
  * setclaw — BOA & Quatarly setup for Claude Code & Factory
5
5
  *
6
6
  * Usage:
7
- * setclaw <API_KEY>
8
- * QUATARLY_API_KEY=<key> setclaw
9
- * setclaw (prompts interactively)
7
+ * setclaw <API_KEY> — setup with key
8
+ * setclaw — interactive prompt
9
+ * setclaw --restore — restore original env vars & Factory config
10
10
  */
11
11
 
12
12
  import { readFileSync, writeFileSync, copyFileSync, appendFileSync, existsSync, rmSync, mkdirSync } from "fs";
@@ -20,29 +20,30 @@ import { createInterface } from "readline";
20
20
  function ask(question) {
21
21
  const rl = createInterface({ input: process.stdin, output: process.stderr, terminal: true });
22
22
  return new Promise((resolve) =>
23
- rl.question(question, (answer) => {
24
- rl.close();
25
- resolve(answer.trim());
26
- })
23
+ rl.question(question, (answer) => { rl.close(); resolve(answer.trim()); })
27
24
  );
28
25
  }
29
26
 
30
- const out = (msg) => process.stdout.write(msg + "\n");
31
- const err = (msg) => process.stderr.write(msg + "\n");
32
-
33
- function log(msg) { err(`\x1b[36m>\x1b[0m ${msg}`); }
34
- function success(msg) { err(`\x1b[32m✔\x1b[0m ${msg}`); }
35
- function warn(msg) { err(`\x1b[33m!\x1b[0m ${msg}`); }
36
- function fail(msg) { err(`\x1b[31m✖\x1b[0m ${msg}`); }
37
-
38
- // ─── First-run detection ──────────────────────────────────────────────
39
- // If run via npx (npm_execpath contains 'npx' or no global install marker),
40
- // skip the first-run gate and always go interactive.
41
-
42
- const markerFile = join(homedir(), ".setclaw", "pending");
43
- const isNpx = (process.env.npm_execpath || "").includes("npx") ||
44
- process.argv[1].includes("_npx");
45
- const isFirstRun = !isNpx && existsSync(markerFile);
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, ".setclaw");
37
+ const MARKER_FILE = join(BACKUP_DIR, "pending");
38
+ const BACKUP_FILE = join(BACKUP_DIR, "backup.json");
39
+
40
+ const VAR_KEYS = [
41
+ "ANTHROPIC_BASE_URL",
42
+ "ANTHROPIC_AUTH_TOKEN",
43
+ "ANTHROPIC_DEFAULT_HAIKU_MODEL",
44
+ "ANTHROPIC_DEFAULT_SONNET_MODEL",
45
+ "ANTHROPIC_DEFAULT_OPUS_MODEL",
46
+ ];
46
47
 
47
48
  // ─── Banner ──────────────────────────────────────────────────────────
48
49
 
@@ -51,11 +52,93 @@ out("\x1b[1m setclaw\x1b[0m — BOA & Quatarly setup for Claude Code & Factory"
51
52
  out(" \x1b[2m─────────────────────────────────────────────────\x1b[0m");
52
53
  out("");
53
54
 
54
- // ─── Get API key ─────────────────────────────────────────────────────
55
+ // ─── Arg parsing ─────────────────────────────────────────────────────
56
+
57
+ const args = process.argv.slice(2);
58
+ const restore = args.includes("--restore");
59
+
60
+ // Only treat an arg as API key if it doesn't start with - and looks like a key
61
+ const keyArg = args.find(a => !a.startsWith("-") && a.length > 8);
62
+ const apiKey = !restore ? (process.env.QUATARLY_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
+ if (!existsSync(BACKUP_FILE)) {
75
+ fail("No backup found. Nothing to restore.");
76
+ process.exit(1);
77
+ }
78
+
79
+ const backup = JSON.parse(readFileSync(BACKUP_FILE, "utf-8"));
80
+ log("Restoring original environment variables...");
81
+
82
+ if (OS === "win32") {
83
+ for (const key of VAR_KEYS) {
84
+ if (backup.env[key] !== undefined) {
85
+ // Restore to both user and system
86
+ try {
87
+ execSync(`reg add "HKLM\\SYSTEM\\CurrentControlSet\\Control\\Session Manager\\Environment" /v "${key}" /t REG_SZ /d "${backup.env[key]}" /f`, { stdio: "ignore" });
88
+ } catch {}
89
+ execSync(`reg add "HKCU\\Environment" /v "${key}" /t REG_SZ /d "${backup.env[key]}" /f`, { stdio: "ignore" });
90
+ ok(`Restored ${key} = ${backup.env[key] || "(empty)"}`);
91
+ } else {
92
+ // Was not set before — delete it
93
+ try {
94
+ execSync(`reg delete "HKLM\\SYSTEM\\CurrentControlSet\\Control\\Session Manager\\Environment" /v "${key}" /f`, { stdio: "ignore" });
95
+ } catch {}
96
+ try {
97
+ execSync(`reg delete "HKCU\\Environment" /v "${key}" /f`, { stdio: "ignore" });
98
+ } catch {}
99
+ ok(`Removed ${key} (was not set before)`);
100
+ }
101
+ }
102
+ } else {
103
+ // Remove setclaw block from all rc files
104
+ const marker = "# --- Quatarly / Claude Code env ---";
105
+ const endMarker = "# --- end Quatarly ---";
106
+ const rcFiles = ["/etc/environment", "/etc/zshenv", "/etc/bash.bashrc",
107
+ join(HOME, ".bashrc"), join(HOME, ".zshrc")];
108
+ for (const rc of rcFiles) {
109
+ if (!existsSync(rc)) continue;
110
+ const content = readFileSync(rc, "utf-8");
111
+ if (!content.includes(marker)) continue;
112
+ const regex = new RegExp(
113
+ `\\n?${marker.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}[\\s\\S]*?${endMarker.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\n?`, "g"
114
+ );
115
+ writeFileSync(rc, content.replace(regex, ""), "utf-8");
116
+ ok(`Cleaned ${rc}`);
117
+ }
118
+ }
119
+
120
+ // Restore Factory settings
121
+ const factoryPath = join(HOME, ".factory", "settings.json");
122
+ if (backup.factory && existsSync(factoryPath)) {
123
+ log("Restoring Factory settings...");
124
+ writeFileSync(factoryPath, JSON.stringify(backup.factory, null, 2), "utf-8");
125
+ ok("Factory settings restored.");
126
+ }
127
+
128
+ // Clean up backup
129
+ try { rmSync(BACKUP_FILE); } catch {}
130
+
131
+ out("");
132
+ ok("Restore complete! Open a new terminal to apply changes.");
133
+ out("");
134
+ process.exit(0);
135
+ }
55
136
 
56
- let apiKey = process.env.QUATARLY_API_KEY || process.argv[2];
137
+ // ════════════════════════════════════════════════════════════════════
138
+ // SETUP MODE
139
+ // ════════════════════════════════════════════════════════════════════
57
140
 
58
- // If no key and this is first run (just installed), show clear instructions
141
+ // First-run gate
59
142
  if (!apiKey && isFirstRun) {
60
143
  out(" \x1b[33m⚡ Setup required!\x1b[0m Run with your Quatarly API key:");
61
144
  out("");
@@ -66,32 +149,59 @@ if (!apiKey && isFirstRun) {
66
149
  process.exit(0);
67
150
  }
68
151
 
69
- if (!apiKey) {
70
- try {
71
- apiKey = await ask(" Enter your Quatarly API key: ");
72
- } catch (err) {
73
- fail(err.message);
74
- process.exit(1);
152
+ let key = apiKey;
153
+ if (!key) {
154
+ try { key = await ask(" Enter your Quatarly API key: "); }
155
+ catch (e) { fail(e.message); process.exit(1); }
156
+ }
157
+ if (!key) { fail("API key cannot be empty."); process.exit(1); }
158
+
159
+ err("");
160
+
161
+ // ─── Backup current state ─────────────────────────────────────────────
162
+
163
+ mkdirSync(BACKUP_DIR, { recursive: true });
164
+
165
+ const backup = { env: {}, factory: null };
166
+
167
+ if (OS === "win32") {
168
+ for (const k of VAR_KEYS) {
169
+ try {
170
+ const val = execSync(`reg query "HKLM\\SYSTEM\\CurrentControlSet\\Control\\Session Manager\\Environment" /v "${k}" 2>nul`, { encoding: "utf-8" });
171
+ const match = val.match(/REG_SZ\s+(.+)/);
172
+ backup.env[k] = match ? match[1].trim() : "";
173
+ } catch {
174
+ // Also try HKCU
175
+ try {
176
+ const val = execSync(`reg query "HKCU\\Environment" /v "${k}" 2>nul`, { encoding: "utf-8" });
177
+ const match = val.match(/REG_SZ\s+(.+)/);
178
+ backup.env[k] = match ? match[1].trim() : "";
179
+ } catch {
180
+ backup.env[k] = undefined; // Was not set
181
+ }
182
+ }
75
183
  }
76
184
  }
77
185
 
78
- if (!apiKey) {
79
- fail("API key cannot be empty.");
80
- process.exit(1);
186
+ // Backup Factory settings
187
+ const factoryPath = join(HOME, ".factory", "settings.json");
188
+ if (existsSync(factoryPath)) {
189
+ let raw = readFileSync(factoryPath, "utf-8");
190
+ if (raw.charCodeAt(0) === 0xfeff) raw = raw.slice(1);
191
+ backup.factory = JSON.parse(raw.trim() || "{}");
81
192
  }
82
193
 
83
- process.stderr.write("\n");
194
+ writeFileSync(BACKUP_FILE, JSON.stringify(backup, null, 2), "utf-8");
195
+ ok(`Backup saved to ${BACKUP_FILE}`);
84
196
 
85
197
  // ─── Step 1: Add models to Factory ──────────────────────────────────
86
198
 
87
- const settingsPath = join(homedir(), ".factory", "settings.json");
88
-
89
- if (existsSync(settingsPath)) {
199
+ if (existsSync(factoryPath)) {
90
200
  log("Adding models to Factory...");
91
201
 
92
- copyFileSync(settingsPath, `${settingsPath}.backup`);
202
+ copyFileSync(factoryPath, `${factoryPath}.backup`);
93
203
 
94
- let raw = readFileSync(settingsPath, "utf-8");
204
+ let raw = readFileSync(factoryPath, "utf-8");
95
205
  if (raw.charCodeAt(0) === 0xfeff) raw = raw.slice(1);
96
206
  const settings = JSON.parse(raw.trim() || "{}");
97
207
 
@@ -109,120 +219,136 @@ if (existsSync(settingsPath)) {
109
219
  { model: "gpt-5.3-codex", baseUrl: "https://api.quatarly.cloud/v1", provider: "openai" },
110
220
  ];
111
221
 
112
- const existing = settings.customModels || [];
222
+ const existing = settings.customModels || [];
113
223
  const existingNames = new Set(existing.map((m) => m.model));
114
- let maxIndex = existing.reduce((max, m) => Math.max(max, m.index ?? -1), -1);
115
- let nextIndex = maxIndex + 1;
116
-
117
- let added = 0;
118
- let updated = 0;
224
+ let maxIndex = existing.reduce((max, m) => Math.max(max, m.index ?? -1), -1);
225
+ let nextIndex = maxIndex + 1;
226
+ let added = 0, updated = 0;
119
227
 
120
228
  for (const m of newModels) {
121
229
  if (existingNames.has(m.model)) {
122
230
  for (const e of existing) {
123
- if (e.model === m.model) {
124
- e.apiKey = apiKey;
125
- e.baseUrl = m.baseUrl;
126
- e.provider = m.provider;
127
- }
231
+ if (e.model === m.model) { e.apiKey = key; e.baseUrl = m.baseUrl; e.provider = m.provider; }
128
232
  }
129
233
  updated++;
130
234
  } else {
131
- existing.push({
132
- model: m.model,
133
- id: `custom:${m.model}-${nextIndex}`,
134
- index: nextIndex,
135
- baseUrl: m.baseUrl,
136
- apiKey,
137
- displayName: m.model,
138
- noImageSupport: false,
139
- provider: m.provider,
140
- });
141
- nextIndex++;
142
- added++;
235
+ existing.push({ model: m.model, id: `custom:${m.model}-${nextIndex}`, index: nextIndex,
236
+ baseUrl: m.baseUrl, apiKey: key, displayName: m.model, noImageSupport: false, provider: m.provider });
237
+ nextIndex++; added++;
143
238
  }
144
239
  }
145
240
 
146
241
  settings.customModels = existing;
147
- writeFileSync(settingsPath, JSON.stringify(settings, null, 2), "utf-8");
148
-
149
- success(`Factory: ${added} models added, ${updated} updated (${existing.length} total)`);
150
- success(`Backup saved to ${settingsPath}.backup`);
242
+ writeFileSync(factoryPath, JSON.stringify(settings, null, 2), "utf-8");
243
+ ok(`Factory: ${added} added, ${updated} updated (${existing.length} total)`);
151
244
  } else {
152
- warn(`Factory settings not found at ${settingsPath} — skipped.`);
245
+ warn("Factory settings not found — skipped.");
153
246
  }
154
247
 
155
- // ─── Step 2: Set Claude Code env vars ────────────────────────────────
248
+ // ─── Step 2: Set env vars (system-wide, fallback to user) ─────────────
156
249
 
157
- log("Setting Claude Code environment variables...");
250
+ log("Setting environment variables system-wide...");
158
251
 
159
252
  const vars = {
160
253
  ANTHROPIC_BASE_URL: "https://api.quatarly.cloud/",
161
- ANTHROPIC_AUTH_TOKEN: apiKey,
254
+ ANTHROPIC_AUTH_TOKEN: key,
162
255
  ANTHROPIC_DEFAULT_HAIKU_MODEL: "claude-haiku-4-5-20251001",
163
256
  ANTHROPIC_DEFAULT_SONNET_MODEL: "claude-sonnet-4-6-20250929",
164
257
  ANTHROPIC_DEFAULT_OPUS_MODEL: "claude-opus-4-6-thinking",
165
258
  };
166
259
 
167
- const os = platform();
260
+ if (OS === "win32") {
261
+ let usedSystem = false;
168
262
 
169
- if (os === "win32") {
170
- for (const [key, value] of Object.entries(vars)) {
171
- execSync(`reg add "HKCU\\Environment" /v "${key}" /t REG_SZ /d "${value}" /f`, {
172
- stdio: "ignore",
173
- });
263
+ // Try HKLM (system-wide, requires admin)
264
+ try {
265
+ for (const [k, v] of Object.entries(vars)) {
266
+ execSync(
267
+ `reg add "HKLM\\SYSTEM\\CurrentControlSet\\Control\\Session Manager\\Environment" /v "${k}" /t REG_SZ /d "${v}" /f`,
268
+ { stdio: "ignore" }
269
+ );
270
+ }
271
+ usedSystem = true;
272
+ ok("Env vars written system-wide (HKLM — all users).");
273
+ } catch {
274
+ // No admin — fall back to HKCU
275
+ warn("No admin rights — setting for current user only (HKCU).");
276
+ for (const [k, v] of Object.entries(vars)) {
277
+ execSync(`reg add "HKCU\\Environment" /v "${k}" /t REG_SZ /d "${v}" /f`, { stdio: "ignore" });
278
+ }
279
+ ok("Env vars written to HKCU\\Environment.");
174
280
  }
175
- success("Env vars written to Windows registry (HKCU\\Environment).");
281
+
282
+ // Broadcast WM_SETTINGCHANGE so open apps pick up the change without reboot
283
+ try {
284
+ execSync(
285
+ `powershell -NoProfile -Command "[System.Environment]::SetEnvironmentVariable('_setclaw_refresh', $null, 'Machine')" 2>nul`,
286
+ { stdio: "ignore" }
287
+ );
288
+ } catch {}
289
+
176
290
  } else {
177
- const marker = "# --- Quatarly / Claude Code env ---";
291
+ const marker = "# --- Quatarly / Claude Code env ---";
178
292
  const endMarker = "# --- end Quatarly ---";
179
- const block = [
180
- "",
181
- marker,
182
- ...Object.entries(vars).map(([k, v]) => `export ${k}="${v}"`),
183
- endMarker,
184
- "",
185
- ].join("\n");
293
+ const block = ["", marker, ...Object.entries(vars).map(([k, v]) => `export ${k}="${v}"`), endMarker, ""].join("\n");
186
294
 
187
- const rcFiles = [join(homedir(), ".bashrc"), join(homedir(), ".zshrc")];
188
- const written = [];
295
+ // Try system-wide files first
296
+ const systemFiles = OS === "darwin"
297
+ ? ["/etc/zshenv", "/etc/bashrc"]
298
+ : ["/etc/environment", "/etc/bash.bashrc", "/etc/zshenv"];
189
299
 
190
- for (const rc of rcFiles) {
191
- if (!existsSync(rc)) continue;
300
+ const userFiles = [join(HOME, ".bashrc"), join(HOME, ".zshrc")];
301
+ let written = [];
192
302
 
303
+ const writeToRc = (rc) => {
304
+ if (!existsSync(rc)) return false;
193
305
  const content = readFileSync(rc, "utf-8");
194
306
  if (content.includes(marker)) {
195
307
  const regex = new RegExp(
196
- `\\n?${marker.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}[\\s\\S]*?${endMarker.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\n?`,
197
- "g"
308
+ `\\n?${marker.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}[\\s\\S]*?${endMarker.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\n?`, "g"
198
309
  );
199
310
  writeFileSync(rc, content.replace(regex, block), "utf-8");
200
311
  } else {
201
312
  appendFileSync(rc, block, "utf-8");
202
313
  }
203
- written.push(rc);
314
+ return true;
315
+ };
316
+
317
+ // Try system-wide
318
+ let systemOk = false;
319
+ for (const rc of systemFiles) {
320
+ try {
321
+ if (writeToRc(rc)) { written.push(rc); systemOk = true; }
322
+ } catch {}
204
323
  }
205
324
 
206
- if (written.length === 0) {
207
- const fallback = join(homedir(), ".bashrc");
208
- appendFileSync(fallback, block, "utf-8");
209
- written.push(fallback);
325
+ if (systemOk) {
326
+ ok(`Env vars written system-wide: ${written.join(", ")}`);
327
+ } else {
328
+ warn("No write access to system files — writing to user profile.");
329
+ for (const rc of userFiles) {
330
+ if (writeToRc(rc)) written.push(rc);
331
+ }
332
+ if (written.length === 0) {
333
+ appendFileSync(join(HOME, ".bashrc"), block, "utf-8");
334
+ written.push(join(HOME, ".bashrc"));
335
+ }
336
+ ok(`Env vars written to: ${written.join(", ")}`);
210
337
  }
211
-
212
- success(`Env vars written to: ${written.join(", ")}`);
213
338
  }
214
339
 
215
340
  // ─── Done ────────────────────────────────────────────────────────────
216
341
 
217
- // Clear the first-run marker
218
- try { rmSync(markerFile); } catch {}
342
+ try { rmSync(MARKER_FILE); } catch {}
219
343
 
220
344
  out("");
221
345
  out(" \x1b[1mEnvironment variables set:\x1b[0m");
222
- for (const [key, value] of Object.entries(vars)) {
223
- const display = key === "ANTHROPIC_AUTH_TOKEN" ? value.slice(0, 8) + "..." : value;
224
- out(` ${key} = ${display}`);
346
+ for (const [k, v] of Object.entries(vars)) {
347
+ const display = k === "ANTHROPIC_AUTH_TOKEN" ? v.slice(0, 8) + "..." : v;
348
+ out(` ${k} = ${display}`);
225
349
  }
226
350
  out("");
227
- success("All done! Restart your terminal, then launch Claude Code.");
351
+ ok("All done! Open a new terminal, then launch Claude Code.");
352
+ out("");
353
+ out(" \x1b[2mTo restore original settings: setclaw --restore\x1b[0m");
228
354
  out("");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "setclaw",
3
- "version": "1.2.1",
3
+ "version": "1.3.1",
4
4
  "description": "BOA & Quatarly setup for Claude Code and Factory",
5
5
  "type": "module",
6
6
  "bin": {