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 +10 -0
- package/bin/clawreform.js +239 -2
- package/lib/install.js +59 -5
- package/package.json +3 -3
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
134
|
-
|
|
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.
|
|
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": "
|
|
26
|
+
"clawreform": "bin/clawreform.js"
|
|
27
27
|
},
|
|
28
28
|
"scripts": {
|
|
29
29
|
"postinstall": "node ./scripts/postinstall.js"
|