nightpay 0.1.0 → 0.4.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/bin/cli.js CHANGED
@@ -1,12 +1,16 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- import { cpSync, existsSync, mkdirSync } from "node:fs";
4
- import { resolve, dirname, join } from "node:path";
3
+ import { cpSync, copyFileSync, existsSync, mkdirSync, readFileSync, chmodSync, readdirSync, statSync, writeFileSync } from "node:fs";
4
+ import { resolve, dirname, join, basename } from "node:path";
5
5
  import { fileURLToPath } from "node:url";
6
+ import { execSync, spawnSync } from "node:child_process";
6
7
 
7
8
  const __dirname = dirname(fileURLToPath(import.meta.url));
8
- const SKILL_SRC = resolve(__dirname, "..", "skills", "nightpay");
9
- const COMMANDS = ["init", "add", "list", "help"];
9
+ const PKG_ROOT = resolve(__dirname, "..");
10
+ const SKILL_SRC = resolve(PKG_ROOT, "skills", "nightpay");
11
+ const SDK_SRC = resolve(PKG_ROOT, "nightpay_sdk.py");
12
+ const SETUP_SRC = resolve(PKG_ROOT, "scripts", "setup.sh");
13
+ const COMMANDS = ["init", "add", "setup", "validate", "doctor", "list", "help"];
10
14
 
11
15
  const command = process.argv[2] || "help";
12
16
 
@@ -15,42 +19,541 @@ if (!COMMANDS.includes(command)) {
15
19
  process.exit(1);
16
20
  }
17
21
 
22
+ // ─── Version ─────────────────────────────────────────────────────────────────
23
+ let VERSION = "0.3.2";
24
+ try {
25
+ const pkg = JSON.parse(readFileSync(resolve(PKG_ROOT, "package.json"), "utf8"));
26
+ VERSION = pkg.version || VERSION;
27
+ } catch {}
28
+
29
+ // ─── Colors ──────────────────────────────────────────────────────────────────
30
+ const isTTY = process.stderr.isTTY;
31
+ const C = {
32
+ red: isTTY ? "\x1b[31m" : "",
33
+ green: isTTY ? "\x1b[32m" : "",
34
+ yellow: isTTY ? "\x1b[33m" : "",
35
+ cyan: isTTY ? "\x1b[36m" : "",
36
+ bold: isTTY ? "\x1b[1m" : "",
37
+ dim: isTTY ? "\x1b[2m" : "",
38
+ reset: isTTY ? "\x1b[0m" : "",
39
+ };
40
+ const OK = `${C.green}✅${C.reset}`;
41
+ const FAIL = `${C.red}❌${C.reset}`;
42
+ const WARN = `${C.yellow}⚠️${C.reset}`;
43
+ const INFO = `${C.cyan}ℹ${C.reset}`;
44
+
45
+ // ─── Help ────────────────────────────────────────────────────────────────────
18
46
  if (command === "help") {
19
47
  console.log(`
20
- nightpay — anonymous community bounties for AI agents
48
+ ${C.bold}nightpay${C.reset} v${VERSION} — anonymous community bounties for AI agents
49
+
50
+ ${C.bold}COMMANDS${C.reset}
51
+ npx nightpay ${C.cyan}init${C.reset} Install skill files, SDK, and setup script
52
+ npx nightpay ${C.cyan}setup${C.reset} Full onboarding: install + validate + platform config
53
+ npx nightpay ${C.cyan}validate${C.reset} Check env vars, prerequisites, and connectivity
54
+ npx nightpay ${C.cyan}doctor${C.reset} Diagnose and auto-fix common issues
55
+ npx nightpay ${C.cyan}list${C.reset} Show skill info
56
+ npx nightpay ${C.cyan}help${C.reset} This message
57
+
58
+ ${C.bold}QUICK START${C.reset}
59
+ ${C.dim}# Install and validate in one step:${C.reset}
60
+ npx nightpay setup
21
61
 
22
- Commands:
23
- npx nightpay init Copy nightpay skill into ./skills/nightpay
24
- npx nightpay add Same as init
25
- npx nightpay list Show skill info
26
- npx nightpay help This message
62
+ ${C.dim}# Or step by step:${C.reset}
63
+ npx nightpay init
64
+ export MASUMI_API_KEY="your-key"
65
+ export OPERATOR_ADDRESS="your-address"
66
+ export NIGHTPAY_API_URL="https://api.nightpay.dev"
67
+ export BRIDGE_URL="https://bridge.nightpay.dev"
68
+ npx nightpay validate
69
+
70
+ ${C.bold}WHAT INIT INSTALLS${C.reset}
71
+ ./skills/nightpay/ Skill files (SKILL.md, scripts, config)
72
+ ./skills/nightpay/sdk/ Python SDK (nightpay_sdk.py)
73
+ ./skills/nightpay/scripts/ Gateway + setup scripts
27
74
  `);
28
75
  process.exit(0);
29
76
  }
