@paynodelabs/paynode-402-cli 2.5.0
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 +73 -0
- package/commands/check.ts +126 -0
- package/commands/get-api-detail.ts +30 -0
- package/commands/invoke-paid-api.ts +91 -0
- package/commands/list-paid-apis.ts +58 -0
- package/commands/mint.ts +97 -0
- package/commands/request.ts +400 -0
- package/commands/tasks.ts +74 -0
- package/index.ts +114 -0
- package/marketplace/client.ts +189 -0
- package/marketplace/types.ts +37 -0
- package/package.json +40 -0
- package/tests/commands.test.ts +88 -0
- package/tests/network.test.ts +53 -0
- package/tests/utils.test.ts +50 -0
- package/tsconfig.json +19 -0
- package/utils.ts +400 -0
|
@@ -0,0 +1,400 @@
|
|
|
1
|
+
import { PayNodeAgentClient, RequestOptions, ethers } from '@paynodelabs/sdk-js';
|
|
2
|
+
import { join, parse } from 'path';
|
|
3
|
+
import { tmpdir } from 'os';
|
|
4
|
+
import fs from 'fs';
|
|
5
|
+
import { spawn } from 'child_process';
|
|
6
|
+
import {
|
|
7
|
+
getPrivateKey,
|
|
8
|
+
resolveNetwork,
|
|
9
|
+
requireMainnetConfirmation,
|
|
10
|
+
reportError,
|
|
11
|
+
jsonEnvelope,
|
|
12
|
+
withRetry,
|
|
13
|
+
generateTaskId,
|
|
14
|
+
isInlineContent,
|
|
15
|
+
cleanupOldTasks,
|
|
16
|
+
DEFAULT_TASK_DIR,
|
|
17
|
+
DEFAULT_MAX_AGE_SECONDS,
|
|
18
|
+
EXIT_CODES,
|
|
19
|
+
SKILL_VERSION,
|
|
20
|
+
GLOBAL_CONFIG,
|
|
21
|
+
BaseCliOptions,
|
|
22
|
+
maskAddress
|
|
23
|
+
} from '../utils.ts';
|
|
24
|
+
|
|
25
|
+
interface UnifiedRequestOptions extends BaseCliOptions {
|
|
26
|
+
method?: string;
|
|
27
|
+
data?: string;
|
|
28
|
+
header?: string | string[];
|
|
29
|
+
background?: boolean;
|
|
30
|
+
output?: string;
|
|
31
|
+
maxAge?: number;
|
|
32
|
+
taskDir?: string;
|
|
33
|
+
taskId?: string;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
interface CoreResult {
|
|
37
|
+
result: {
|
|
38
|
+
url: string;
|
|
39
|
+
method: string;
|
|
40
|
+
http_status: number;
|
|
41
|
+
content_type: string;
|
|
42
|
+
body_type: 'json' | 'text' | 'binary';
|
|
43
|
+
network: string;
|
|
44
|
+
data: any;
|
|
45
|
+
duration_ms: number;
|
|
46
|
+
dry_run?: boolean;
|
|
47
|
+
wallet?: string;
|
|
48
|
+
message?: string;
|
|
49
|
+
data_binary?: string;
|
|
50
|
+
data_size?: number;
|
|
51
|
+
};
|
|
52
|
+
binaryBuffer?: Uint8Array;
|
|
53
|
+
contentType: string;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// --- Background Launcher ---
|
|
57
|
+
function spawnBackground(url: string, args: string[], options: UnifiedRequestOptions) {
|
|
58
|
+
const taskId = options.taskId || generateTaskId(); // Use existing if re-spawning (though unlikely)
|
|
59
|
+
const taskDir = options.taskDir || DEFAULT_TASK_DIR;
|
|
60
|
+
const maxAge = options.maxAge || DEFAULT_MAX_AGE_SECONDS;
|
|
61
|
+
const outputPath = options.output || join(taskDir, `${taskId}.json`);
|
|
62
|
+
const logPath = join(taskDir, `${taskId}.log`);
|
|
63
|
+
|
|
64
|
+
fs.mkdirSync(taskDir, { recursive: true });
|
|
65
|
+
cleanupOldTasks(taskDir, maxAge);
|
|
66
|
+
|
|
67
|
+
const originalArgs = process.argv.slice(2);
|
|
68
|
+
const flagsToRemove = ['--background', '--json', '--task-id', '--output', '--dry-run', '--max-age', '--task-dir'];
|
|
69
|
+
const childArgs: string[] = [];
|
|
70
|
+
|
|
71
|
+
for (let i = 0; i < originalArgs.length; i++) {
|
|
72
|
+
const arg = originalArgs[i];
|
|
73
|
+
if (flagsToRemove.includes(arg)) {
|
|
74
|
+
// If flag takes an argument, skip both flag and value
|
|
75
|
+
if (['--output', '--task-id', '--max-age', '--task-dir'].includes(arg) && i + 1 < originalArgs.length) {
|
|
76
|
+
i++;
|
|
77
|
+
}
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
childArgs.push(arg);
|
|
81
|
+
}
|
|
82
|
+
childArgs.push('--task-id', taskId, '--output', outputPath);
|
|
83
|
+
|
|
84
|
+
// [SECURITY & LOGIC]
|
|
85
|
+
// This is a self-re-execution for background processing.
|
|
86
|
+
// 1. We spawn the same script (process.argv[1]) with filtered arguments.
|
|
87
|
+
// 2. We add '--task-id' which signals the next execution to use 'executeAndWrite' path.
|
|
88
|
+
// 3. This avoids infinite recursion because the sub-process will NOT have '--background' in its args.
|
|
89
|
+
// 4. Stderr is piped to a .log file to allow debugging of background failures.
|
|
90
|
+
|
|
91
|
+
const logFd = fs.openSync(logPath, 'a');
|
|
92
|
+
// The child command is pinned to 'process.execPath' (the current runtime) and 'process.argv[1]' (the current script).
|
|
93
|
+
// Arguments are filtered to prevent recursive loops.
|
|
94
|
+
// [SECURITY] Filter environment variables passed to background child process.
|
|
95
|
+
// Minimizes exposure of non-essential credentials.
|
|
96
|
+
const whitelist = [
|
|
97
|
+
'CLIENT_PRIVATE_KEY',
|
|
98
|
+
'CUSTOM_ROUTER_ADDRESS',
|
|
99
|
+
'CUSTOM_USDC_ADDRESS',
|
|
100
|
+
'RPC_URL',
|
|
101
|
+
'ALCHEMY_API_KEY',
|
|
102
|
+
'INFURA_API_KEY',
|
|
103
|
+
'ETHERSCAN_API_KEY',
|
|
104
|
+
'HTTP_PROXY',
|
|
105
|
+
'HTTPS_PROXY',
|
|
106
|
+
'NODE_PATH',
|
|
107
|
+
'NVM_DIR',
|
|
108
|
+
'BUN_INSTALL'
|
|
109
|
+
];
|
|
110
|
+
// Essential OS-level vars
|
|
111
|
+
const baseEnv = Object.fromEntries(
|
|
112
|
+
Object.entries({
|
|
113
|
+
PATH: process.env.PATH,
|
|
114
|
+
HOME: process.env.HOME,
|
|
115
|
+
TMPDIR: process.env.TMPDIR,
|
|
116
|
+
USER: process.env.USER,
|
|
117
|
+
SHELL: process.env.SHELL
|
|
118
|
+
}).filter(([, v]) => v !== undefined)
|
|
119
|
+
) as Record<string, string>;
|
|
120
|
+
const childEnv: Record<string, string | undefined> = { ...baseEnv };
|
|
121
|
+
for (const key of whitelist) {
|
|
122
|
+
if (process.env[key]) childEnv[key] = process.env[key];
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const child = spawn(process.execPath, [process.argv[1], ...childArgs], {
|
|
126
|
+
detached: true,
|
|
127
|
+
stdio: ['ignore', 'ignore', logFd],
|
|
128
|
+
env: childEnv
|
|
129
|
+
});
|
|
130
|
+
child.unref();
|
|
131
|
+
|
|
132
|
+
// After unref. the parent no longer needs logFd. The child has its own copy.
|
|
133
|
+
fs.closeSync(logFd);
|
|
134
|
+
|
|
135
|
+
const pendingInfo = {
|
|
136
|
+
status: 'pending',
|
|
137
|
+
task_id: taskId,
|
|
138
|
+
output_file: outputPath,
|
|
139
|
+
task_dir: taskDir,
|
|
140
|
+
max_age_seconds: maxAge,
|
|
141
|
+
command: `cat ${outputPath}`,
|
|
142
|
+
message: 'š x402 background request started. The wallet will automatically handle payments.'
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
if (options.json) {
|
|
146
|
+
console.log(jsonEnvelope(pendingInfo));
|
|
147
|
+
} else {
|
|
148
|
+
console.log(`\nš **Background Task Started**`);
|
|
149
|
+
console.log(`- **Task ID**: \`${taskId}\``);
|
|
150
|
+
console.log(`- **Output**: \`${outputPath}\``);
|
|
151
|
+
console.log(`- **Log**: \`${logPath}\``);
|
|
152
|
+
console.log(`\nUse \`cat ${outputPath}\` to check progress or \`tail -f ${logPath}\` for logs.`);
|
|
153
|
+
}
|
|
154
|
+
process.exit(0);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// --- Core x402 Execution ---
|
|
158
|
+
async function executeCore(url: string, args: string[], options: UnifiedRequestOptions): Promise<CoreResult> {
|
|
159
|
+
const isJson = !!options.json || !!options.taskId;
|
|
160
|
+
const startTs = Date.now();
|
|
161
|
+
|
|
162
|
+
// [INPUT-GUARD] Validate URL structure
|
|
163
|
+
if (!url || typeof url !== 'string' || (!url.startsWith('http://') && !url.startsWith('https://'))) {
|
|
164
|
+
throw new Error(`Invalid destination URL: '${url}'. Must start with 'http://' or 'https://'.`);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const { rpcUrls, networkName, isSandbox } = await resolveNetwork(options.rpc, options.network, options.rpcTimeout);
|
|
168
|
+
requireMainnetConfirmation(isSandbox, !!options.confirmMainnet, isJson);
|
|
169
|
+
|
|
170
|
+
// Handle params (k=v)
|
|
171
|
+
const kvParams: Record<string, string> = {};
|
|
172
|
+
for (const p of args) {
|
|
173
|
+
if (!p.includes('=')) continue;
|
|
174
|
+
const [k, ...v] = p.split('=');
|
|
175
|
+
kvParams[k.trim()] = v.join('=').trim();
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const method = options.method?.toUpperCase() || (options.data || Object.keys(kvParams).length > 0 ? 'POST' : 'GET');
|
|
179
|
+
|
|
180
|
+
// Headers parsing
|
|
181
|
+
const headers: Record<string, string> = {};
|
|
182
|
+
if (options.header) {
|
|
183
|
+
const headerArray = Array.isArray(options.header) ? options.header : [options.header];
|
|
184
|
+
for (const h of headerArray) {
|
|
185
|
+
if (!h || !h.includes(':')) continue;
|
|
186
|
+
const [k, ...v] = h.split(':');
|
|
187
|
+
headers[k.trim()] = v.join(':').trim();
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
// [P1] Inject network header for Proxy validation
|
|
191
|
+
const paynodeNetwork = isSandbox ? 'testnet' : 'mainnet';
|
|
192
|
+
if (!headers['X-PayNode-Network']) {
|
|
193
|
+
headers['X-PayNode-Network'] = paynodeNetwork;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// Auto-sniff JSON body for manual data
|
|
197
|
+
if (options.data && !headers['Content-Type'] && !headers['content-type']) {
|
|
198
|
+
try {
|
|
199
|
+
JSON.parse(options.data);
|
|
200
|
+
headers['Content-Type'] = 'application/json';
|
|
201
|
+
} catch { /* ignore */ }
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const requestOptions: RequestOptions = { method, headers };
|
|
205
|
+
let targetUrl = url;
|
|
206
|
+
|
|
207
|
+
if (method === 'GET') {
|
|
208
|
+
const urlObj = new URL(url);
|
|
209
|
+
for (const [k, v] of Object.entries(kvParams)) {
|
|
210
|
+
urlObj.searchParams.set(k, v);
|
|
211
|
+
}
|
|
212
|
+
targetUrl = urlObj.toString();
|
|
213
|
+
} else {
|
|
214
|
+
if (options.data) {
|
|
215
|
+
requestOptions.body = options.data;
|
|
216
|
+
} else {
|
|
217
|
+
// [Smart Promotion] For POST/PUT, if no explicit body data is given but
|
|
218
|
+
// query parameters exist (either in URL or as args), put them into JSON body.
|
|
219
|
+
const urlObj = new URL(url);
|
|
220
|
+
const combinedParams = { ...kvParams };
|
|
221
|
+
|
|
222
|
+
// If the user only passed the URL with query params (no extra args)
|
|
223
|
+
if (Object.keys(combinedParams).length === 0 && urlObj.searchParams.size > 0) {
|
|
224
|
+
for (const [k, v] of urlObj.searchParams.entries()) {
|
|
225
|
+
combinedParams[k] = v;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
if (Object.keys(combinedParams).length > 0) {
|
|
230
|
+
requestOptions.json = combinedParams;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// Dry-run
|
|
236
|
+
if (options.dryRun) {
|
|
237
|
+
const pkForAddress = GLOBAL_CONFIG.PRIVATE_KEY;
|
|
238
|
+
let walletAddr: string | undefined;
|
|
239
|
+
try {
|
|
240
|
+
if (pkForAddress && isJson) {
|
|
241
|
+
walletAddr = maskAddress((new ethers.Wallet(pkForAddress)).address);
|
|
242
|
+
}
|
|
243
|
+
} catch { /* skip if PK invalid */ }
|
|
244
|
+
|
|
245
|
+
return {
|
|
246
|
+
result: {
|
|
247
|
+
url: targetUrl,
|
|
248
|
+
method,
|
|
249
|
+
http_status: 0,
|
|
250
|
+
content_type: 'application/json',
|
|
251
|
+
body_type: 'json',
|
|
252
|
+
network: networkName,
|
|
253
|
+
data: null,
|
|
254
|
+
duration_ms: 0,
|
|
255
|
+
dry_run: true,
|
|
256
|
+
wallet: walletAddr,
|
|
257
|
+
message: 'Dry-run: request prepared but not sent.'
|
|
258
|
+
},
|
|
259
|
+
contentType: 'application/json'
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
const pk = getPrivateKey(isJson);
|
|
264
|
+
|
|
265
|
+
const client = new PayNodeAgentClient(pk, rpcUrls);
|
|
266
|
+
const response = await withRetry(
|
|
267
|
+
() => client.requestGate(targetUrl, requestOptions),
|
|
268
|
+
'x402:requestGate'
|
|
269
|
+
);
|
|
270
|
+
|
|
271
|
+
const contentType = response.headers.get('content-type') || 'application/octet-stream';
|
|
272
|
+
const httpStatus = response.status;
|
|
273
|
+
let resultBody: any;
|
|
274
|
+
let bodyType: 'json' | 'text' | 'binary' = 'text';
|
|
275
|
+
let binaryBuffer: Uint8Array | undefined;
|
|
276
|
+
|
|
277
|
+
if (isInlineContent(contentType)) {
|
|
278
|
+
if (contentType.toLowerCase().includes('application/json')) {
|
|
279
|
+
resultBody = await response.json();
|
|
280
|
+
bodyType = 'json';
|
|
281
|
+
} else {
|
|
282
|
+
resultBody = await response.text();
|
|
283
|
+
bodyType = 'text';
|
|
284
|
+
}
|
|
285
|
+
} else {
|
|
286
|
+
const arrayBuf = await response.arrayBuffer();
|
|
287
|
+
binaryBuffer = new Uint8Array(arrayBuf);
|
|
288
|
+
bodyType = 'binary';
|
|
289
|
+
resultBody = null;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
return {
|
|
293
|
+
result: {
|
|
294
|
+
url: targetUrl,
|
|
295
|
+
method,
|
|
296
|
+
http_status: httpStatus,
|
|
297
|
+
content_type: contentType,
|
|
298
|
+
body_type: bodyType,
|
|
299
|
+
network: networkName,
|
|
300
|
+
data: resultBody,
|
|
301
|
+
duration_ms: Date.now() - startTs
|
|
302
|
+
},
|
|
303
|
+
binaryBuffer,
|
|
304
|
+
contentType
|
|
305
|
+
};
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// --- Persistence ---
|
|
309
|
+
async function executeAndWrite(url: string, args: string[], options: UnifiedRequestOptions) {
|
|
310
|
+
const taskId = options.taskId || generateTaskId();
|
|
311
|
+
const taskDir = options.taskDir || DEFAULT_TASK_DIR;
|
|
312
|
+
const outputPath = options.output || join(taskDir, `${taskId}.json`);
|
|
313
|
+
|
|
314
|
+
fs.mkdirSync(taskDir, { recursive: true });
|
|
315
|
+
|
|
316
|
+
try {
|
|
317
|
+
const { result, binaryBuffer, contentType } = await executeCore(url, args, options);
|
|
318
|
+
|
|
319
|
+
if (binaryBuffer) {
|
|
320
|
+
const { dir, name } = parse(outputPath);
|
|
321
|
+
const binaryPath = join(dir, `${name}.bin`);
|
|
322
|
+
fs.writeFileSync(binaryPath, binaryBuffer);
|
|
323
|
+
result.data = `[binary: ${contentType}, ${binaryBuffer.length} bytes ā ${binaryPath}]`;
|
|
324
|
+
result.data_binary = binaryPath;
|
|
325
|
+
result.data_size = binaryBuffer.length;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
const finalOutput = {
|
|
329
|
+
version: SKILL_VERSION,
|
|
330
|
+
status: 'completed',
|
|
331
|
+
task_id: taskId,
|
|
332
|
+
...result,
|
|
333
|
+
completed_at: new Date().toISOString()
|
|
334
|
+
};
|
|
335
|
+
|
|
336
|
+
fs.writeFileSync(outputPath, JSON.stringify(finalOutput, null, 2));
|
|
337
|
+
} catch (error: any) {
|
|
338
|
+
const errorResult = {
|
|
339
|
+
version: SKILL_VERSION,
|
|
340
|
+
status: 'failed',
|
|
341
|
+
task_id: taskId,
|
|
342
|
+
error: error.message,
|
|
343
|
+
errorCode: error?.code || 'internal_error',
|
|
344
|
+
completed_at: new Date().toISOString()
|
|
345
|
+
};
|
|
346
|
+
fs.writeFileSync(outputPath, JSON.stringify(errorResult, null, 2));
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
// --- Main Entry ---
|
|
351
|
+
export async function requestAction(url: string, args: string[], options: UnifiedRequestOptions) {
|
|
352
|
+
if (options.background) {
|
|
353
|
+
spawnBackground(url, args, options);
|
|
354
|
+
return;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
if (options.taskId) {
|
|
358
|
+
await executeAndWrite(url, args, options);
|
|
359
|
+
return;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
const isJson = !!options.json;
|
|
363
|
+
|
|
364
|
+
try {
|
|
365
|
+
if (!isJson && !options.dryRun) {
|
|
366
|
+
console.error(`š x402 Request: ${url}...`);
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
const { result, binaryBuffer, contentType } = await executeCore(url, args, options);
|
|
370
|
+
|
|
371
|
+
if (binaryBuffer) {
|
|
372
|
+
const binPath = options.output
|
|
373
|
+
? join(parse(options.output).dir, `${parse(options.output).name}.bin`)
|
|
374
|
+
: join(tmpdir(), `paynode-${Date.now().toString(36)}.bin`);
|
|
375
|
+
|
|
376
|
+
fs.writeFileSync(binPath, binaryBuffer);
|
|
377
|
+
result.data = `[binary: ${contentType}, ${binaryBuffer.length} bytes ā ${binPath}]`;
|
|
378
|
+
result.data_binary = binPath;
|
|
379
|
+
result.data_size = binaryBuffer.length;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
if (isJson) {
|
|
383
|
+
console.log(jsonEnvelope({ status: 'success', ...result }));
|
|
384
|
+
} else {
|
|
385
|
+
if (result.dry_run) {
|
|
386
|
+
console.log('š§Ŗ DRY RUN PREPARED:');
|
|
387
|
+
console.log(JSON.stringify(result, null, 2));
|
|
388
|
+
} else {
|
|
389
|
+
if (typeof result.data === 'object') {
|
|
390
|
+
console.log(JSON.stringify(result.data, null, 2));
|
|
391
|
+
} else {
|
|
392
|
+
console.log(result.data);
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
} catch (error: any) {
|
|
397
|
+
reportError(error, isJson, EXIT_CODES.NETWORK_ERROR);
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import { join } from 'path';
|
|
3
|
+
import { DEFAULT_TASK_DIR, jsonEnvelope, BaseCliOptions, cleanupOldTasks } from '../utils.ts';
|
|
4
|
+
|
|
5
|
+
interface TasksOptions extends BaseCliOptions {
|
|
6
|
+
clean?: boolean;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export async function tasksAction(subcommand: string | undefined, options: TasksOptions) {
|
|
10
|
+
const isJson = !!options.json;
|
|
11
|
+
const taskDir = DEFAULT_TASK_DIR;
|
|
12
|
+
|
|
13
|
+
if (subcommand === 'clean' || options.clean) {
|
|
14
|
+
const cleaned = cleanupOldTasks(taskDir, 0); // Cleanup everything immediately if explicit
|
|
15
|
+
if (isJson) {
|
|
16
|
+
console.log(jsonEnvelope({ status: 'success', message: `Cleaned ${cleaned} task files from ${taskDir}` }));
|
|
17
|
+
} else {
|
|
18
|
+
console.log(`ā
Successfully cleaned ${cleaned} tasks.`);
|
|
19
|
+
}
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// Default is list
|
|
24
|
+
if (!fs.existsSync(taskDir)) {
|
|
25
|
+
if (isJson) {
|
|
26
|
+
console.log(jsonEnvelope({ status: 'success', tasks: [] }));
|
|
27
|
+
} else {
|
|
28
|
+
console.log('No tasks found (Directory does not exist).');
|
|
29
|
+
}
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const files = fs.readdirSync(taskDir).filter(f => f.endsWith('.json') && !f.startsWith('.'));
|
|
34
|
+
const tasks = files.map(file => {
|
|
35
|
+
try {
|
|
36
|
+
const content = fs.readFileSync(join(taskDir, file), 'utf-8');
|
|
37
|
+
const data = JSON.parse(content);
|
|
38
|
+
const stats = fs.statSync(join(taskDir, file));
|
|
39
|
+
return {
|
|
40
|
+
id: data.task_id || file.replace('.json', ''),
|
|
41
|
+
status: data.status,
|
|
42
|
+
url: data.url,
|
|
43
|
+
method: data.method,
|
|
44
|
+
created_at: stats.birthtime.toISOString(),
|
|
45
|
+
completed_at: data.completed_at,
|
|
46
|
+
error: data.error
|
|
47
|
+
};
|
|
48
|
+
} catch {
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
}).filter(Boolean).sort((a: any, b: any) => new Date(b.created_at).getTime() - new Date(a.created_at).getTime());
|
|
52
|
+
|
|
53
|
+
if (isJson) {
|
|
54
|
+
console.log(jsonEnvelope({ status: 'success', total: tasks.length, tasks }));
|
|
55
|
+
} else {
|
|
56
|
+
if (tasks.length === 0) {
|
|
57
|
+
console.log(`No tasks found in ${taskDir}`);
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
console.log(`\nš Recent x402 Background Tasks in ${taskDir}:`);
|
|
62
|
+
console.log(`āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā`);
|
|
63
|
+
|
|
64
|
+
for (const t of tasks as any[]) {
|
|
65
|
+
const statusIcon = t.status === 'completed' ? 'ā
' : t.status === 'failed' ? 'ā' : 'š';
|
|
66
|
+
const indicator = `(${t.status || 'unknown'})`.padEnd(12);
|
|
67
|
+
const urlPart = t.url ? `| ${t.url}` : '';
|
|
68
|
+
console.log(`${statusIcon} ${t.id.padEnd(12)} ${indicator} ${urlPart}`);
|
|
69
|
+
if (t.error) console.log(` āā Error: ${t.error}`);
|
|
70
|
+
}
|
|
71
|
+
console.log(`āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā`);
|
|
72
|
+
console.log(`š” Usage: 'cat ${taskDir}/<id>.json' for full results.`);
|
|
73
|
+
}
|
|
74
|
+
}
|
package/index.ts
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
import { cac } from 'cac';
|
|
3
|
+
import pkg from './package.json';
|
|
4
|
+
import { checkAction } from './commands/check.ts';
|
|
5
|
+
import { mintAction } from './commands/mint.ts';
|
|
6
|
+
import { requestAction } from './commands/request.ts';
|
|
7
|
+
import { listPaidApisAction } from './commands/list-paid-apis.ts';
|
|
8
|
+
import { getApiDetailAction } from './commands/get-api-detail.ts';
|
|
9
|
+
import { invokePaidApiAction } from './commands/invoke-paid-api.ts';
|
|
10
|
+
import { tasksAction } from './commands/tasks.ts';
|
|
11
|
+
import { EXIT_CODES } from './utils.ts';
|
|
12
|
+
|
|
13
|
+
const cli = cac('paynode-402');
|
|
14
|
+
|
|
15
|
+
// Global Options
|
|
16
|
+
cli.option('--json', 'Output results in JSON format');
|
|
17
|
+
cli.option('--network <name>', 'Network to use: mainnet or testnet/sepolia');
|
|
18
|
+
cli.option('--rpc <url>', 'Custom RPC URL');
|
|
19
|
+
cli.option('--rpc-timeout <ms>', 'Custom RPC timeout in milliseconds (default: 15000)');
|
|
20
|
+
cli.option('--confirm-mainnet', 'Required flag for mainnet operations (real USDC)');
|
|
21
|
+
cli.option('--dry-run', 'Show request details without sending');
|
|
22
|
+
cli.option('--market-url <url>', 'Marketplace base URL');
|
|
23
|
+
|
|
24
|
+
// Command: check
|
|
25
|
+
cli
|
|
26
|
+
.command('check', 'Check wallet balance (ETH and USDC) on Base L2')
|
|
27
|
+
.action((options) => {
|
|
28
|
+
return checkAction(options);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
// Command: mint
|
|
32
|
+
cli
|
|
33
|
+
.command('mint', 'Mint USDC on Base Sepolia')
|
|
34
|
+
.option('--amount <amount>', 'Amount to mint (default: 1000)')
|
|
35
|
+
.action((options) => {
|
|
36
|
+
return mintAction(options);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
// Command: request
|
|
40
|
+
cli
|
|
41
|
+
.command('request <url> [...params]', 'Access protected API and handle x402 payments. Params: key=value pairs for query/body.')
|
|
42
|
+
.option('-X, --method <method>', 'HTTP method (GET, POST, etc.)')
|
|
43
|
+
.option('-d, --data <data>', 'Raw request body data')
|
|
44
|
+
.option('-H, --header [header]', 'HTTP header in "Key: Value" format (can be used multiple times)', { default: [] })
|
|
45
|
+
.option('--background', 'Execute in background, return immediately (AI-friendly)')
|
|
46
|
+
.option('--output <path>', 'Output file path for result (used with --background)')
|
|
47
|
+
.option('--max-age <seconds>', 'Auto-delete task files older than N seconds (default: 3600)')
|
|
48
|
+
.option('--task-dir <path>', 'Task directory for background results (default: <TMPDIR>/paynode-tasks)')
|
|
49
|
+
.option('--task-id <id>', 'Internal: task ID for background worker')
|
|
50
|
+
.action((url, params, options) => {
|
|
51
|
+
return requestAction(url, params, options);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
// Command: list-paid-apis
|
|
55
|
+
cli
|
|
56
|
+
.command('list-paid-apis', 'List paid APIs from the marketplace catalog')
|
|
57
|
+
.option('--limit <n>', 'Maximum number of APIs to return')
|
|
58
|
+
.option('--tag [tag]', 'Catalog tag filter (can be used multiple times)', { default: [] })
|
|
59
|
+
.option('--seller <seller>', 'Seller identifier filter')
|
|
60
|
+
.action((options) => {
|
|
61
|
+
return listPaidApisAction(options);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
// Command: get-api-detail
|
|
65
|
+
cli
|
|
66
|
+
.command('get-api-detail <apiId>', 'Get full detail for one paid API')
|
|
67
|
+
.action((apiId, options) => {
|
|
68
|
+
return getApiDetailAction(apiId, options);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
// Command: invoke-paid-api
|
|
72
|
+
cli
|
|
73
|
+
.command('invoke-paid-api <apiId>', 'Invoke one paid API through the marketplace flow')
|
|
74
|
+
.option('-X, --method <method>', 'HTTP method override')
|
|
75
|
+
.option('-d, --data <data>', 'Invocation payload as raw JSON string')
|
|
76
|
+
.option('-H, --header [header]', 'HTTP header in "Key: Value" format (can be used multiple times)', { default: [] })
|
|
77
|
+
.option('--background', 'Execute in background, return immediately (AI-friendly)')
|
|
78
|
+
.option('--output <path>', 'Output file path for result (used with --background)')
|
|
79
|
+
.option('--max-age <seconds>', 'Auto-delete task files older than N seconds (default: 3600)')
|
|
80
|
+
.option('--task-dir <path>', 'Task directory for background results (default: <TMPDIR>/paynode-tasks)')
|
|
81
|
+
.option('--task-id <id>', 'Internal: task ID for background worker')
|
|
82
|
+
.action((apiId, options) => {
|
|
83
|
+
return invokePaidApiAction(apiId, options);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
// Command: tasks
|
|
87
|
+
cli
|
|
88
|
+
.command('tasks [subcommand]', 'Manage background tasks (subcommands: list, clean)')
|
|
89
|
+
.option('--clean', 'Clean all task files immediately')
|
|
90
|
+
.action((subcommand, options) => {
|
|
91
|
+
return tasksAction(subcommand, options);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
cli.help();
|
|
96
|
+
cli.version(pkg.version);
|
|
97
|
+
|
|
98
|
+
try {
|
|
99
|
+
const result = cli.parse();
|
|
100
|
+
if (result instanceof Promise) {
|
|
101
|
+
result.catch((err) => {
|
|
102
|
+
console.error(`ā Global Error: ${err.message}`);
|
|
103
|
+
process.exit(EXIT_CODES.GENERIC_ERROR);
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
} catch (error: any) {
|
|
107
|
+
if (error.name === 'CACError') {
|
|
108
|
+
console.error(`ā Command Error: ${error.message}`);
|
|
109
|
+
process.exit(EXIT_CODES.INVALID_ARGS);
|
|
110
|
+
} else {
|
|
111
|
+
console.error(`ā Parse Error: ${error.message}`);
|
|
112
|
+
process.exit(EXIT_CODES.GENERIC_ERROR);
|
|
113
|
+
}
|
|
114
|
+
}
|