clawpowers 1.1.1 → 1.1.3

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.
@@ -0,0 +1,459 @@
1
+ #!/usr/bin/env node
2
+ // runtime/payments/pipeline.js — Unified Payment Decision Pipeline
3
+ //
4
+ // Central gate for ALL skill payment decisions. Any skill that encounters a
5
+ // payment boundary (HTTP 402, premium scanner, paid API, x402 endpoint) calls
6
+ // evaluatePayment() here instead of making its own policy judgements.
7
+ //
8
+ // Flow:
9
+ // estimate_cost → check_config → check_policy →
10
+ // (disabled | dry_run | queued | approved | rejected) → log
11
+ //
12
+ // Usage (module):
13
+ // const { evaluatePayment } = require('./pipeline');
14
+ // const result = await evaluatePayment({ skill, reason, amount_usd, asset, chain, recipient, url });
15
+ //
16
+ // Usage (CLI):
17
+ // node pipeline.js evaluate --amount 0.05 --skill security-audit --reason "premium scanner"
18
+ // npx clawpowers payments evaluate --amount 0.05 --skill security-audit --reason "premium scanner"
19
+ 'use strict';
20
+
21
+ const fs = require('fs');
22
+ const path = require('path');
23
+ const os = require('os');
24
+ const { logPaymentDecision, LOGS_DIR } = require('./ledger');
25
+
26
+ // ─── Config paths ──────────────────────────────────────────────────────────
27
+
28
+ /** Root of the ~/.clawpowers/ runtime directory. */
29
+ const CLAWPOWERS_DIR = process.env.CLAWPOWERS_DIR || path.join(os.homedir(), '.clawpowers');
30
+
31
+ /** Main config file path. */
32
+ const CONFIG_FILE = path.join(CLAWPOWERS_DIR, 'config.json');
33
+
34
+ /** Payment ledger path (JSONL — one record per decision). */
35
+ const PAYMENTS_LEDGER = path.join(CLAWPOWERS_DIR, 'logs', 'payments.jsonl');
36
+
37
+ // ─── Helpers ───────────────────────────────────────────────────────────────
38
+
39
+ /**
40
+ * Load and parse ~/.clawpowers/config.json.
41
+ * Returns an empty object if the file doesn't exist.
42
+ *
43
+ * @returns {object} Parsed config or {}.
44
+ */
45
+ function loadConfig() {
46
+ try {
47
+ if (!fs.existsSync(CONFIG_FILE)) return {};
48
+ const raw = fs.readFileSync(CONFIG_FILE, 'utf8');
49
+ return JSON.parse(raw);
50
+ } catch (err) {
51
+ process.stderr.write(`[pipeline] Warning: could not read config: ${err.message}\n`);
52
+ return {};
53
+ }
54
+ }
55
+
56
+ /**
57
+ * Compute today's total spend (USD) from the payments ledger.
58
+ * Only counts entries with action "approved" or "executed" and today's date.
59
+ *
60
+ * @returns {number} Total USD spent today across all skills and chains.
61
+ */
62
+ function getTodaySpend() {
63
+ try {
64
+ if (!fs.existsSync(PAYMENTS_LEDGER)) return 0;
65
+ const today = new Date().toISOString().slice(0, 10); // "YYYY-MM-DD"
66
+ const lines = fs.readFileSync(PAYMENTS_LEDGER, 'utf8').split('\n');
67
+ let total = 0;
68
+ for (const line of lines) {
69
+ if (!line.trim()) continue;
70
+ try {
71
+ const r = JSON.parse(line);
72
+ if (
73
+ r.timestamp &&
74
+ r.timestamp.startsWith(today) &&
75
+ (r.policy_result === 'approved' || r.policy_result === 'executed')
76
+ ) {
77
+ total += parseFloat(r.required_amount) || 0;
78
+ }
79
+ } catch (_) { /* skip malformed */ }
80
+ }
81
+ return total;
82
+ } catch {
83
+ return 0;
84
+ }
85
+ }
86
+
87
+ /**
88
+ * Check if a recipient address appears in the configured allowlist.
89
+ * An empty allowlist means NO recipients are automatically allowed.
90
+ *
91
+ * @param {string} recipient - Recipient address (0x-prefixed).
92
+ * @param {string[]} allowlist - Array of allowed addresses (case-insensitive).
93
+ * @returns {boolean} True if allowed or allowlist is not configured.
94
+ */
95
+ function isAllowlisted(recipient, allowlist) {
96
+ // If allowlist is undefined/null → not configured → no automatic allowance
97
+ if (!Array.isArray(allowlist)) return false;
98
+ // Empty allowlist → nothing allowed
99
+ if (allowlist.length === 0) return false;
100
+ const lower = (recipient || '').toLowerCase();
101
+ return allowlist.some((a) => (a || '').toLowerCase() === lower);
102
+ }
103
+
104
+ /**
105
+ * Returns an ISO 8601 timestamp without milliseconds.
106
+ * @returns {string}
107
+ */
108
+ function isoNow() {
109
+ return new Date().toISOString().replace(/\.\d{3}Z$/, 'Z');
110
+ }
111
+
112
+ // ─── Core pipeline ─────────────────────────────────────────────────────────
113
+
114
+ /**
115
+ * Payment Decision Pipeline — evaluate whether a payment should be made.
116
+ *
117
+ * Called by any skill when it hits a payment boundary. Reads config, checks
118
+ * policy limits, and returns a decision with a reason. Always logs the decision
119
+ * to the payments ledger regardless of outcome.
120
+ *
121
+ * ### Decision flow
122
+ *
123
+ * ```
124
+ * payments.enabled === false → "disabled"
125
+ * payments.mode === "dry_run" → "dry_run"
126
+ * amount > per_tx_limit → "rejected" (hard limit)
127
+ * today_spend + amount > daily → "rejected" (daily limit)
128
+ * recipient not in allowlist → "queued" (needs approval)
129
+ * amount > require_approval_above → "queued" (needs approval)
130
+ * all checks pass → "approved"
131
+ * ```
132
+ *
133
+ * @param {object} decision - Payment details.
134
+ * @param {string} decision.skill - ClawPowers skill triggering the payment.
135
+ * @param {string} decision.reason - Human-readable reason ("HTTP 402", "premium scanner", …).
136
+ * @param {number} decision.amount_usd - Estimated cost in USD (decimal).
137
+ * @param {string} [decision.asset] - Token/asset symbol (default "USDC").
138
+ * @param {string} [decision.chain] - Chain name (default "base").
139
+ * @param {string} [decision.recipient] - Payment recipient address.
140
+ * @param {string} [decision.url] - URL or service that triggered the boundary.
141
+ *
142
+ * @returns {{
143
+ * action: "disabled"|"dry_run"|"queued"|"approved"|"rejected",
144
+ * reason: string,
145
+ * logged: boolean,
146
+ * amount_usd: number,
147
+ * config_used: object
148
+ * }} Decision result.
149
+ *
150
+ * @example
151
+ * ```javascript
152
+ * const { evaluatePayment } = require('./pipeline');
153
+ * const result = await evaluatePayment({
154
+ * skill: 'security-audit',
155
+ * reason: 'premium scanner API',
156
+ * amount_usd: 0.05,
157
+ * asset: 'USDC',
158
+ * chain: 'base',
159
+ * recipient: '0xSCANNER_RECIPIENT',
160
+ * });
161
+ * if (result.action === 'approved') {
162
+ * // proceed with payment
163
+ * }
164
+ * ```
165
+ */
166
+ function evaluatePayment(decision) {
167
+ const {
168
+ skill = 'unknown',
169
+ reason = '',
170
+ amount_usd = 0,
171
+ asset = 'USDC',
172
+ chain = 'base',
173
+ recipient = '',
174
+ url = '',
175
+ } = decision;
176
+
177
+ // ── 1. Load config ────────────────────────────────────────────────────────
178
+ const config = loadConfig();
179
+ const payments = config.payments || {};
180
+
181
+ // ── 2. payments.enabled === false → disabled ──────────────────────────────
182
+ if (payments.enabled === false) {
183
+ const result = {
184
+ action: 'disabled',
185
+ reason: 'Payments disabled in ~/.clawpowers/config.json (payments.enabled = false)',
186
+ logged: false,
187
+ amount_usd,
188
+ config_used: payments,
189
+ };
190
+ result.logged = _logDecision(skill, url, String(amount_usd), asset, chain, 'disabled', result.reason, false);
191
+ return result;
192
+ }
193
+
194
+ // ── 3. payments.mode === "dry_run" → log what would happen, no payment ────
195
+ if (payments.mode === 'dry_run') {
196
+ const summary =
197
+ `DRY-RUN: skill=${skill} reason="${reason}" would pay ` +
198
+ `$${amount_usd.toFixed ? amount_usd.toFixed(4) : amount_usd} ${asset} on ${chain}` +
199
+ (url ? ` for ${url}` : '');
200
+ console.log(`[pipeline] ${summary}`);
201
+
202
+ const result = {
203
+ action: 'dry_run',
204
+ reason: summary,
205
+ logged: false,
206
+ amount_usd,
207
+ config_used: payments,
208
+ };
209
+ result.logged = _logDecision(skill, url, String(amount_usd), asset, chain, 'dry_run', reason, true);
210
+ return result;
211
+ }
212
+
213
+ // ── 4. Check per-transaction limit ────────────────────────────────────────
214
+ const perTxLimit = typeof payments.per_tx_limit === 'number' ? payments.per_tx_limit : Infinity;
215
+ if (amount_usd > perTxLimit) {
216
+ const msg = `Amount $${amount_usd} exceeds per_tx_limit $${perTxLimit}`;
217
+ const result = {
218
+ action: 'rejected',
219
+ reason: msg,
220
+ logged: false,
221
+ amount_usd,
222
+ config_used: payments,
223
+ };
224
+ result.logged = _logDecision(skill, url, String(amount_usd), asset, chain, 'rejected', msg, false);
225
+ return result;
226
+ }
227
+
228
+ // ── 5. Check daily limit ─────────────────────────────────────────────────
229
+ const dailyLimit = typeof payments.daily_limit === 'number' ? payments.daily_limit : Infinity;
230
+ const todaySpend = getTodaySpend();
231
+ if (todaySpend + amount_usd > dailyLimit) {
232
+ const msg =
233
+ `Daily limit would be exceeded: today=$${todaySpend.toFixed(4)}, ` +
234
+ `new=$${amount_usd}, limit=$${dailyLimit}`;
235
+ const result = {
236
+ action: 'rejected',
237
+ reason: msg,
238
+ logged: false,
239
+ amount_usd,
240
+ config_used: payments,
241
+ };
242
+ result.logged = _logDecision(skill, url, String(amount_usd), asset, chain, 'rejected', msg, false);
243
+ return result;
244
+ }
245
+
246
+ // ── 6. Check recipient allowlist ─────────────────────────────────────────
247
+ const allowlist = payments.allowlist; // undefined = not configured
248
+ if (recipient && !isAllowlisted(recipient, allowlist)) {
249
+ // Not in allowlist — queue for approval unless allowlist is intentionally absent
250
+ // (If allowlist key doesn't exist at all, we fall through to approval-above check)
251
+ if (Array.isArray(allowlist)) {
252
+ const msg = `Recipient ${recipient} is not in payments.allowlist`;
253
+ const result = {
254
+ action: 'queued',
255
+ reason: msg,
256
+ logged: false,
257
+ amount_usd,
258
+ config_used: payments,
259
+ };
260
+ result.logged = _logDecision(skill, url, String(amount_usd), asset, chain, 'queued', msg, false);
261
+ return result;
262
+ }
263
+ }
264
+
265
+ // ── 7. Check require_approval_above threshold ─────────────────────────────
266
+ const approvalThreshold = typeof payments.require_approval_above === 'number'
267
+ ? payments.require_approval_above
268
+ : Infinity;
269
+ if (amount_usd > approvalThreshold) {
270
+ const msg =
271
+ `Amount $${amount_usd} exceeds require_approval_above threshold $${approvalThreshold}`;
272
+ const result = {
273
+ action: 'queued',
274
+ reason: msg,
275
+ logged: false,
276
+ amount_usd,
277
+ config_used: payments,
278
+ };
279
+ result.logged = _logDecision(skill, url, String(amount_usd), asset, chain, 'queued', msg, false);
280
+ return result;
281
+ }
282
+
283
+ // ── 8. All checks passed → approved ───────────────────────────────────────
284
+ const approveMsg = `Payment approved: $${amount_usd} ${asset} on ${chain} for ${skill} (${reason})`;
285
+ const result = {
286
+ action: 'approved',
287
+ reason: approveMsg,
288
+ logged: false,
289
+ amount_usd,
290
+ config_used: payments,
291
+ };
292
+ result.logged = _logDecision(skill, url, String(amount_usd), asset, chain, 'approved', reason, true);
293
+ return result;
294
+ }
295
+
296
+ /**
297
+ * Internal: write decision to the payments ledger via the ledger module.
298
+ *
299
+ * @param {string} skill
300
+ * @param {string} url
301
+ * @param {string} required_amount
302
+ * @param {string} asset
303
+ * @param {string} chain
304
+ * @param {string} policy_result
305
+ * @param {string} reason
306
+ * @param {boolean} would_have_paid
307
+ * @returns {boolean} Always true (indicates logging was attempted).
308
+ */
309
+ function _logDecision(skill, url, required_amount, asset, chain, policy_result, reason, would_have_paid) {
310
+ try {
311
+ logPaymentDecision({
312
+ skill,
313
+ type: 'decision',
314
+ url,
315
+ required_amount,
316
+ asset,
317
+ chain,
318
+ policy_result,
319
+ reason,
320
+ would_have_paid,
321
+ });
322
+ return true;
323
+ } catch (err) {
324
+ process.stderr.write(`[pipeline] Warning: could not write to ledger: ${err.message}\n`);
325
+ return false;
326
+ }
327
+ }
328
+
329
+ // ─── CLI ───────────────────────────────────────────────────────────────────
330
+
331
+ /**
332
+ * Print CLI usage to stdout.
333
+ */
334
+ function printUsage() {
335
+ console.log(`Usage: clawpowers payments evaluate [options]
336
+
337
+ Options:
338
+ --amount <n> Payment amount in USD (required, decimal, e.g. 0.05)
339
+ --skill <name> Skill that triggered the payment (default: "cli")
340
+ --reason <text> Reason for the payment (default: "cli test")
341
+ --asset <symbol> Token symbol (default: USDC)
342
+ --chain <name> Chain name (default: base)
343
+ --recipient <addr> Recipient wallet address (optional)
344
+ --url <url> URL or service requiring payment (optional)
345
+ --json Output raw JSON result
346
+
347
+ Examples:
348
+ node pipeline.js evaluate --amount 0.05 --skill security-audit --reason "premium scanner"
349
+ node pipeline.js evaluate --amount 0.01 --skill prospecting --reason "contact enrichment" --json
350
+ npx clawpowers payments evaluate --amount 0.05 --skill agent-payments --reason "x402 API"
351
+
352
+ Config file: ~/.clawpowers/config.json
353
+ Ledger file: ~/.clawpowers/logs/payments.jsonl`);
354
+ }
355
+
356
+ /**
357
+ * CLI handler for the `evaluate` sub-command.
358
+ * Parses --flags and calls evaluatePayment(), printing the result.
359
+ *
360
+ * @param {string[]} argv - Arguments after "evaluate".
361
+ */
362
+ function cmdEvaluate(argv) {
363
+ const args = {};
364
+ for (let i = 0; i < argv.length; i++) {
365
+ const flag = argv[i];
366
+ const next = argv[i + 1];
367
+ switch (flag) {
368
+ case '--amount': args.amount_usd = parseFloat(next); i++; break;
369
+ case '--skill': args.skill = next; i++; break;
370
+ case '--reason': args.reason = next; i++; break;
371
+ case '--asset': args.asset = next; i++; break;
372
+ case '--chain': args.chain = next; i++; break;
373
+ case '--recipient': args.recipient = next; i++; break;
374
+ case '--url': args.url = next; i++; break;
375
+ case '--json': args._json = true; break;
376
+ }
377
+ }
378
+
379
+ if (args.amount_usd === undefined || isNaN(args.amount_usd)) {
380
+ process.stderr.write('Error: --amount is required and must be a number\n\n');
381
+ printUsage();
382
+ process.exit(1);
383
+ }
384
+
385
+ const decision = {
386
+ skill: args.skill || 'cli',
387
+ reason: args.reason || 'cli test',
388
+ amount_usd: args.amount_usd,
389
+ asset: args.asset || 'USDC',
390
+ chain: args.chain || 'base',
391
+ recipient: args.recipient || '',
392
+ url: args.url || '',
393
+ };
394
+
395
+ const result = evaluatePayment(decision);
396
+
397
+ if (args._json) {
398
+ console.log(JSON.stringify(result, null, 2));
399
+ } else {
400
+ console.log('');
401
+ console.log(`Payment Decision Pipeline`);
402
+ console.log(`─────────────────────────`);
403
+ console.log(` Skill: ${decision.skill}`);
404
+ console.log(` Reason: ${decision.reason}`);
405
+ console.log(` Amount: $${decision.amount_usd} ${decision.asset} on ${decision.chain}`);
406
+ if (decision.url) console.log(` URL: ${decision.url}`);
407
+ if (decision.recipient) console.log(` To: ${decision.recipient}`);
408
+ console.log('');
409
+ console.log(` ► Action: ${result.action.toUpperCase()}`);
410
+ console.log(` ► Reason: ${result.reason}`);
411
+ console.log(` ► Logged: ${result.logged ? 'yes' : 'no'}`);
412
+ console.log('');
413
+ }
414
+
415
+ // Non-zero exit for rejected/disabled so scripts can branch on exit code
416
+ if (result.action === 'rejected') process.exit(2);
417
+ if (result.action === 'disabled') process.exit(3);
418
+ }
419
+
420
+ /**
421
+ * Main CLI dispatch.
422
+ * @param {string[]} argv
423
+ */
424
+ function main(argv) {
425
+ const [cmd, ...rest] = argv;
426
+
427
+ switch (cmd) {
428
+ case 'evaluate':
429
+ case 'eval':
430
+ cmdEvaluate(rest);
431
+ break;
432
+ case 'help':
433
+ case '-h':
434
+ case '--help':
435
+ printUsage();
436
+ break;
437
+ case undefined:
438
+ case '':
439
+ printUsage();
440
+ process.exit(1);
441
+ break;
442
+ default:
443
+ process.stderr.write(`Unknown command: ${cmd}\n`);
444
+ printUsage();
445
+ process.exit(1);
446
+ }
447
+ }
448
+
449
+ // Guard: only run CLI dispatch when invoked directly
450
+ if (require.main === module) {
451
+ try {
452
+ main(process.argv.slice(2));
453
+ } catch (err) {
454
+ process.stderr.write(`Error: ${err.message}\n`);
455
+ process.exit(1);
456
+ }
457
+ }
458
+
459
+ module.exports = { evaluatePayment };
package/skill.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "clawpowers",
3
- "version": "1.0.0",
4
- "description": "Runtime-powered skills framework 20 skills with persistent memory, self-improvement (RSI), outcome tracking, and autonomous agent payments (x402). Works on Claude Code, Cursor, Codex, OpenCode, Gemini CLI.",
3
+ "version": "1.1.1",
4
+ "description": "Runtime-powered skills framework \u2014 26 skills with persistent memory, self-improvement (RSI), outcome tracking, and autonomous agent payments (x402). Works on Claude Code, Cursor, Codex, OpenCode, Gemini CLI.",
5
5
  "author": {
6
6
  "name": "AI Agent Economy",
7
7
  "url": "https://github.com/up2itnow0822"
@@ -33,12 +33,36 @@
33
33
  "code review",
34
34
  "brainstorm",
35
35
  "security audit",
36
+ "fix this bug",
37
+ "refactor this",
36
38
  "pay for this API",
37
- "find leads",
39
+ "402",
40
+ "paywall",
41
+ "payment required",
42
+ "buy",
43
+ "order",
44
+ "subscribe",
45
+ "credits",
46
+ "pricing",
38
47
  "market research",
39
- "analyze performance"
48
+ "find leads",
49
+ "analyze performance",
50
+ "improve my skills",
51
+ "what went wrong",
52
+ "setup payments",
53
+ "enable wallet",
54
+ "configure spending",
55
+ "payment log",
56
+ "spending history",
57
+ "demo x402",
58
+ "test payments",
59
+ "mock merchant"
60
+ ],
61
+ "platforms": [
62
+ "macos",
63
+ "linux",
64
+ "windows"
40
65
  ],
41
- "platforms": ["macos", "linux", "windows"],
42
66
  "category": "development",
43
67
  "runtime": {
44
68
  "init": "node bin/clawpowers.js init",
@@ -69,6 +93,14 @@
69
93
  "learn-how-to-learn",
70
94
  "market-intelligence",
71
95
  "prospecting"
96
+ ],
97
+ "rsi_intelligence": [
98
+ "meta-skill-evolution",
99
+ "self-healing-code",
100
+ "cross-project-knowledge",
101
+ "formal-verification-lite",
102
+ "economic-code-optimization",
103
+ "agent-bounties"
72
104
  ]
73
105
  }
74
- }
106
+ }