30
77
 
78
+ // ─── List ────────────────────────────────────────────────────────────────────
31
79
  if (command === "list") {
32
80
  console.log(`
33
- Available skill:
81
+ ${C.bold}Available skill:${C.reset}
34
82
  nightpay Anonymous community bounty board (Midnight + Masumi + Cardano)
35
83
  Many funders pool shielded NIGHT → AI agent completes work → ZK receipt
84
+
85
+ ${C.bold}Platforms:${C.reset} OpenClaw, Claude Code, Cursor, GitHub Copilot, ACP, Raw API
86
+ ${C.bold}Version:${C.reset} ${VERSION}
87
+ ${C.bold}License:${C.reset} Apache-2.0
36
88
  `);
37
89
  process.exit(0);
38
90
  }
39
91
 
40
- // init / add
41
- const dest = resolve(process.cwd(), "skills", "nightpay");
92
+ // ─── Detect platform ────────────────────────────────────────────────────────
93
+ function detectPlatform() {
94
+ try { execSync("which openclaw", { stdio: "ignore" }); return "openclaw"; } catch {}
95
+ if (existsSync(".claude") || existsSync(".claude/settings.json")) return "claude-code";
96
+ if (existsSync(".cursor") || existsSync(".cursorrules")) return "cursor";
97
+ if (existsSync(".github/copilot-instructions.md")) return "copilot";
98
+ return "raw";
99
+ }
100
+
101
+ // ─── Copy one file safely ───────────────────────────────────────────────────
102
+ function safeCopy(src, dest, label) {
103
+ if (!existsSync(src)) {
104
+ return { status: "skip", reason: "source not found in package" };
105
+ }
106
+ mkdirSync(dirname(dest), { recursive: true });
107
+ if (existsSync(dest)) {
108
+ // Compare sizes — if same, skip
109
+ try {
110
+ const srcStat = statSync(src);
111
+ const destStat = statSync(dest);
112
+ if (srcStat.size === destStat.size) {
113
+ return { status: "exists", reason: "already up to date" };
114
+ }
115
+ } catch {}
116
+ }
117
+ copyFileSync(src, dest);
118
+ return { status: "copied" };
119
+ }
120
+
121
+ // ─── Init (copy ALL files) ──────────────────────────────────────────────────
122
+ function init() {
123
+ const dest = resolve(process.cwd(), "skills", "nightpay");
124
+ const installed = [];
42
125
 
43
- if (existsSync(dest)) {
44
- console.log(`nightpay skill already exists at ${dest}`);
45
- console.log("To reinstall, remove the directory first.");
46
- process.exit(0);
126
+ console.log(`\n${C.bold}Installing NightPay${C.reset} v${VERSION}\n`);
127
+
128
+ // 1. Core skill files (SKILL.md, scripts/gateway.sh, etc.)
129
+ mkdirSync(resolve(process.cwd(), "skills"), { recursive: true });
130
+ if (existsSync(join(dest, "SKILL.md"))) {
131
+ // Update existing — re-copy to catch any upstream changes
132
+ cpSync(SKILL_SRC, dest, { recursive: true });
133
+ console.log(` ${OK} Skill files updated at ${C.dim}./skills/nightpay/${C.reset}`);
134
+ installed.push("skills/nightpay/ (updated)");
135
+ } else {
136
+ cpSync(SKILL_SRC, dest, { recursive: true });
137
+ console.log(` ${OK} Skill files installed to ${C.dim}./skills/nightpay/${C.reset}`);
138
+ installed.push("skills/nightpay/");
139
+ }
140
+
141
+ // 2. Python SDK → ./skills/nightpay/sdk/nightpay_sdk.py
142
+ const sdkDest = join(dest, "sdk", "nightpay_sdk.py");
143
+ const sdkResult = safeCopy(SDK_SRC, sdkDest, "Python SDK");
144
+ if (sdkResult.status === "copied") {
145
+ console.log(` ${OK} Python SDK installed to ${C.dim}./skills/nightpay/sdk/nightpay_sdk.py${C.reset}`);
146
+ installed.push("sdk/nightpay_sdk.py");
147
+ } else if (sdkResult.status === "exists") {
148
+ console.log(` ${OK} Python SDK ${C.dim}(already up to date)${C.reset}`);
149
+ } else {
150
+ console.log(` ${INFO} Python SDK not bundled in this version ${C.dim}(download from GitHub)${C.reset}`);
151
+ }
152
+
153
+ // Also copy SDK to project root for direct import convenience
154
+ const sdkRootDest = resolve(process.cwd(), "nightpay_sdk.py");
155
+ const sdkRootResult = safeCopy(SDK_SRC, sdkRootDest, "Python SDK (root)");
156
+ if (sdkRootResult.status === "copied") {
157
+ console.log(` ${OK} Python SDK also at ${C.dim}./nightpay_sdk.py${C.reset} ${C.dim}(for direct import)${C.reset}`);
158
+ }
159
+
160
+ // 3. Setup script → ./skills/nightpay/scripts/setup.sh
161
+ const setupDest = join(dest, "scripts", "setup.sh");
162
+ const setupResult = safeCopy(SETUP_SRC, setupDest, "setup.sh");
163
+ if (setupResult.status === "copied") {
164
+ console.log(` ${OK} Setup script installed to ${C.dim}./skills/nightpay/scripts/setup.sh${C.reset}`);
165
+ installed.push("scripts/setup.sh");
166
+ } else if (setupResult.status === "exists") {
167
+ console.log(` ${OK} Setup script ${C.dim}(already up to date)${C.reset}`);
168
+ } else {
169
+ console.log(` ${INFO} Setup script not bundled in this version`);
170
+ }
171
+
172
+ // 4. Fix permissions on ALL scripts
173
+ const scriptsDir = join(dest, "scripts");
174
+ if (existsSync(scriptsDir)) {
175
+ let chmodCount = 0;
176
+ try {
177
+ for (const f of readdirSync(scriptsDir)) {
178
+ if (f.endsWith(".sh")) {
179
+ chmodSync(join(scriptsDir, f), 0o755);
180
+ chmodCount++;
181
+ }
182
+ }
183
+ if (chmodCount > 0) {
184
+ console.log(` ${OK} Made ${chmodCount} script(s) executable`);
185
+ }
186
+ } catch {}
187
+ }
188
+
189
+ // 5. Auto-flatten nested skill directory (common misinstall)
190
+ const nestedSkill = join(dest, "skills", "nightpay", "SKILL.md");
191
+ if (existsSync(nestedSkill)) {
192
+ console.log(` ${WARN} Nested skill directory detected — flattening...`);
193
+ cpSync(join(dest, "skills", "nightpay"), dest, { recursive: true });
194
+ console.log(` ${OK} Flattened: ${C.dim}skills/nightpay/skills/nightpay/ → skills/nightpay/${C.reset}`);
195
+ }
196
+
197
+ // 6. Summary
198
+ console.log(`\n${C.bold}Installed ${installed.length} component(s):${C.reset}`);
199
+ for (const item of installed) {
200
+ console.log(` ${C.dim}•${C.reset} ${item}`);
201
+ }
202
+
203
+ return dest;
204
+ }
205
+
206
+ // ─── Validate ────────────────────────────────────────────────────────────────
207
+ function validate() {
208
+ let errors = 0;
209
+ let warnings = 0;
210
+
211
+ console.log(`\n${C.bold}Prerequisites${C.reset}`);
212
+ for (const bin of ["bash", "curl", "openssl", "sqlite3"]) {
213
+ try {
214
+ execSync(`which ${bin}`, { stdio: "ignore" });
215
+ console.log(` ${OK} ${bin} found`);
216
+ } catch {
217
+ if (bin === "sqlite3") {
218
+ // sqlite3 is only needed for local receipt caching — downgrade to warning
219
+ console.log(` ${WARN} ${bin} not found ${C.dim}(optional — needed for local receipt caching only)${C.reset}`);
220
+ console.log(` ${C.dim}Fix (Debian/Ubuntu): sudo apt-get install sqlite3${C.reset}`);
221
+ console.log(` ${C.dim}Fix (macOS): brew install sqlite3${C.reset}`);
222
+ warnings++;
223
+ } else {
224
+ console.log(` ${FAIL} ${bin} not found`);
225
+ errors++;
226
+ }
227
+ }
228
+ }
229
+
230
+ let hasHash = false;
231
+ try { execSync("which sha256sum", { stdio: "ignore" }); hasHash = true; } catch {}
232
+ try { execSync("which shasum", { stdio: "ignore" }); hasHash = true; } catch {}
233
+ if (hasHash) console.log(` ${OK} sha256sum/shasum found`);
234
+ else { console.log(` ${FAIL} sha256sum/shasum not found`); errors++; }
235
+
236
+ // Python check (for SDK)
237
+ let hasPython = false;
238
+ try { execSync("which python3", { stdio: "ignore" }); hasPython = true; } catch {}
239
+ if (hasPython) console.log(` ${OK} python3 found (SDK available)`);
240
+ else console.log(` ${INFO} python3 not found ${C.dim}(optional — needed for Python SDK)${C.reset}`);
241
+
242
+ console.log(`\n${C.bold}Environment variables${C.reset}`);
243
+ // Apply defaults before validation
244
+ const DEFAULTS = {
245
+ NIGHTPAY_API_URL: "https://api.nightpay.dev",
246
+ MIDNIGHT_NETWORK: "preprod",
247
+ OPERATOR_FEE_BPS: "200",
248
+ };
249
+ for (const [key, def] of Object.entries(DEFAULTS)) {
250
+ if (!process.env[key]) process.env[key] = def;
251
+ }
252
+
253
+ const required = {
254
+ MASUMI_API_KEY: "Masumi payment API key",
255
+ OPERATOR_ADDRESS: "Midnight operator address (64-char hex)",
256
+ NIGHTPAY_API_URL: "MIP-003 API URL",
257
+ BRIDGE_URL: "Midnight bridge URL",
258
+ };
259
+
260
+ for (const [key, desc] of Object.entries(required)) {
261
+ const val = process.env[key];
262
+ if (!val) {
263
+ console.log(` ${FAIL} ${key} not set — ${desc}`);
264
+ console.log(` ${C.dim}Fix: export ${key}="your-value"${C.reset}`);
265
+ errors++;
266
+ } else if (val === key) {
267
+ console.log(` ${FAIL} ${key} is placeholder "${key}" — replace with real value`);
268
+ errors++;
269
+ } else if (key === "OPERATOR_ADDRESS" && (val.length !== 64 || !/^[0-9a-fA-F]+$/.test(val))) {
270
+ console.log(` ${WARN} ${key} doesn't look like 64-char hex (got ${val.length} chars)`);
271
+ warnings++;
272
+ } else {
273
+ const display = key.includes("URL") ? val : `${val.slice(0, 8)}...`;
274
+ console.log(` ${OK} ${key} set (${display})`);
275
+ }
276
+ }
277
+
278
+ console.log(`\n${C.bold}Skill files${C.reset}`);
279
+ const dest = resolve(process.cwd(), "skills", "nightpay");
280
+
281
+ const fileChecks = [
282
+ { path: join(dest, "SKILL.md"), label: "SKILL.md", required: true },
283
+ { path: join(dest, "scripts", "gateway.sh"), label: "gateway.sh", required: true },
284
+ { path: join(dest, "scripts", "setup.sh"), label: "setup.sh", required: false },
285
+ { path: join(dest, "sdk", "nightpay_sdk.py"), label: "Python SDK (sdk/)", required: false },
286
+ ];
287
+
288
+ for (const check of fileChecks) {
289
+ if (existsSync(check.path)) {
290
+ console.log(` ${OK} ${check.label} found`);
291
+ } else if (check.required) {
292
+ console.log(` ${FAIL} ${check.label} not found — run: ${C.cyan}npx nightpay init${C.reset}`);
293
+ errors++;
294
+ } else {
295
+ console.log(` ${WARN} ${check.label} not found — run: ${C.cyan}npx nightpay init${C.reset} to install`);
296
+ warnings++;
297
+ }
298
+ }
299
+
300
+ // Check for root-level SDK copy too
301
+ const rootSdk = resolve(process.cwd(), "nightpay_sdk.py");
302
+ if (existsSync(rootSdk)) {
303
+ console.log(` ${OK} Python SDK also at ./nightpay_sdk.py`);
304
+ }
305
+
306
+ console.log(`\n${C.bold}Connectivity${C.reset}`);
307
+ const apiUrl = process.env.NIGHTPAY_API_URL;
308
+ if (apiUrl && apiUrl !== "NIGHTPAY_API_URL") {
309
+ try {
310
+ execSync(`curl -sf --max-time 10 "${apiUrl}/availability"`, { stdio: "ignore" });
311
+ console.log(` ${OK} API reachable at ${apiUrl}`);
312
+ } catch {
313
+ console.log(` ${WARN} API unreachable at ${apiUrl}`);
314
+ warnings++;
315
+ }
316
+ }
317
+
318
+ const bridgeUrl = process.env.BRIDGE_URL;
319
+ if (bridgeUrl && bridgeUrl !== "BRIDGE_URL") {
320
+ try {
321
+ execSync(`curl -sf --max-time 10 "${bridgeUrl}/health"`, { stdio: "ignore" });
322
+ console.log(` ${OK} Bridge reachable at ${bridgeUrl}`);
323
+ } catch {
324
+ console.log(` ${WARN} Bridge unreachable at ${bridgeUrl}`);
325
+ warnings++;
326
+ }
327
+ }
328
+
329
+ // Summary
330
+ console.log("");
331
+ if (errors === 0 && warnings === 0) {
332
+ console.log(`${C.green}${C.bold}NightPay is ready!${C.reset} Run: bash skills/nightpay/scripts/gateway.sh stats`);
333
+ } else if (errors === 0) {
334
+ console.log(`${C.yellow}${C.bold}Ready with ${warnings} warning(s)${C.reset} — review above`);
335
+ } else {
336
+ console.log(`${C.red}${C.bold}Not ready: ${errors} error(s), ${warnings} warning(s)${C.reset}`);
337
+ console.log(`Fix the issues above and run: npx nightpay validate`);
338
+ }
339
+
340
+ return { errors, warnings };
47
341
  }
48
342
 
49
- mkdirSync(resolve(process.cwd(), "skills"), { recursive: true });
50
- cpSync(SKILL_SRC, dest, { recursive: true });
343
+ // ─── Doctor (diagnose + auto-fix) ────────────────────────────────────────────
344
+ function doctor() {
345
+ console.log(`\n${C.bold}NightPay Doctor${C.reset} v${VERSION} — diagnosing and fixing issues...\n`);
346
+ let fixed = 0;
347
+
348
+ const dest = resolve(process.cwd(), "skills", "nightpay");
349
+
350
+ // Fix 1: Missing skill files → full init
351
+ if (!existsSync(join(dest, "SKILL.md"))) {
352
+ console.log(` ${WARN} Skill not installed — running full init...`);
353
+ init();
354
+ fixed++;
355
+ }
356
+
357
+ // Fix 2: Nested SKILL.md
358
+ const nestedSkill = join(dest, "skills", "nightpay", "SKILL.md");
359
+ if (existsSync(nestedSkill)) {
360
+ console.log(` ${WARN} SKILL.md nested — flattening...`);
361
+ cpSync(join(dest, "skills", "nightpay"), dest, { recursive: true });
362
+ console.log(` ${OK} Fixed: flattened skill directory`);
363
+ fixed++;
364
+ }
365
+
366
+ // Fix 3: Script permissions
367
+ const scriptsDir = join(dest, "scripts");
368
+ if (existsSync(scriptsDir)) {
369
+ for (const f of readdirSync(scriptsDir)) {
370
+ if (f.endsWith(".sh")) {
371
+ try {
372
+ chmodSync(join(scriptsDir, f), 0o755);
373
+ fixed++;
374
+ } catch {}
375
+ }
376
+ }
377
+ console.log(` ${OK} Fixed: script permissions`);
378
+ }
379
+
380
+ // Fix 4: Missing SDK
381
+ const sdkDest = join(dest, "sdk", "nightpay_sdk.py");
382
+ if (!existsSync(sdkDest) && existsSync(SDK_SRC)) {
383
+ mkdirSync(join(dest, "sdk"), { recursive: true });
384
+ copyFileSync(SDK_SRC, sdkDest);
385
+ console.log(` ${OK} Fixed: installed Python SDK to ${C.dim}sdk/nightpay_sdk.py${C.reset}`);
386
+ fixed++;
387
+ }
388
+
389
+ // Fix 5: Missing setup.sh
390
+ const setupDest = join(dest, "scripts", "setup.sh");
391
+ if (!existsSync(setupDest) && existsSync(SETUP_SRC)) {
392
+ mkdirSync(join(dest, "scripts"), { recursive: true });
393
+ copyFileSync(SETUP_SRC, setupDest);
394
+ chmodSync(setupDest, 0o755);
395
+ console.log(` ${OK} Fixed: installed setup.sh to ${C.dim}scripts/setup.sh${C.reset}`);
396
+ fixed++;
397
+ }
51
398
 
52
- console.log(`Installed nightpay skill to ${dest}`);
53
- console.log(`\nNext steps:`);
54
- console.log(` 1. Set environment variables: MASUMI_API_KEY, OPERATOR_ADDRESS`);
55
- console.log(` 2. Deploy receipt.compact to Midnight testnet`);
56
- console.log(` 3. Tell your agent: "post a bounty"`);
399
+ // Fix 6: Root SDK convenience copy
400
+ const rootSdk = resolve(process.cwd(), "nightpay_sdk.py");
401
+ if (!existsSync(rootSdk) && existsSync(SDK_SRC)) {
402
+ copyFileSync(SDK_SRC, rootSdk);
403
+ console.log(` ${OK} Fixed: copied SDK to ${C.dim}./nightpay_sdk.py${C.reset} for direct import`);
404
+ fixed++;
405
+ }
406
+
407
+ // Fix 7: Warn about placeholder env vars
408
+ const fragment = join(dest, "openclaw-fragment.json");
409
+ if (existsSync(fragment)) {
410
+ try {
411
+ const content = readFileSync(fragment, "utf8");
412
+ const data = JSON.parse(content);
413
+ const env = data?.skills?.entries?.nightpay?.env || {};
414
+ const placeholders = Object.entries(env).filter(([k, v]) => k === v);
415
+ if (placeholders.length > 0) {
416
+ console.log(`\n ${WARN} openclaw-fragment.json has ${placeholders.length} placeholder value(s):`);
417
+ for (const [k] of placeholders) {
418
+ console.log(` ${C.dim}${k}: "${k}" → replace with real value${C.reset}`);
419
+ }
420
+ }
421
+ } catch {}
422
+ }
423
+
424
+ console.log(`\n Applied ${fixed} fix(es). Running validation...\n`);
425
+ validate();
426
+ }
427
+
428
+ // ─── Setup (full onboarding) ─────────────────────────────────────────────────
429
+ function setup() {
430
+ const platform = detectPlatform();
431
+ console.log(`\n${C.bold}NightPay Agent Onboarding${C.reset} v${VERSION}`);
432
+ console.log(`${C.dim}Anonymous community bounties for AI agents${C.reset}`);
433
+ console.log(`\n Platform: ${C.bold}${platform}${C.reset}\n`);
434
+
435
+ // Step 1: Smart install (all files)
436
+ const dest = init();
437
+
438
+ // Step 2: Platform-specific config
439
+ console.log(`\n${C.bold}Platform config (${platform})${C.reset}`);
440
+
441
+ if (platform === "claude-code") {
442
+ const cmdDir = resolve(process.cwd(), ".claude", "commands");
443
+ const cmdFile = join(cmdDir, "nightpay.md");
444
+ if (!existsSync(cmdFile)) {
445
+ mkdirSync(cmdDir, { recursive: true });
446
+ writeFileSync(cmdFile, [
447
+ "# NightPay",
448
+ "",
449
+ "Use the nightpay skill at ./skills/nightpay/ for bounty operations.",
450
+ "",
451
+ "## Quick commands",
452
+ "- `bash skills/nightpay/scripts/gateway.sh stats` — contract stats",
453
+ "- `bash skills/nightpay/scripts/gateway.sh post-bounty \"<desc>\" <amount>` — post bounty",
454
+ "- `python3 skills/nightpay/sdk/nightpay_sdk.py validate` — health check",
455
+ "- `python3 skills/nightpay/sdk/nightpay_sdk.py doctor --auto-fix` — self-heal",
456
+ "",
457
+ ].join("\n"));
458
+ console.log(` ${OK} Created ${C.dim}.claude/commands/nightpay.md${C.reset}`);
459
+ } else {
460
+ console.log(` ${OK} ${C.dim}.claude/commands/nightpay.md${C.reset} already exists`);
461
+ }
462
+ } else if (platform === "cursor") {
463
+ const rulesDir = resolve(process.cwd(), ".cursor", "rules");
464
+ const rulesFile = join(rulesDir, "nightpay.md");
465
+ if (!existsSync(rulesFile)) {
466
+ mkdirSync(rulesDir, { recursive: true });
467
+ writeFileSync(rulesFile, [
468
+ "# NightPay Skill",
469
+ "",
470
+ "The nightpay skill is at ./skills/nightpay/. Read SKILL.md for capabilities.",
471
+ "Python SDK at ./skills/nightpay/sdk/nightpay_sdk.py or ./nightpay_sdk.py.",
472
+ "",
473
+ "Quick: `bash skills/nightpay/scripts/gateway.sh stats`",
474
+ "",
475
+ ].join("\n"));
476
+ console.log(` ${OK} Created ${C.dim}.cursor/rules/nightpay.md${C.reset}`);
477
+ } else {
478
+ console.log(` ${OK} ${C.dim}.cursor/rules/nightpay.md${C.reset} already exists`);
479
+ }
480
+ } else if (platform === "copilot") {
481
+ const instrFile = resolve(process.cwd(), ".github", "copilot-instructions.md");
482
+ if (existsSync(instrFile)) {
483
+ const content = readFileSync(instrFile, "utf8");
484
+ if (!content.includes("nightpay")) {
485
+ const addition = [
486
+ "",
487
+ "## NightPay",
488
+ "",
489
+ "Bounty skill at ./skills/nightpay/. Read SKILL.md for full capabilities.",
490
+ "Python SDK at ./skills/nightpay/sdk/nightpay_sdk.py.",
491
+ "Gateway: `bash skills/nightpay/scripts/gateway.sh`",
492
+ "",
493
+ ].join("\n");
494
+ writeFileSync(instrFile, content + addition);
495
+ console.log(` ${OK} Appended NightPay section to ${C.dim}.github/copilot-instructions.md${C.reset}`);
496
+ } else {
497
+ console.log(` ${OK} Copilot instructions already mention nightpay`);
498
+ }
499
+ } else {
500
+ console.log(` ${INFO} No .github/copilot-instructions.md — skipping Copilot config`);
501
+ }
502
+ } else if (platform === "openclaw") {
503
+ // Check if already installed as plugin
504
+ let pluginInstalled = false;
505
+ try {
506
+ const result = spawnSync("openclaw", ["plugins", "list", "--json"], { encoding: "utf8", stdio: "pipe" });
507
+ if (result.stdout && result.stdout.includes('"nightpay"')) pluginInstalled = true;
508
+ } catch {}
509
+
510
+ if (pluginInstalled) {
511
+ console.log(` ${OK} NightPay plugin already installed in OpenClaw`);
512
+ console.log(` ${C.dim} Run: openclaw plugins enable nightpay${C.reset}`);
513
+ } else {
514
+ console.log(` ${INFO} Installing NightPay as OpenClaw plugin...`);
515
+ const installResult = spawnSync("openclaw", ["plugins", "install", "nightpay"], { encoding: "utf8", stdio: "inherit" });
516
+ if (installResult.status === 0) {
517
+ console.log(` ${OK} Installed! Run: ${C.cyan}openclaw plugins enable nightpay${C.reset}`);
518
+ } else {
519
+ console.log(` ${WARN} Plugin install failed — falling back to skill files`);
520
+ console.log(` ${C.dim} Skill files are at ./skills/nightpay/ (already installed above)${C.reset}`);
521
+ console.log(` ${C.dim} Merge ./skills/nightpay/openclaw-fragment.json into openclaw.json${C.reset}`);
522
+ }
523
+ }
524
+ console.log(` ${C.dim} Set env: openclaw config set skills.entries.nightpay.env.MASUMI_API_KEY "your-key"${C.reset}`);
525
+ } else {
526
+ console.log(` ${INFO} Raw platform — no config file needed`);
527
+ console.log(` ${C.dim} Use: bash skills/nightpay/scripts/gateway.sh <command>${C.reset}`);
528
+ }
529
+
530
+ // Step 3: Run validate
531
+ console.log("");
532
+ const { errors } = validate();
533
+
534
+ // Step 4: Next steps
535
+ console.log(`\n${C.bold}Next steps${C.reset}`);
536
+ if (errors > 0) {
537
+ console.log(` 1. Fix the ${errors} error(s) above`);
538
+ console.log(` 2. Run: ${C.cyan}npx nightpay validate${C.reset}`);
539
+ } else {
540
+ console.log(` 1. ${C.cyan}bash skills/nightpay/scripts/gateway.sh stats${C.reset} — check contract`);
541
+ console.log(` 2. ${C.cyan}bash skills/nightpay/scripts/gateway.sh post-bounty "Review this PR" 5000${C.reset}`);
542
+ }
543
+ console.log(`\n ${C.dim}Python SDK:${C.reset} from nightpay_sdk import NightPay; NightPay().stats()`);
544
+ console.log(` ${C.dim}Self-heal:${C.reset} npx nightpay doctor`);
545
+ console.log("");
546
+ }
547
+
548
+ // ─── Route command ───────────────────────────────────────────────────────────
549
+ if (command === "init" || command === "add") {
550
+ init();
551
+ console.log(`\nNext: run ${C.cyan}npx nightpay validate${C.reset} to check your setup`);
552
+ } else if (command === "setup") {
553
+ setup();
554
+ } else if (command === "validate") {
555
+ const { errors } = validate();
556
+ process.exit(errors > 0 ? 1 : 0);
557
+ } else if (command === "doctor") {
558
+ doctor();
559
+ }