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/plugin.js ADDED
@@ -0,0 +1,712 @@
1
+ #!/usr/bin/env node
2
+ // NightPay OpenClaw plugin entrypoint -- v0.3.11
3
+ // Fix: bridge health probe on gateway_start, RECEIPT_CONTRACT_ADDRESS validation,
4
+ // wallet connectivity status in ready log
5
+
6
+ import { fileURLToPath } from "node:url";
7
+ import { dirname, join } from "node:path";
8
+ import { existsSync, mkdirSync, cpSync, rmSync } from "node:fs";
9
+ import { spawnSync } from "node:child_process";
10
+ import { createHash } from "node:crypto";
11
+
12
+ const __dirname = dirname(fileURLToPath(import.meta.url));
13
+ const SKILL_SRC = join(__dirname, "skills", "nightpay");
14
+
15
+ const REQUIRED_ENV = ["MASUMI_API_KEY", "OPERATOR_ADDRESS", "BRIDGE_URL"];
16
+ const WALLET_ENV = ["RECEIPT_CONTRACT_ADDRESS", "OPERATOR_SECRET_KEY"];
17
+
18
+ const DEFAULTS = {
19
+ NIGHTPAY_API_URL: "https://api.nightpay.dev",
20
+ MIDNIGHT_NETWORK: "preprod",
21
+ OPERATOR_FEE_BPS: "200",
22
+ };
23
+
24
+ const STRONG_TRIGGERS = [
25
+ "nightpay", "bounty pool", "bounty board", "create a pool", "create a bounty",
26
+ "fund this anonymously", "anonymous bounty", "anonymous pool", "crowdfund",
27
+ "masumi", "midnight zk", "cardano bounty", "hire an agent", "post a bounty",
28
+ "claim refund", "zk receipt", "verify receipt", "fund the pool",
29
+ ];
30
+ const WEAK_TRIGGERS = [
31
+ "bounty", "anonymous fund", "fund the ", "pool ", "pool,", "pool.",
32
+ "funder", "operator fee", "on-chain settlement",
33
+ ];
34
+
35
+ const BRIEF_CONTEXT = [
36
+ "## NightPay available",
37
+ "Anonymous community bounty pools on Cardano.",
38
+ 'Activate: "create a bounty pool for X", "show bounty board", "fund this anonymously".',
39
+ "Skill docs auto-loaded: skills/nightpay/SKILL.md / AGENTS.md / ontology/ontology.md",
40
+ ].join("\n");
41
+
42
+ const FULL_CONTEXT = [
43
+ "## NightPay Skill Active",
44
+ "",
45
+ "Anonymous community bounty pools -- Midnight ZK proofs / Masumi agent hiring / Cardano settlement.",
46
+ "",
47
+ "### Operating model",
48
+ "You are acting as a NightPay operator agent:",
49
+ "1. Help users CREATE bounty pools (description, goal, deadline, max funders)",
50
+ "2. Help funders CONTRIBUTE anonymously via ZK nullifier (NEVER store or log the nullifier)",
51
+ "3. HIRE agents via Masumi MIP-003 when pool activates",
52
+ "4. VERIFY work and release funds on completion",
53
+ "5. HANDLE disputes impartially if work is rejected",
54
+ "",
55
+ "### Pool lifecycle",
56
+ "funding -> activated (goal met) -> completed (ZK receipt minted)",
57
+ " `-> expired (deadline passed, goal unmet) -> claimRefund (100% returned)",
58
+ "",
59
+ "### Job lifecycle",
60
+ "running -> awaiting_approval -> completed | disputed | refunded",
61
+ "",
62
+ "### Key tools (bridge or MIP-003 API)",
63
+ "- create_pool(description, contributionAmountSpecks, fundingGoalSpecks, maxFunders)",
64
+ "- fund_pool(poolCommitment, funderNullifier)",
65
+ "- submit_work(jobId, workOutput, bountyCommitment, outputHash)",
66
+ "- verify_receipt(receiptHash)",
67
+ "- get_ontology() -> GET /ontology -- call before complex ops",
68
+ "",
69
+ "### Pre-flight (ALWAYS before funding or accepting work)",
70
+ "1. GET /availability -- operator online?",
71
+ "2. GET BRIDGE_URL/health -> contractAddress + stub status",
72
+ "3. verify-receipt <any_hash> -> ZK system live?",
73
+ "",
74
+ "### CRITICAL privacy rule",
75
+ "NEVER log, store, or expose funderNullifier or nonce.",
76
+ "Use memoryId pattern for encrypted credential storage.",
77
+ "",
78
+ "### Amounts: always in specks. 1 NIGHT = 1,000,000 specks.",
79
+ "Full docs at skills/nightpay/ (copied into your agent workspace by this plugin).",
80
+ ].join("\n");
81
+
82
+ const OPERATING_MODEL = [
83
+ "NightPay Operating Model -- v0.3.11",
84
+ "=".repeat(50),
85
+ "",
86
+ "POOL CREATION",
87
+ ' User: "create a bounty pool for reviewing this PR"',
88
+ " Agent: calls create_pool() -> returns poolId + poolCommitment",
89
+ " Agent: shares poolCommitment with potential funders",
90
+ "",
91
+ "FUNDING (anonymous)",
92
+ " Funder generates nullifier (ZK private key -- NEVER expose this)",
93
+ " Funder calls fund_pool(poolCommitment, nullifier)",
94
+ " When goal met -> pool auto-activates",
95
+ "",
96
+ "HIRING AN AGENT",
97
+ " Agent calls Masumi MIP-003 to post job from activated pool",
98
+ " Masumi routes to available agents on the network",
99
+ "",
100
+ "WORK COMPLETION",
101
+ " Worker: submit_work(jobId, output, commitment, hash)",
102
+ " Operator: reviews -> approve releases funds + mints ZK receipt",
103
+ " Funder: verify receipt proves their anonymous contribution was honored",
104
+ "",
105
+ "REFUND PATH",
106
+ " Pool expires (deadline + goal unmet) -> claimRefund() returns 100% to funders",
107
+ " Work disputed -> dispute resolution flow (see skills/nightpay/AGENTS.md)",
108
+ "",
109
+ "WALLET CONNECTIVITY",
110
+ " Masumi: NIGHTPAY_API_URL (MIP-003, /availability /start_job /status)",
111
+ " Midnight: OPERATOR_ADDRESS (shielded 64-char hex, set at initialize())",
112
+ " Midnight: BRIDGE_URL/health (ZK contract, auto-discovered contractAddress)",
113
+ " Optional: midnight-wallet-cli / midnight-wallet-mcp for agent wallet ops",
114
+ " Keys: MASUMI_API_KEY + OPERATOR_SECRET_KEY + RECEIPT_CONTRACT_ADDRESS",
115
+ "",
116
+ "ACTIVATION PHRASES",
117
+ ' "create a bounty pool for X" "show bounty board" "fund this anonymously"',
118
+ ' "hire an agent to do X" "post a bounty for X" "claim refund on pool X"',
119
+ "",
120
+ "COMMANDS",
121
+ " /nightpay status -- config + bridge + connectivity check",
122
+ " /nightpay wallet -- optional midnight-wallet-cli status",
123
+ " /nightpay wallet help -- install + MCP wiring hints",
124
+ " /nightpay help -- this message",
125
+ " /nightpay <task> -- delegate task to nightpay skill",
126
+ "",
127
+ "DOCS (refreshed on every gateway_start)",
128
+ " skills/nightpay/SKILL.md -- full tool reference, trust model",
129
+ " skills/nightpay/AGENTS.md -- roles, decision trees, guardrails",
130
+ " skills/nightpay/ontology/ontology.md -- concepts, lifecycle states, examples",
131
+ ].join("\n");
132
+
133
+ const WALLET_CLI_HELP = [
134
+ "NightPay wallet helper (optional)",
135
+ "",
136
+ "Install:",
137
+ " npm install -g midnight-wallet-cli",
138
+ " npm install -g openshart",
139
+ "",
140
+ "Quick checks:",
141
+ " midnight --version",
142
+ " midnight info --json",
143
+ " midnight balance --json",
144
+ "",
145
+ "Provision encrypted wallet seed (no seed/mnemonic in chat output):",
146
+ " /nightpay wallet provision",
147
+ " /nightpay wallet provision preprod",
148
+ "",
149
+ "MCP server command (for agent runtimes):",
150
+ " midnight-wallet-mcp",
151
+ "",
152
+ "Notes:",
153
+ " - Provision uses OpenShart and stores secrets in compartment NIGHTPAY_FUNDING.",
154
+ " - This CLI manages wallet files for transfers and localnet workflows.",
155
+ " - NightPay OPERATOR_ADDRESS is still the bridge-side shielded 64-char hex value.",
156
+ ].join("\n");
157
+
158
+ function resolveEnv(config) {
159
+ return config?.skills?.entries?.nightpay?.env ?? {};
160
+ }
161
+
162
+ function missingEnv(env) {
163
+ return REQUIRED_ENV.filter((k) => !env[k] || env[k] === k || env[k] === "");
164
+ }
165
+
166
+ function missingWalletEnv(env) {
167
+ return WALLET_ENV.filter((k) => !env[k] || env[k] === k || env[k] === "");
168
+ }
169
+
170
+ function isPlaceholderAddress(addr) {
171
+ if (!addr) return true;
172
+ // Detect obvious placeholders like aabbcc... repeated patterns
173
+ const cleaned = addr.replace(/[^a-f0-9]/gi, "").toLowerCase();
174
+ if (cleaned.length < 32) return true;
175
+ // Check if it's all one repeated pattern (aaaa... or aabb... cycles)
176
+ const unique = new Set(cleaned.split("")).size;
177
+ return unique <= 4; // real hex addresses have more entropy
178
+ }
179
+
180
+ function detectIntent(prompt, messages) {
181
+ const text = (prompt || "").toLowerCase();
182
+ if (STRONG_TRIGGERS.some((t) => text.includes(t))) return "full";
183
+ if (WEAK_TRIGGERS.some((t) => text.includes(t))) return "brief";
184
+ if (Array.isArray(messages) && messages.length > 0) {
185
+ const recent = messages.slice(-6).map((m) => {
186
+ if (typeof m === "string") return m.toLowerCase();
187
+ if (typeof m?.content === "string") return m.content.toLowerCase();
188
+ if (Array.isArray(m?.content))
189
+ return m.content.map((c) => c?.text ?? "").join(" ").toLowerCase();
190
+ return "";
191
+ }).join(" ");
192
+ if (STRONG_TRIGGERS.some((t) => recent.includes(t))) return "brief";
193
+ if (WEAK_TRIGGERS.some((t) => recent.includes(t))) return "brief";
194
+ }
195
+ return "none";
196
+ }
197
+
198
+ function runCommand(command, args, timeoutMs = 7000) {
199
+ const result = spawnSync(command, args, {
200
+ encoding: "utf8",
201
+ timeout: timeoutMs,
202
+ stdio: ["ignore", "pipe", "pipe"],
203
+ });
204
+
205
+ const stdout = String(result.stdout ?? "").trim();
206
+ const stderr = String(result.stderr ?? "").trim();
207
+ let parsed = null;
208
+ if (stdout) {
209
+ try {
210
+ parsed = JSON.parse(stdout);
211
+ } catch {
212
+ parsed = null;
213
+ }
214
+ }
215
+
216
+ return {
217
+ ok: result.status === 0,
218
+ status: result.status ?? 1,
219
+ stdout,
220
+ stderr,
221
+ parsed,
222
+ errorCode: result.error?.code ?? "",
223
+ errorMessage: result.error?.message ?? "",
224
+ };
225
+ }
226
+
227
+ function parseCommandSpec(raw, fallback = "") {
228
+ const value = String(raw || fallback || "").trim();
229
+ if (!value) return null;
230
+ const parts = value.split(/\s+/).filter(Boolean);
231
+ if (parts.length === 0) return null;
232
+ return {
233
+ command: parts[0],
234
+ args: parts.slice(1),
235
+ label: value,
236
+ };
237
+ }
238
+
239
+ function runCommandSpec(spec, args, timeoutMs = 7000) {
240
+ if (!spec) {
241
+ return { ok: false, status: 1, stdout: "", stderr: "", parsed: null, errorCode: "ENOENT", errorMessage: "command spec missing" };
242
+ }
243
+ return runCommand(spec.command, [...spec.args, ...args], timeoutMs);
244
+ }
245
+
246
+ function detectOpenShart(env, options = {}) {
247
+ const allowNpx = options.allowNpx === true;
248
+ const configured = parseCommandSpec(env.OPENSHART_BIN || process.env.OPENSHART_BIN || "");
249
+ const defaults = [parseCommandSpec("openshart")];
250
+ if (allowNpx) defaults.push(parseCommandSpec("npx openshart"));
251
+ const normalizedDefaults = defaults.filter(Boolean);
252
+ const candidates = [];
253
+ if (configured) candidates.push(configured);
254
+ candidates.push(...normalizedDefaults);
255
+
256
+ const seen = new Set();
257
+ for (const candidate of candidates) {
258
+ if (!candidate) continue;
259
+ if (seen.has(candidate.label)) continue;
260
+ seen.add(candidate.label);
261
+ const result = runCommandSpec(candidate, ["--version"], 5000);
262
+ if (result.ok) {
263
+ return {
264
+ available: true,
265
+ spec: candidate,
266
+ version: result.stdout || "",
267
+ };
268
+ }
269
+ }
270
+
271
+ return {
272
+ available: false,
273
+ spec: configured || normalizedDefaults[0] || parseCommandSpec("openshart"),
274
+ version: "",
275
+ };
276
+ }
277
+
278
+ function storeWalletSecret(shartSpec, payload, tags) {
279
+ const content = JSON.stringify(payload);
280
+ const result = runCommandSpec(
281
+ shartSpec,
282
+ [
283
+ "store",
284
+ "--content",
285
+ content,
286
+ "--classification",
287
+ "CONFIDENTIAL",
288
+ "--tags",
289
+ tags,
290
+ "--compartments",
291
+ "NIGHTPAY_FUNDING",
292
+ ],
293
+ 12000,
294
+ );
295
+
296
+ if (!result.ok) return { ok: false, id: "" };
297
+ const id = String(result.parsed?.id || "").trim();
298
+ if (!id) return { ok: false, id: "" };
299
+ return { ok: true, id };
300
+ }
301
+
302
+ function normalizeWalletNetwork(raw, fallback) {
303
+ const value = String(raw || fallback || "").trim().toLowerCase();
304
+ if (!value) return "preprod";
305
+ if (value === "kukolu") return "mainnet";
306
+ return value;
307
+ }
308
+
309
+ function provisionWalletEncrypted(env, requestedNetwork = "") {
310
+ const walletProbe = probeWalletCli(env);
311
+ if (!walletProbe.available) {
312
+ return {
313
+ ok: false,
314
+ text:
315
+ "Wallet provisioning requires midnight-wallet-cli.\n" +
316
+ "Install: npm install -g midnight-wallet-cli\n" +
317
+ "Then run: /nightpay wallet provision",
318
+ };
319
+ }
320
+
321
+ const shartProbe = detectOpenShart(env, { allowNpx: true });
322
+ if (!shartProbe.available) {
323
+ return {
324
+ ok: false,
325
+ text:
326
+ "Encrypted seed storage requires OpenShart.\n" +
327
+ "Install: npm install -g openshart\n" +
328
+ "Optional override: set OPENSHART_BIN in skills.entries.nightpay.env\n" +
329
+ "No wallet was provisioned because encrypted storage is mandatory for this flow.",
330
+ };
331
+ }
332
+
333
+ const network = normalizeWalletNetwork(requestedNetwork, env.MIDNIGHT_NETWORK || DEFAULTS.MIDNIGHT_NETWORK);
334
+ const generateResult = runCommand(walletProbe.command, ["generate", "--network", network, "--json"], 20000);
335
+ if (!generateResult.ok || !generateResult.parsed || generateResult.parsed.error) {
336
+ const msg = String(generateResult.parsed?.message || generateResult.stderr || generateResult.errorMessage || "wallet generate failed").trim();
337
+ return {
338
+ ok: false,
339
+ text:
340
+ "Wallet provisioning failed before secret storage.\n" +
341
+ `Reason: ${msg}`,
342
+ };
343
+ }
344
+
345
+ const wallet = generateResult.parsed;
346
+ const seed = String(wallet.seed || "").trim();
347
+ const mnemonic = String(wallet.mnemonic || "").trim();
348
+ const address = String(wallet.address || "").trim();
349
+ const walletFile = String(wallet.file || "").trim();
350
+ const walletNetwork = String(wallet.network || network).trim();
351
+
352
+ if (!seed || !mnemonic) {
353
+ if (walletFile && existsSync(walletFile)) {
354
+ try { rmSync(walletFile, { force: true }); } catch {}
355
+ }
356
+ return {
357
+ ok: false,
358
+ text:
359
+ "Wallet CLI did not return seed/mnemonic in JSON mode.\n" +
360
+ "Aborted to avoid an unsafe provisioning state.",
361
+ };
362
+ }
363
+
364
+ const seedFingerprint = createHash("sha256").update(seed).digest("hex").slice(0, 16);
365
+ const secretPayload = {
366
+ kind: "midnight_wallet_seed_v1",
367
+ seed,
368
+ mnemonic,
369
+ address,
370
+ network: walletNetwork,
371
+ walletFile,
372
+ createdAt: wallet.createdAt || new Date().toISOString(),
373
+ source: "nightpay-openclaw-plugin",
374
+ seedFingerprint,
375
+ };
376
+ const tags = `nightpay,wallet,midnight,${walletNetwork}`;
377
+ const stored = storeWalletSecret(shartProbe.spec, secretPayload, tags);
378
+ if (!stored.ok) {
379
+ if (walletFile && existsSync(walletFile)) {
380
+ try { rmSync(walletFile, { force: true }); } catch {}
381
+ }
382
+ return {
383
+ ok: false,
384
+ text:
385
+ "OpenShart storage failed. Provisioning rolled back and wallet file was removed to avoid plaintext seed retention.\n" +
386
+ "Check OpenShart availability and retry.",
387
+ };
388
+ }
389
+
390
+ return {
391
+ ok: true,
392
+ text:
393
+ "Wallet provisioned with encrypted seed storage.\n" +
394
+ ` Address: ${address || "unknown"}\n` +
395
+ ` Network: ${walletNetwork}\n` +
396
+ ` Wallet file: ${walletFile || "default path"}\n` +
397
+ ` Seed fingerprint: ${seedFingerprint}\n` +
398
+ ` OpenShart ID: ${stored.id}\n` +
399
+ " Secret output: suppressed (seed/mnemonic not printed)\n" +
400
+ "Use the OpenShart memory ID for controlled recovery in operator workflows.",
401
+ };
402
+ }
403
+
404
+ function probeWalletCli(env) {
405
+ const command = (env.MIDNIGHT_WALLET_CLI_BIN || process.env.MIDNIGHT_WALLET_CLI_BIN || "midnight").trim();
406
+ const versionResult = runCommand(command || "midnight", ["--version"], 4000);
407
+ if (!versionResult.ok) {
408
+ return {
409
+ command: command || "midnight",
410
+ available: false,
411
+ version: "",
412
+ walletReady: false,
413
+ summary: versionResult.errorCode === "ENOENT" ? "not installed" : "not reachable",
414
+ };
415
+ }
416
+
417
+ const infoResult = runCommand(command || "midnight", ["info", "--json"], 8000);
418
+ if (infoResult.ok && infoResult.parsed && !infoResult.parsed.error) {
419
+ const wallet = infoResult.parsed;
420
+ return {
421
+ command: command || "midnight",
422
+ available: true,
423
+ version: versionResult.stdout,
424
+ walletReady: true,
425
+ walletAddress: wallet.address || "",
426
+ walletNetwork: wallet.network || "",
427
+ walletFile: wallet.file || "",
428
+ summary: "wallet loaded",
429
+ };
430
+ }
431
+
432
+ if (infoResult.parsed?.code === "WALLET_NOT_FOUND") {
433
+ return {
434
+ command: command || "midnight",
435
+ available: true,
436
+ version: versionResult.stdout,
437
+ walletReady: false,
438
+ summary: "installed (wallet not initialized)",
439
+ };
440
+ }
441
+
442
+ return {
443
+ command: command || "midnight",
444
+ available: true,
445
+ version: versionResult.stdout,
446
+ walletReady: false,
447
+ summary: "installed (wallet check failed)",
448
+ };
449
+ }
450
+
451
+ function walletStatusText(walletProbe, includeHelp = false, env = {}) {
452
+ const lines = [];
453
+ const shartProbe = detectOpenShart(env, { allowNpx: false });
454
+ lines.push("Midnight wallet CLI (optional)");
455
+ lines.push(` Command: ${walletProbe.command}`);
456
+
457
+ if (!walletProbe.available) {
458
+ lines.push(` Status: ${walletProbe.summary}`);
459
+ lines.push(" Install: npm install -g midnight-wallet-cli");
460
+ lines.push(' MCP: command "midnight-wallet-mcp"');
461
+ lines.push(` OpenShart: ${shartProbe.available ? "available" : "missing (required for encrypted wallet provisioning)"}`);
462
+ return lines.join("\n");
463
+ }
464
+
465
+ lines.push(` Status: ${walletProbe.version ? `v${walletProbe.version}` : "installed"} (${walletProbe.summary})`);
466
+
467
+ if (walletProbe.walletReady) {
468
+ lines.push(` Wallet: ${walletProbe.walletAddress || "unknown"}`);
469
+ lines.push(` Network: ${walletProbe.walletNetwork || "unknown"}`);
470
+ } else {
471
+ lines.push(" Wallet: not initialized");
472
+ lines.push(" Init: midnight generate --network preprod");
473
+ }
474
+
475
+ lines.push(` OpenShart: ${shartProbe.available ? "available" : "missing (required for /nightpay wallet provision)"}`);
476
+ lines.push(" Note: does not replace bridge OPERATOR_ADDRESS (64-char shielded hex)");
477
+
478
+ if (includeHelp) {
479
+ lines.push("");
480
+ lines.push("Run /nightpay wallet help for full setup notes.");
481
+ }
482
+
483
+ return lines.join("\n");
484
+ }
485
+
486
+ /**
487
+ * Probe bridge/health: returns { contractAddress, network, stub } or null on failure.
488
+ */
489
+ async function probeBridge(bridgeUrl, logger) {
490
+ try {
491
+ const res = await fetch(`${bridgeUrl}/health`, { signal: AbortSignal.timeout(6000) });
492
+ if (!res.ok) {
493
+ logger.warn(`[nightpay] Bridge health check failed: HTTP ${res.status}`);
494
+ return null;
495
+ }
496
+ return await res.json();
497
+ } catch (err) {
498
+ logger.warn(`[nightpay] Bridge unreachable: ${err.message}`);
499
+ return null;
500
+ }
501
+ }
502
+
503
+ /**
504
+ * Copy skills/nightpay into every configured agent workspace.
505
+ * Always removes the existing path first (handles symlinks, real dirs, absent).
506
+ * OpenClaw realpath() rejects symlinks outside workspace root; real files pass.
507
+ */
508
+ function wireSkillIntoWorkspaces(config, logger) {
509
+ const workspaces = new Set();
510
+ const defaultWs = config?.agents?.defaults?.workspace;
511
+ if (defaultWs) workspaces.add(defaultWs);
512
+ const agents = config?.agents?.list ?? [];
513
+ for (const agent of agents) {
514
+ if (agent?.workspace) workspaces.add(agent.workspace);
515
+ }
516
+ let wired = 0, errors = 0;
517
+ for (const ws of workspaces) {
518
+ const skillsDir = join(ws, "skills");
519
+ const destPath = join(skillsDir, "nightpay");
520
+ try {
521
+ if (!existsSync(skillsDir)) mkdirSync(skillsDir, { recursive: true });
522
+ rmSync(destPath, { recursive: true, force: true });
523
+ cpSync(SKILL_SRC, destPath, { recursive: true });
524
+ wired++;
525
+ } catch (err) {
526
+ errors++;
527
+ logger.warn(`[nightpay] Could not copy skill docs into ${ws}: ${err.message}`);
528
+ }
529
+ }
530
+ return { wired, errors };
531
+ }
532
+
533
+ const plugin = {
534
+ id: "nightpay",
535
+ name: "NightPay",
536
+ description: "Anonymous community bounties -- Midnight ZK proofs + Masumi settlement + Cardano finality",
537
+ configSchema: { safeParse: () => ({ success: true }) },
538
+
539
+ register(api) {
540
+ api.on("gateway_start", async () => {
541
+ // 1. Wire skill docs into all workspaces
542
+ const { wired, errors } = wireSkillIntoWorkspaces(api.config, api.logger);
543
+ api.logger.info(
544
+ `[nightpay] Skill docs refreshed in ${wired} workspace(s)` +
545
+ (errors > 0 ? ` (${errors} error(s))` : "")
546
+ );
547
+
548
+ const env = resolveEnv(api.config);
549
+ const missing = missingEnv(env);
550
+
551
+ if (missing.length > 0) {
552
+ api.logger.warn(
553
+ `[nightpay] Plugin loaded -- ${missing.length} required credential(s) missing: ${missing.join(", ")}.\n` +
554
+ ` Set: openclaw config set skills.entries.nightpay.env.<KEY> "value"\n` +
555
+ ` Then: openclaw gateway restart\n` +
556
+ ` Run /nightpay help for the operating model`
557
+ );
558
+ return;
559
+ }
560
+
561
+ // 2. Probe bridge for Midnight contract + stub status
562
+ const bridgeUrl = env.BRIDGE_URL || "";
563
+ const bridgeHealth = bridgeUrl ? await probeBridge(bridgeUrl, api.logger) : null;
564
+ const walletProbe = probeWalletCli(env);
565
+
566
+ // 3. Auto-populate RECEIPT_CONTRACT_ADDRESS from bridge if not already set
567
+ if (bridgeHealth?.contractAddress && !env.RECEIPT_CONTRACT_ADDRESS) {
568
+ api.logger.warn(
569
+ `[nightpay] RECEIPT_CONTRACT_ADDRESS not set -- discovered from bridge: ${bridgeHealth.contractAddress}\n` +
570
+ ` Set it: openclaw config set skills.entries.nightpay.env.RECEIPT_CONTRACT_ADDRESS "${bridgeHealth.contractAddress}"`
571
+ );
572
+ }
573
+
574
+ // 4. Warn about missing wallet keys
575
+ const missingWallet = missingWalletEnv(env);
576
+ if (missingWallet.length > 0) {
577
+ api.logger.warn(
578
+ `[nightpay] Wallet credentials missing (ZK receipts + operator signing disabled): ${missingWallet.join(", ")}\n` +
579
+ ` RECEIPT_CONTRACT_ADDRESS: from bridge /health (contractAddress field)\n` +
580
+ ` OPERATOR_SECRET_KEY: from your Midnight wallet / operator setup`
581
+ );
582
+ }
583
+
584
+ // 5. Warn if operator address looks like a placeholder
585
+ if (isPlaceholderAddress(env.OPERATOR_ADDRESS)) {
586
+ api.logger.warn(
587
+ `[nightpay] OPERATOR_ADDRESS looks like a placeholder ("${env.OPERATOR_ADDRESS?.slice(0, 12)}...").\n` +
588
+ ` Set a real Midnight shielded address (64-char lowercase hex).\n` +
589
+ ` Read it from bridge /operator-address or your operator wallet setup.`
590
+ );
591
+ }
592
+
593
+ // 6. Build wallet connectivity summary
594
+ const url = env.NIGHTPAY_API_URL || DEFAULTS.NIGHTPAY_API_URL;
595
+ const net = env.MIDNIGHT_NETWORK || DEFAULTS.MIDNIGHT_NETWORK;
596
+ const fee = env.OPERATOR_FEE_BPS || DEFAULTS.OPERATOR_FEE_BPS;
597
+
598
+ const bridgeStatus = bridgeHealth
599
+ ? `${bridgeHealth.status}${bridgeHealth.stub ? " (stub/preprod)" : " (live)"} | contract: ${bridgeHealth.contractAddress?.slice(0, 16)}...`
600
+ : "unreachable";
601
+
602
+ api.logger.info(
603
+ `[nightpay] Ready -- ${url} | network: ${net} | fee: ${fee}bps\n` +
604
+ ` Masumi (MIP-003): ${url} [API reachable on startup]\n` +
605
+ ` Midnight bridge: ${bridgeUrl} -> ${bridgeStatus}\n` +
606
+ ` Midnight operator: ${env.OPERATOR_ADDRESS ? env.OPERATOR_ADDRESS.slice(0, 16) + "..." : "NOT SET"}\n` +
607
+ ` Wallet CLI: ${walletProbe.summary}${walletProbe.available && walletProbe.version ? ` (v${walletProbe.version})` : ""}\n` +
608
+ ` ZK receipts: ${env.RECEIPT_CONTRACT_ADDRESS ? "contract set" : "RECEIPT_CONTRACT_ADDRESS missing"}\n` +
609
+ ` Skill docs: skills/nightpay/ (in all agent workspaces)\n` +
610
+ ` Type /nightpay help for the full operating model`
611
+ );
612
+ });
613
+
614
+ api.on("before_prompt_build", async (event, ctx) => {
615
+ const env = resolveEnv(api.config);
616
+ if (missingEnv(env).length > 0) return;
617
+ const intent = detectIntent(event.prompt, event.messages);
618
+ if (intent === "none") return;
619
+ return { prependContext: intent === "full" ? FULL_CONTEXT : BRIEF_CONTEXT };
620
+ });
621
+
622
+ api.registerCommand({
623
+ name: "nightpay",
624
+ description: "NightPay -- status, bridge check, wallet helper, operating model",
625
+ acceptsArgs: true,
626
+ requireAuth: true,
627
+ handler: async (ctx) => {
628
+ const env = resolveEnv(api.config);
629
+ const missing = missingEnv(env);
630
+ const args = (ctx.args || "").trim();
631
+
632
+ if (args === "help") return { text: OPERATING_MODEL };
633
+ if (args === "wallet help") return { text: WALLET_CLI_HELP };
634
+ if (args === "wallet provision" || args.startsWith("wallet provision ")) {
635
+ const parts = args.split(/\s+/).filter(Boolean);
636
+ const requestedNetwork = parts[2] || "";
637
+ const provision = provisionWalletEncrypted(env, requestedNetwork);
638
+ return { text: provision.text };
639
+ }
640
+ if (args === "wallet" || args === "wallet status") {
641
+ const walletProbe = probeWalletCli(env);
642
+ return { text: walletStatusText(walletProbe, true, env) };
643
+ }
644
+
645
+ if (missing.length > 0) {
646
+ const fixes = missing
647
+ .map((k) => ` openclaw config set skills.entries.nightpay.env.${k} "your-value"`)
648
+ .join("\n");
649
+ return {
650
+ text:
651
+ `NightPay not configured -- missing: ${missing.join(", ")}\n\n` +
652
+ `Fix:\n${fixes}\n\nThen: openclaw gateway restart\n` +
653
+ `Run /nightpay help to see the operating model.`,
654
+ };
655
+ }
656
+
657
+ const apiUrl = env.NIGHTPAY_API_URL || DEFAULTS.NIGHTPAY_API_URL;
658
+ const network = env.MIDNIGHT_NETWORK || DEFAULTS.MIDNIGHT_NETWORK;
659
+ const feeBps = env.OPERATOR_FEE_BPS || DEFAULTS.OPERATOR_FEE_BPS;
660
+ const bridgeUrl = env.BRIDGE_URL || "";
661
+ const walletProbe = probeWalletCli(env);
662
+
663
+ if (!args || args === "status") {
664
+ // Live bridge probe for /nightpay status
665
+ let bridgeLine = "not configured";
666
+ if (bridgeUrl) {
667
+ try {
668
+ const res = await fetch(`${bridgeUrl}/health`, { signal: AbortSignal.timeout(5000) });
669
+ const h = res.ok ? await res.json() : null;
670
+ bridgeLine = h
671
+ ? `${h.status}${h.stub ? " (stub)" : " (live)"} | contract: ${h.contractAddress?.slice(0, 16)}... | network: ${h.network}`
672
+ : `HTTP ${res.status}`;
673
+ } catch (e) {
674
+ bridgeLine = `unreachable (${e.message})`;
675
+ }
676
+ }
677
+
678
+ const walletMissing = missingWalletEnv(env);
679
+ const addrOk = !isPlaceholderAddress(env.OPERATOR_ADDRESS);
680
+
681
+ return {
682
+ text:
683
+ `NightPay v0.3.11\n\n` +
684
+ `Masumi (MIP-003)\n` +
685
+ ` API: ${apiUrl}\n` +
686
+ ` Key: ${env.MASUMI_API_KEY ? "set" : "MISSING"}\n\n` +
687
+ `Midnight bridge\n` +
688
+ ` URL: ${bridgeUrl || "MISSING"}\n` +
689
+ ` Status: ${bridgeLine}\n` +
690
+ ` Receipt: ${env.RECEIPT_CONTRACT_ADDRESS ? env.RECEIPT_CONTRACT_ADDRESS.slice(0, 16) + "..." : "MISSING -- set RECEIPT_CONTRACT_ADDRESS"}\n\n` +
691
+ `Midnight operator\n` +
692
+ ` Address: ${addrOk ? env.OPERATOR_ADDRESS.slice(0, 16) + "..." : "PLACEHOLDER -- set a real 64-char hex address"}\n` +
693
+ ` Fee: ${feeBps} bps (${(Number(feeBps) / 100).toFixed(1)}%)\n` +
694
+ ` Network: ${network}\n\n` +
695
+ (walletMissing.length > 0
696
+ ? `Missing wallet credentials: ${walletMissing.join(", ")}\n RECEIPT_CONTRACT_ADDRESS: from bridge /health\n OPERATOR_SECRET_KEY: from your Midnight wallet\n\n`
697
+ : "Wallet: fully configured\n\n") +
698
+ walletStatusText(walletProbe, false, env) + "\n\n" +
699
+ `Run /nightpay help for the operating model.`,
700
+ };
701
+ }
702
+
703
+ return {
704
+ text: `NightPay: handling "${args}"`,
705
+ agentInstructions: `Use the nightpay skill. Task: ${args}. Read skills/nightpay/SKILL.md for tool reference.`,
706
+ };
707
+ },
708
+ });
709
+ },
710
+ };
711
+
712
+ export default plugin;