clawreform 0.3.1 → 0.3.4

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/README.md CHANGED
@@ -8,6 +8,15 @@ Installs the native `clawreform` CLI from GitHub Releases and exposes it on your
8
8
  npm install -g clawreform
9
9
  ```
10
10
 
11
+ ## Launch
12
+
13
+ ```bash
14
+ clawreform
15
+ ```
16
+
17
+ When run with no args in an interactive terminal, this npm launcher opens the web dashboard flow by default.
18
+ On first run, it also performs `clawreform setup --quick` automatically when config is missing.
19
+
11
20
  ## Verify
12
21
 
13
22
  ```bash
@@ -18,3 +27,4 @@ clawreform --version
18
27
 
19
28
  - This package downloads the matching binary for Linux, macOS, and Windows.
20
29
  - No third-party runtime dependencies are used in this npm package.
30
+ - You can disable no-arg auto-dashboard behavior with `CLAWREFORM_NO_AUTO_DASHBOARD=1`.
package/bin/clawreform.js CHANGED
@@ -1,8 +1,239 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- const { spawnSync } = require("node:child_process");
3
+ const fs = require("node:fs");
4
+ const os = require("node:os");
5
+ const path = require("node:path");
6
+ const { spawn, spawnSync } = require("node:child_process");
4
7
  const { ensureBinaryInstalled } = require("../lib/install");
5
8
 
9
+ const API_KEY_VARS = [
10
+ "OPENROUTER_API_KEY",
11
+ "GROQ_API_KEY",
12
+ "ANTHROPIC_API_KEY",
13
+ "OPENAI_API_KEY",
14
+ "GEMINI_API_KEY",
15
+ "GOOGLE_API_KEY",
16
+ "DEEPSEEK_API_KEY",
17
+ "MINIMAX_API_KEY",
18
+ "MISTRAL_API_KEY",
19
+ "TOGETHER_API_KEY",
20
+ "FIREWORKS_API_KEY",
21
+ "COHERE_API_KEY",
22
+ "PERPLEXITY_API_KEY",
23
+ "XAI_API_KEY"
24
+ ];
25
+
26
+ function sleep(ms) {
27
+ return new Promise((resolve) => setTimeout(resolve, ms));
28
+ }
29
+
30
+ function normalizeArgs(argv) {
31
+ const args = argv.slice(2);
32
+
33
+ // UX default for npm installs:
34
+ // In an interactive terminal, no-arg invocation should open the GUI dashboard.
35
+ const noArgs = args.length === 0;
36
+ const interactive = Boolean(process.stdin.isTTY && process.stdout.isTTY);
37
+ const autoDashboardDisabled =
38
+ process.env.CLAWREFORM_NO_AUTO_DASHBOARD === "1" ||
39
+ process.env.CLAWREFORM_NO_AUTO_DASHBOARD === "true";
40
+
41
+ if (noArgs && interactive && !autoDashboardDisabled) {
42
+ process.stderr.write("No command provided. Launching web dashboard.\n");
43
+ return ["dashboard"];
44
+ }
45
+
46
+ // Friendly aliases for non-technical users.
47
+ if (args[0] === "gui" || args[0] === "web") {
48
+ return ["dashboard", ...args.slice(1)];
49
+ }
50
+
51
+ return args;
52
+ }
53
+
54
+ function clawreformHome() {
55
+ return path.join(os.homedir(), ".clawreform");
56
+ }
57
+
58
+ function configPath() {
59
+ return path.join(clawreformHome(), "config.toml");
60
+ }
61
+
62
+ function daemonInfoPath() {
63
+ return path.join(clawreformHome(), "daemon.json");
64
+ }
65
+
66
+ function dotEnvPath() {
67
+ return path.join(clawreformHome(), ".env");
68
+ }
69
+
70
+ function daemonBaseUrlFromFile() {
71
+ try {
72
+ const raw = fs.readFileSync(daemonInfoPath(), "utf8");
73
+ const parsed = JSON.parse(raw);
74
+ if (parsed && typeof parsed.listen_addr === "string" && parsed.listen_addr.trim()) {
75
+ return `http://${parsed.listen_addr.replace("0.0.0.0", "127.0.0.1")}`;
76
+ }
77
+ } catch {
78
+ // ignore
79
+ }
80
+ return `http://127.0.0.1:4332`;
81
+ }
82
+
83
+ function hasConfiguredApiKey() {
84
+ for (const key of API_KEY_VARS) {
85
+ const value = process.env[key];
86
+ if (typeof value === "string" && value.trim()) {
87
+ return true;
88
+ }
89
+ }
90
+
91
+ try {
92
+ const content = fs.readFileSync(dotEnvPath(), "utf8");
93
+ for (const line of content.split(/\r?\n/)) {
94
+ const trimmed = line.trim();
95
+ if (!trimmed || trimmed.startsWith("#")) {
96
+ continue;
97
+ }
98
+ const idx = trimmed.indexOf("=");
99
+ if (idx <= 0) {
100
+ continue;
101
+ }
102
+ const key = trimmed.slice(0, idx).trim();
103
+ const rawValue = trimmed.slice(idx + 1).trim();
104
+ const value = rawValue.replace(/^['"]|['"]$/g, "").trim();
105
+ if (API_KEY_VARS.includes(key) && value) {
106
+ return true;
107
+ }
108
+ }
109
+ } catch {
110
+ // ignore missing/unreadable .env
111
+ }
112
+
113
+ return false;
114
+ }
115
+
116
+ async function daemonHealthy(baseUrl) {
117
+ try {
118
+ const response = await fetch(`${baseUrl}/api/health`, {
119
+ headers: { "User-Agent": "clawreform-npm-launcher" }
120
+ });
121
+ return response.ok;
122
+ } catch {
123
+ return false;
124
+ }
125
+ }
126
+
127
+ async function waitForDaemonReady(timeoutMs) {
128
+ const deadline = Date.now() + timeoutMs;
129
+ while (Date.now() < deadline) {
130
+ const baseUrl = daemonBaseUrlFromFile();
131
+ if (await daemonHealthy(baseUrl)) {
132
+ return baseUrl;
133
+ }
134
+ await sleep(500);
135
+ }
136
+ return null;
137
+ }
138
+
139
+ function runQuickSetup(binaryPath) {
140
+ if (fs.existsSync(configPath())) {
141
+ return true;
142
+ }
143
+
144
+ process.stderr.write("First run detected. Applying quick setup...\n");
145
+ const result = spawnSync(binaryPath, ["setup", "--quick"], {
146
+ stdio: "inherit",
147
+ env: process.env
148
+ });
149
+
150
+ if (result.error) {
151
+ process.stderr.write(`Quick setup failed: ${result.error.message}\n`);
152
+ return false;
153
+ }
154
+ return result.status === 0;
155
+ }
156
+
157
+ function spawnDaemon(binaryPath, injectPlaceholderKeys) {
158
+ const env = { ...process.env };
159
+ if (injectPlaceholderKeys) {
160
+ for (const key of API_KEY_VARS) {
161
+ if (!env[key] || !String(env[key]).trim()) {
162
+ env[key] = "clawreform-placeholder-key";
163
+ }
164
+ }
165
+ }
166
+
167
+ try {
168
+ const child = spawn(binaryPath, ["start"], {
169
+ detached: true,
170
+ stdio: "ignore",
171
+ env,
172
+ windowsHide: true
173
+ });
174
+ child.unref();
175
+ return true;
176
+ } catch {
177
+ return false;
178
+ }
179
+ }
180
+
181
+ function openBrowser(url) {
182
+ try {
183
+ if (process.platform === "darwin") {
184
+ return spawnSync("open", [url], { stdio: "ignore" }).status === 0;
185
+ }
186
+ if (process.platform === "win32") {
187
+ return (
188
+ spawnSync("cmd.exe", ["/c", "start", "", url], {
189
+ stdio: "ignore",
190
+ windowsHide: true
191
+ }).status === 0
192
+ );
193
+ }
194
+ return spawnSync("xdg-open", [url], { stdio: "ignore" }).status === 0;
195
+ } catch {
196
+ return false;
197
+ }
198
+ }
199
+
200
+ async function runDashboardFlow(binaryPath) {
201
+ if (!runQuickSetup(binaryPath)) {
202
+ process.stderr.write("Setup did not complete. Run: clawreform setup --quick\n");
203
+ return 1;
204
+ }
205
+
206
+ let baseUrl = await waitForDaemonReady(2000);
207
+ if (!baseUrl) {
208
+ const injectPlaceholderKeys = !hasConfiguredApiKey();
209
+ if (injectPlaceholderKeys) {
210
+ process.stderr.write(
211
+ "No API key found yet. Starting in setup mode so you can finish setup in the dashboard.\n"
212
+ );
213
+ }
214
+ process.stderr.write("Starting background daemon...\n");
215
+ if (!spawnDaemon(binaryPath, injectPlaceholderKeys)) {
216
+ process.stderr.write("Could not start daemon process.\n");
217
+ process.stderr.write("Run manually: clawreform start\n");
218
+ return 1;
219
+ }
220
+ baseUrl = await waitForDaemonReady(45000);
221
+ }
222
+
223
+ if (!baseUrl) {
224
+ process.stderr.write("Daemon did not become ready in time.\n");
225
+ process.stderr.write("Run manually: clawreform start\n");
226
+ return 1;
227
+ }
228
+
229
+ const url = `${baseUrl}/`;
230
+ process.stderr.write(`Opening dashboard: ${url}\n`);
231
+ if (!openBrowser(url)) {
232
+ process.stderr.write(`Open this URL manually: ${url}\n`);
233
+ }
234
+ return 0;
235
+ }
236
+
6
237
  async function main() {
7
238
  let binaryPath;
8
239
  try {
@@ -13,7 +244,13 @@ async function main() {
13
244
  process.exit(1);
14
245
  }
15
246
 
16
- const result = spawnSync(binaryPath, process.argv.slice(2), {
247
+ const args = normalizeArgs(process.argv);
248
+
249
+ if (args[0] === "dashboard") {
250
+ process.exit(await runDashboardFlow(binaryPath));
251
+ }
252
+
253
+ const result = spawnSync(binaryPath, args, {
17
254
  stdio: "inherit",
18
255
  env: process.env
19
256
  });
package/lib/install.js CHANGED
@@ -52,13 +52,32 @@ async function downloadToFile(url, destination) {
52
52
  });
53
53
 
54
54
  if (!response.ok) {
55
- throw new Error(`Download failed (${response.status}): ${url}`);
55
+ const error = new Error(`Download failed (${response.status}): ${url}`);
56
+ error.status = response.status;
57
+ throw error;
56
58
  }
57
59
 
58
60
  const arrayBuffer = await response.arrayBuffer();
59
61
  fs.writeFileSync(destination, Buffer.from(arrayBuffer));
60
62
  }
61
63
 
64
+ async function fetchLatestReleaseTag() {
65
+ const url = `https://api.github.com/repos/${REPO}/releases/latest`;
66
+ const response = await fetch(url, {
67
+ headers: {
68
+ "User-Agent": "clawreform-npm-installer"
69
+ }
70
+ });
71
+ if (!response.ok) {
72
+ throw new Error(`Failed to fetch latest release tag (${response.status})`);
73
+ }
74
+ const payload = await response.json();
75
+ if (!payload || typeof payload.tag_name !== "string" || !payload.tag_name.trim()) {
76
+ throw new Error("Latest release payload did not include tag_name.");
77
+ }
78
+ return payload.tag_name.trim();
79
+ }
80
+
62
81
  function extractArchive(archivePath, destinationPath, ext) {
63
82
  if (ext === "tar.gz") {
64
83
  const result = spawnSync("tar", ["-xzf", archivePath, "-C", destinationPath], {
@@ -120,9 +139,8 @@ function installedBinaryPath() {
120
139
 
121
140
  async function installBinary(options = {}) {
122
141
  const log = typeof options.log === "function" ? options.log : console.log;
123
- const version = normalizeVersion(options.version || process.env.CLAWREFORM_VERSION || require("../package.json").version);
142
+ const preferredVersion = normalizeVersion(options.version || process.env.CLAWREFORM_VERSION || require("../package.json").version);
124
143
  const { target, ext, binName } = resolveTarget();
125
- const downloadUrl = assetUrl(version, target, ext);
126
144
 
127
145
  const tmpRoot = fs.mkdtempSync(path.join(os.tmpdir(), "clawreform-npm-"));
128
146
  const archivePath = path.join(tmpRoot, `clawreform.${ext}`);
@@ -130,8 +148,44 @@ async function installBinary(options = {}) {
130
148
  fs.mkdirSync(extractPath, { recursive: true });
131
149
 
132
150
  try {
133
- log(`clawreform npm: downloading ${version} (${target})`);
134
- await downloadToFile(downloadUrl, archivePath);
151
+ let downloadedVersion = null;
152
+ const versionsToTry = [preferredVersion];
153
+ try {
154
+ const latestTag = normalizeVersion(await fetchLatestReleaseTag());
155
+ if (!versionsToTry.includes(latestTag)) {
156
+ versionsToTry.push(latestTag);
157
+ }
158
+ } catch (error) {
159
+ const message = error instanceof Error ? error.message : String(error);
160
+ log(`clawreform npm: warning: unable to resolve latest release tag (${message})`);
161
+ }
162
+
163
+ let lastError = null;
164
+ for (const version of versionsToTry) {
165
+ const downloadUrl = assetUrl(version, target, ext);
166
+ try {
167
+ log(`clawreform npm: downloading ${version} (${target})`);
168
+ await downloadToFile(downloadUrl, archivePath);
169
+ downloadedVersion = version;
170
+ break;
171
+ } catch (error) {
172
+ lastError = error;
173
+ const status = error && typeof error === "object" ? error.status : null;
174
+ if (status === 404) {
175
+ continue;
176
+ }
177
+ throw error;
178
+ }
179
+ }
180
+
181
+ if (!downloadedVersion) {
182
+ throw lastError || new Error("Could not download a matching clawreform release asset.");
183
+ }
184
+
185
+ if (downloadedVersion !== preferredVersion) {
186
+ log(`clawreform npm: release ${preferredVersion} was unavailable, using ${downloadedVersion} instead`);
187
+ }
188
+
135
189
  extractArchive(archivePath, extractPath, ext);
136
190
 
137
191
  const sourceBinary = findFileRecursively(extractPath, binName);
package/package.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "clawreform",
3
- "version": "0.3.1",
3
+ "version": "0.3.4",
4
4
  "description": "Cross-platform npm launcher for clawREFORM by aegntic.ai CLI",
5
5
  "license": "MIT",
6
6
  "author": "clawREFORM by aegntic.ai Team",
7
7
  "repository": {
8
8
  "type": "git",
9
- "url": "https://github.com/aegntic/clawreform"
9
+ "url": "git+https://github.com/aegntic/clawreform.git"
10
10
  },
11
11
  "bugs": {
12
12
  "url": "https://github.com/aegntic/clawreform/issues"
@@ -23,7 +23,7 @@
23
23
  "node": ">=18.0.0"
24
24
  },
25
25
  "bin": {
26
- "clawreform": "./bin/clawreform.js"
26
+ "clawreform": "bin/clawreform.js"
27
27
  },
28
28
  "scripts": {
29
29
  "postinstall": "node ./scripts/postinstall.js"