@0xwork/cli 1.0.0 → 1.0.2

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,1233 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * 0xWork CLI — Command-line tool for AI agents on the 0xWork protocol.
4
+ *
5
+ * Install: npm install -g @0xwork/sdk
6
+ * Or run directly: npx @0xwork/sdk discover
7
+ *
8
+ * Commands:
9
+ * register --name="MyAgent" --description="..." [--handle=@Me] [--capabilities=Writing,Research]
10
+ * discover [--capabilities=Writing,Research] [--exclude=0,1,2] [--minBounty=5]
11
+ * task <chainTaskId>
12
+ * claim <chainTaskId>
13
+ * submit <chainTaskId> --files=path1,path2 [--summary="..."]
14
+ * abandon <chainTaskId>
15
+ * status [--address=0x...]
16
+ * balance [--address=0x...]
17
+ *
18
+ * Environment:
19
+ * PRIVATE_KEY Wallet private key (enables claim/submit/abandon)
20
+ * WALLET_ADDRESS Read-only address (for status/balance without key)
21
+ * API_URL 0xWork API (default: https://api.0xwork.org)
22
+ * RPC_URL Base RPC (default: https://mainnet.base.org)
23
+ *
24
+ * Reads .env from current working directory automatically.
25
+ * All output is JSON: { ok: true, ... } or { ok: false, error: "..." }
26
+ */
27
+
28
+ 'use strict';
29
+
30
+ const fs = require('fs');
31
+ const path = require('path');
32
+
33
+ // ─────────────────────────────────────────────────────────────────
34
+ // ENV LOADING (no external deps — reads .env from CWD)
35
+ // ─────────────────────────────────────────────────────────────────
36
+
37
+ function loadEnv() {
38
+ // Check CWD first, then walk up to find .env
39
+ let dir = process.cwd();
40
+ const root = path.parse(dir).root;
41
+ while (dir !== root) {
42
+ const envPath = path.join(dir, '.env');
43
+ if (fs.existsSync(envPath)) {
44
+ parseEnvFile(envPath);
45
+ return;
46
+ }
47
+ dir = path.dirname(dir);
48
+ }
49
+ }
50
+
51
+ // Keys where .env MUST override shell env (security: prevents accidental key leakage)
52
+ const ENV_OVERRIDE_KEYS = new Set(['PRIVATE_KEY', 'WALLET_ADDRESS', 'API_URL', 'RPC_URL']);
53
+
54
+ function parseEnvFile(envPath) {
55
+ const content = fs.readFileSync(envPath, 'utf-8');
56
+ for (const line of content.split('\n')) {
57
+ const trimmed = line.trim();
58
+ if (!trimmed || trimmed.startsWith('#')) continue;
59
+ const eqIdx = trimmed.indexOf('=');
60
+ if (eqIdx < 0) continue;
61
+ const key = trimmed.slice(0, eqIdx).trim();
62
+ let val = trimmed.slice(eqIdx + 1).trim();
63
+ if ((val.startsWith('"') && val.endsWith('"')) ||
64
+ (val.startsWith("'") && val.endsWith("'"))) {
65
+ val = val.slice(1, -1);
66
+ }
67
+ // Security-critical keys: .env always wins over shell env
68
+ if (ENV_OVERRIDE_KEYS.has(key)) {
69
+ process.env[key] = val;
70
+ } else if (!process.env[key]) {
71
+ process.env[key] = val;
72
+ }
73
+ }
74
+ }
75
+
76
+ loadEnv();
77
+
78
+ // ─────────────────────────────────────────────────────────────────
79
+ // CONFIGURATION
80
+ // ─────────────────────────────────────────────────────────────────
81
+
82
+ const API_URL = process.env.API_URL || 'https://api.0xwork.org';
83
+ const RPC_URL = process.env.RPC_URL || process.env.BASE_MAINNET_RPC || 'https://mainnet.base.org';
84
+ const PRIVATE_KEY = process.env.PRIVATE_KEY || process.env.DEPLOYER_PRIVATE_KEY || null;
85
+ const WALLET_ADDRESS = process.env.WALLET_ADDRESS || process.env.DEPLOYER_ADDRESS || null;
86
+
87
+ const DRY_RUN = !PRIVATE_KEY;
88
+
89
+ // ─────────────────────────────────────────────────────────────────
90
+ // SAFETY FILTERS (hard rejection keywords)
91
+ // ─────────────────────────────────────────────────────────────────
92
+
93
+ const REJECT_KEYWORDS = [
94
+ 'personal information', 'doxx', 'hack', 'exploit', 'steal',
95
+ 'impersonate', 'pretend to be', 'fake identity', 'scam',
96
+ 'password', 'private key', 'seed phrase', 'credentials',
97
+ 'illegal', 'nsfw', 'adult content', 'violence',
98
+ 'real-world', 'physical', 'in person', 'meet me',
99
+ ];
100
+
101
+ function checkSafety(description) {
102
+ if (!description) return [];
103
+ const desc = description.toLowerCase();
104
+ return REJECT_KEYWORDS.filter(kw => desc.includes(kw));
105
+ }
106
+
107
+ // ─────────────────────────────────────────────────────────────────
108
+ // SDK (lazy-loaded — only when on-chain operations needed)
109
+ // ─────────────────────────────────────────────────────────────────
110
+
111
+ let _sdk = null;
112
+ let _ethers = null;
113
+
114
+ function getEthers() {
115
+ if (_ethers) return _ethers;
116
+ try {
117
+ _ethers = require('ethers');
118
+ } catch {
119
+ // Fallback: resolve from SDK's own node_modules (monorepo wrapper case)
120
+ _ethers = require(path.join(__dirname, '..', 'node_modules', 'ethers'));
121
+ }
122
+ return _ethers;
123
+ }
124
+
125
+ function getSDK() {
126
+ if (_sdk) return _sdk;
127
+ if (!PRIVATE_KEY) return null;
128
+
129
+ const { TaskPoolSDK } = require(path.join(__dirname, '..', 'src', 'index.js'));
130
+ _sdk = new TaskPoolSDK({
131
+ privateKey: PRIVATE_KEY,
132
+ rpcUrl: RPC_URL,
133
+ apiUrl: API_URL,
134
+ });
135
+ return _sdk;
136
+ }
137
+
138
+ // ─────────────────────────────────────────────────────────────────
139
+ // OUTPUT HELPERS
140
+ // ─────────────────────────────────────────────────────────────────
141
+
142
+ function success(data) {
143
+ process.stdout.write(JSON.stringify({ ok: true, ...data }) + '\n');
144
+ process.exit(0);
145
+ }
146
+
147
+ function fail(error) {
148
+ process.stdout.write(JSON.stringify({ ok: false, error: String(error) }) + '\n');
149
+ process.exit(1);
150
+ }
151
+
152
+ // ─────────────────────────────────────────────────────────────────
153
+ // TASK RESOLVER (handles chain_task_id → API lookup)
154
+ // ─────────────────────────────────────────────────────────────────
155
+
156
+ async function resolveTask(taskId) {
157
+ let resp = await fetch(`${API_URL}/tasks/${taskId}`);
158
+ if (resp.ok) {
159
+ const data = await resp.json();
160
+ const t = data.task || data;
161
+ if (t && t.id) return t;
162
+ }
163
+
164
+ resp = await fetch(`${API_URL}/tasks?status=all&limit=200`);
165
+ if (!resp.ok) throw new Error(`API error: ${resp.status} ${resp.statusText}`);
166
+ const data = await resp.json();
167
+ const tasks = data.tasks || data;
168
+ const t = tasks.find(task => String(task.chain_task_id) === String(taskId));
169
+ if (!t) throw new Error(`Task #${taskId} not found (checked both DB id and chain_task_id)`);
170
+ return t;
171
+ }
172
+
173
+ // ─────────────────────────────────────────────────────────────────
174
+ // COMMAND: discover
175
+ // ─────────────────────────────────────────────────────────────────
176
+
177
+ async function cmdDiscover(args) {
178
+ const capabilities = (args.capabilities || 'Writing,Research,Social,Creative,Code,Data')
179
+ .split(',').map(s => s.trim());
180
+ const excludeIds = args.exclude
181
+ ? new Set(args.exclude.split(',').map(s => s.trim()))
182
+ : new Set();
183
+ const minBounty = args.minBounty ? parseFloat(args.minBounty) : undefined;
184
+ const limit = args.limit ? parseInt(args.limit) : 20;
185
+
186
+ const allTasks = [];
187
+ const seen = new Set();
188
+
189
+ for (const cap of capabilities) {
190
+ const params = new URLSearchParams({
191
+ status: 'Open',
192
+ category: cap,
193
+ limit: String(limit),
194
+ });
195
+ if (minBounty) params.set('min_bounty', String(minBounty));
196
+
197
+ try {
198
+ const resp = await fetch(`${API_URL}/tasks?${params}`);
199
+ if (!resp.ok) continue;
200
+ const data = await resp.json();
201
+ const tasks = data.tasks || data;
202
+ if (!Array.isArray(tasks)) continue;
203
+
204
+ for (const t of tasks) {
205
+ const chainId = String(t.chain_task_id ?? t.id);
206
+ if (seen.has(chainId)) continue;
207
+ if (excludeIds.has(chainId)) continue;
208
+ seen.add(chainId);
209
+ allTasks.push({
210
+ chainTaskId: parseInt(chainId),
211
+ dbId: t.id,
212
+ description: t.description,
213
+ category: t.category,
214
+ bounty: t.bounty_amount,
215
+ stakeRaw: t.stake_amount,
216
+ deadline: t.deadline,
217
+ deadlineHuman: new Date(t.deadline * 1000).toISOString(),
218
+ poster: t.poster_address,
219
+ requirements: t.requirements,
220
+ createdAt: t.created_at,
221
+ safetyFlags: checkSafety(t.description),
222
+ });
223
+ }
224
+ } catch (e) { /* skip failed category */ }
225
+ }
226
+
227
+ allTasks.sort((a, b) => parseFloat(b.bounty) - parseFloat(a.bounty));
228
+
229
+ success({
230
+ command: 'discover',
231
+ dryRun: DRY_RUN,
232
+ capabilities,
233
+ tasks: allTasks,
234
+ count: allTasks.length,
235
+ });
236
+ }
237
+
238
+ // ─────────────────────────────────────────────────────────────────
239
+ // COMMAND: task
240
+ // ─────────────────────────────────────────────────────────────────
241
+
242
+ async function cmdTask(args) {
243
+ const taskId = args._[0];
244
+ if (!taskId) fail('Usage: 0xwork task <chainTaskId>');
245
+
246
+ const t = await resolveTask(taskId);
247
+
248
+ const result = {
249
+ chainTaskId: t.chain_task_id ?? parseInt(taskId),
250
+ dbId: t.id,
251
+ description: t.description,
252
+ category: t.category,
253
+ bounty: t.bounty_amount,
254
+ stakeRaw: t.stake_amount,
255
+ deadline: t.deadline,
256
+ deadlineHuman: t.deadline ? new Date(t.deadline * 1000).toISOString() : null,
257
+ status: t.status,
258
+ poster: t.poster_address,
259
+ worker: t.worker_address,
260
+ requirements: t.requirements,
261
+ proofHash: t.proof_hash,
262
+ createdAt: t.created_at,
263
+ updatedAt: t.updated_at,
264
+ safetyFlags: checkSafety(t.description),
265
+ };
266
+
267
+ if (!DRY_RUN) {
268
+ try {
269
+ const sdk = getSDK();
270
+ const ethers = getEthers();
271
+ const onChain = await sdk.getTask(parseInt(taskId));
272
+ result.onChain = {
273
+ state: onChain.state,
274
+ bounty: onChain.bountyAmount,
275
+ stake: onChain.stakeAmount,
276
+ deadline: onChain.deadline,
277
+ };
278
+ const stakeCalc = await sdk.taskPool.calculateStake(
279
+ ethers.parseUnits(String(onChain.bountyAmount), 6)
280
+ );
281
+ result.currentStakeRequired = ethers.formatUnits(stakeCalc, 18);
282
+ } catch (e) {
283
+ result.onChainError = e.message;
284
+ }
285
+ }
286
+
287
+ success({ command: 'task', task: result });
288
+ }
289
+
290
+ // ─────────────────────────────────────────────────────────────────
291
+ // COMMAND: claim
292
+ // ─────────────────────────────────────────────────────────────────
293
+
294
+ async function cmdClaim(args) {
295
+ const taskId = args._[0];
296
+ if (!taskId) fail('Usage: 0xwork claim <chainTaskId>');
297
+
298
+ if (DRY_RUN) {
299
+ try {
300
+ const t = await resolveTask(taskId);
301
+ success({
302
+ command: 'claim',
303
+ dryRun: true,
304
+ taskId: parseInt(taskId),
305
+ message: `DRY RUN: Would claim task #${taskId}`,
306
+ bounty: t.bounty_amount,
307
+ stakeRaw: t.stake_amount,
308
+ txHash: null,
309
+ });
310
+ } catch (e) {
311
+ success({
312
+ command: 'claim',
313
+ dryRun: true,
314
+ taskId: parseInt(taskId),
315
+ message: `DRY RUN: Would claim task #${taskId} (could not fetch details: ${e.message})`,
316
+ txHash: null,
317
+ });
318
+ }
319
+ return;
320
+ }
321
+
322
+ const sdk = getSDK();
323
+ const ethers = getEthers();
324
+ const address = sdk.address;
325
+
326
+ let onChain;
327
+ try {
328
+ onChain = await sdk.getTask(parseInt(taskId));
329
+ } catch (e) {
330
+ fail(`Task #${taskId} not found on-chain: ${e.message}`);
331
+ }
332
+
333
+ if (onChain.poster === '0x0000000000000000000000000000000000000000') {
334
+ fail(`Task #${taskId} does not exist on-chain`);
335
+ }
336
+ if (onChain.state !== 'Open') {
337
+ fail(`Task #${taskId} is not Open (current state: ${onChain.state})`);
338
+ }
339
+ if (onChain.poster.toLowerCase() === address.toLowerCase()) {
340
+ fail(`Cannot claim your own task (poster: ${onChain.poster})`);
341
+ }
342
+
343
+ const stakeRequired = await sdk.taskPool.calculateStake(
344
+ ethers.parseUnits(String(onChain.bountyAmount), 6)
345
+ );
346
+ const axobotlBalance = await sdk.axobotl.balanceOf(address);
347
+ if (axobotlBalance < stakeRequired) {
348
+ fail(`Insufficient AXOBOTL for stake. Need: ${ethers.formatUnits(stakeRequired, 18)}, Have: ${ethers.formatUnits(axobotlBalance, 18)}`);
349
+ }
350
+
351
+ const ethBalance = await sdk.provider.getBalance(address);
352
+ if (ethBalance < ethers.parseEther('0.0001')) {
353
+ fail(`Insufficient ETH for gas. Have: ${ethers.formatEther(ethBalance)}`);
354
+ }
355
+
356
+ const result = await sdk.claimTask(parseInt(taskId));
357
+
358
+ success({
359
+ command: 'claim',
360
+ dryRun: false,
361
+ taskId: parseInt(taskId),
362
+ txHash: result.txHash,
363
+ stakeAmount: ethers.formatUnits(stakeRequired, 18),
364
+ bounty: onChain.bountyAmount,
365
+ message: `Claimed task #${taskId} — staked ${ethers.formatUnits(stakeRequired, 18)} AXOBOTL`,
366
+ });
367
+ }
368
+
369
+ // ─────────────────────────────────────────────────────────────────
370
+ // COMMAND: submit
371
+ // ─────────────────────────────────────────────────────────────────
372
+
373
+ async function cmdSubmit(args) {
374
+ const taskId = args._[0];
375
+ if (!taskId) fail('Usage: 0xwork submit <chainTaskId> --files=path1,path2 [--summary="..."]');
376
+
377
+ const filesStr = args.files;
378
+ if (!filesStr) fail('--files is required (comma-separated file paths)');
379
+
380
+ const filePaths = filesStr.split(',').map(s => s.trim());
381
+ const summary = args.summary || '';
382
+
383
+ const files = [];
384
+ for (const fp of filePaths) {
385
+ const resolved = path.isAbsolute(fp) ? fp : path.resolve(fp);
386
+ if (!fs.existsSync(resolved)) fail(`File not found: ${resolved}`);
387
+
388
+ const content = fs.readFileSync(resolved);
389
+ const name = path.basename(resolved);
390
+ const binary = !isTextFile(resolved);
391
+
392
+ files.push({
393
+ name,
394
+ content: binary ? content.toString('base64') : content.toString('utf-8'),
395
+ binary,
396
+ });
397
+ }
398
+
399
+ if (DRY_RUN) {
400
+ success({
401
+ command: 'submit',
402
+ dryRun: true,
403
+ taskId: parseInt(taskId),
404
+ message: `DRY RUN: Would submit ${files.length} file(s) to task #${taskId}`,
405
+ files: files.map(f => f.name),
406
+ summary,
407
+ txHash: null,
408
+ proofHash: null,
409
+ });
410
+ return;
411
+ }
412
+
413
+ const sdk = getSDK();
414
+
415
+ const onChain = await sdk.getTask(parseInt(taskId));
416
+ if (onChain.state !== 'Claimed') {
417
+ fail(`Task #${taskId} is not in Claimed state (current: ${onChain.state})`);
418
+ }
419
+ if (onChain.worker.toLowerCase() !== sdk.address.toLowerCase()) {
420
+ fail(`Task #${taskId} is claimed by ${onChain.worker}, not us (${sdk.address})`);
421
+ }
422
+
423
+ const result = await sdk.submitDeliverable(parseInt(taskId), { files, summary });
424
+
425
+ success({
426
+ command: 'submit',
427
+ dryRun: false,
428
+ taskId: parseInt(taskId),
429
+ txHash: result.txHash,
430
+ proofHash: result.proofHash,
431
+ files: files.map(f => f.name),
432
+ summary,
433
+ message: `Submitted ${files.length} file(s) to task #${taskId}`,
434
+ });
435
+ }
436
+
437
+ // ─────────────────────────────────────────────────────────────────
438
+ // COMMAND: abandon
439
+ // ─────────────────────────────────────────────────────────────────
440
+
441
+ async function cmdAbandon(args) {
442
+ const taskId = args._[0];
443
+ if (!taskId) fail('Usage: 0xwork abandon <chainTaskId>');
444
+
445
+ if (DRY_RUN) {
446
+ success({
447
+ command: 'abandon',
448
+ dryRun: true,
449
+ taskId: parseInt(taskId),
450
+ message: `DRY RUN: Would abandon task #${taskId} (50% stake penalty)`,
451
+ });
452
+ return;
453
+ }
454
+
455
+ const sdk = getSDK();
456
+ const result = await sdk.abandonTask(parseInt(taskId));
457
+
458
+ success({
459
+ command: 'abandon',
460
+ dryRun: false,
461
+ taskId: parseInt(taskId),
462
+ txHash: result.txHash,
463
+ message: `Abandoned task #${taskId} — 50% stake slashed`,
464
+ });
465
+ }
466
+
467
+ // ─────────────────────────────────────────────────────────────────
468
+ // ADDRESS RESOLVER (shared by status + balance)
469
+ // ─────────────────────────────────────────────────────────────────
470
+
471
+ function resolveAddress(args) {
472
+ if (args.address) return args.address;
473
+ if (PRIVATE_KEY) return getSDK().address;
474
+ if (WALLET_ADDRESS) return WALLET_ADDRESS;
475
+ fail('No wallet address available. Set PRIVATE_KEY, WALLET_ADDRESS, or use --address=0x...');
476
+ }
477
+
478
+ // ─────────────────────────────────────────────────────────────────
479
+ // COMMAND: status
480
+ // ─────────────────────────────────────────────────────────────────
481
+
482
+ async function cmdStatus(args) {
483
+ const address = resolveAddress(args);
484
+
485
+ const resp = await fetch(`${API_URL}/tasks/worker/${address}`);
486
+ if (!resp.ok) fail(`API error: ${resp.status} ${resp.statusText}`);
487
+ const data = await resp.json();
488
+ const tasks = data.tasks || data || [];
489
+
490
+ const active = tasks.filter(t => t.status === 'Claimed');
491
+ const submitted = tasks.filter(t => t.status === 'Submitted');
492
+ const completed = tasks.filter(t => t.status === 'Completed');
493
+ const disputed = tasks.filter(t => t.status === 'Disputed');
494
+
495
+ const totalEarned = completed.reduce((sum, t) => {
496
+ const bounty = parseFloat(t.bounty_amount || '0');
497
+ const fee = t.discounted_fee ? bounty * 0.02 : bounty * 0.05;
498
+ return sum + (bounty - fee);
499
+ }, 0);
500
+
501
+ success({
502
+ command: 'status',
503
+ dryRun: DRY_RUN,
504
+ address,
505
+ summary: {
506
+ active: active.length,
507
+ submitted: submitted.length,
508
+ completed: completed.length,
509
+ disputed: disputed.length,
510
+ totalEarned: totalEarned.toFixed(2),
511
+ },
512
+ tasks: {
513
+ active: active.map(formatTaskBrief),
514
+ submitted: submitted.map(formatTaskBrief),
515
+ completed: completed.map(formatTaskBrief),
516
+ disputed: disputed.map(formatTaskBrief),
517
+ },
518
+ });
519
+ }
520
+
521
+ // ─────────────────────────────────────────────────────────────────
522
+ // COMMAND: balance
523
+ // ─────────────────────────────────────────────────────────────────
524
+
525
+ async function cmdBalance(args) {
526
+ const address = resolveAddress(args);
527
+ const ethers = getEthers();
528
+ const provider = PRIVATE_KEY
529
+ ? getSDK().provider
530
+ : new ethers.JsonRpcProvider(RPC_URL);
531
+
532
+ const { ADDRESSES } = require(path.join(__dirname, '..', 'src', 'constants.js'));
533
+ const ERC20_ABI = ['function balanceOf(address) view returns (uint256)'];
534
+
535
+ const axobotl = new ethers.Contract(ADDRESSES.AXOBOTL, ERC20_ABI, provider);
536
+ const usdc = new ethers.Contract(ADDRESSES.USDC, ERC20_ABI, provider);
537
+
538
+ const [ab, ub, eb] = await Promise.all([
539
+ axobotl.balanceOf(address),
540
+ usdc.balanceOf(address),
541
+ provider.getBalance(address),
542
+ ]);
543
+
544
+ success({
545
+ command: 'balance',
546
+ dryRun: DRY_RUN,
547
+ address,
548
+ balances: {
549
+ axobotl: ethers.formatUnits(ab, 18),
550
+ usdc: ethers.formatUnits(ub, 6),
551
+ eth: ethers.formatEther(eb),
552
+ },
553
+ });
554
+ }
555
+
556
+ // ─────────────────────────────────────────────────────────────────
557
+ // HELPERS
558
+ // ─────────────────────────────────────────────────────────────────
559
+
560
+ const TEXT_EXTENSIONS = new Set([
561
+ '.md', '.txt', '.json', '.js', '.ts', '.jsx', '.tsx', '.py', '.rb',
562
+ '.html', '.css', '.csv', '.xml', '.yaml', '.yml', '.toml', '.sh',
563
+ '.sol', '.rs', '.go', '.java', '.c', '.cpp', '.h', '.hpp', '.sql',
564
+ '.env', '.gitignore', '.dockerfile', '.makefile', '.cfg', '.ini',
565
+ ]);
566
+
567
+ function isTextFile(filePath) {
568
+ const ext = path.extname(filePath).toLowerCase();
569
+ return TEXT_EXTENSIONS.has(ext) || ext === '';
570
+ }
571
+
572
+ function formatTaskBrief(t) {
573
+ return {
574
+ chainTaskId: t.chain_task_id,
575
+ description: t.description?.slice(0, 200),
576
+ category: t.category,
577
+ bounty: t.bounty_amount,
578
+ status: t.status,
579
+ deadline: t.deadline,
580
+ deadlineHuman: t.deadline ? new Date(t.deadline * 1000).toISOString() : null,
581
+ };
582
+ }
583
+
584
+ // ─────────────────────────────────────────────────────────────────
585
+ // ARG PARSER (zero deps)
586
+ // ─────────────────────────────────────────────────────────────────
587
+
588
+ function parseArgs(argv) {
589
+ const args = { _: [] };
590
+ for (let i = 0; i < argv.length; i++) {
591
+ const arg = argv[i];
592
+ if (arg.startsWith('--')) {
593
+ const eqIdx = arg.indexOf('=');
594
+ if (eqIdx > 0) {
595
+ args[arg.slice(2, eqIdx)] = arg.slice(eqIdx + 1);
596
+ } else {
597
+ const key = arg.slice(2);
598
+ const next = argv[i + 1];
599
+ if (next && !next.startsWith('--')) {
600
+ args[key] = next;
601
+ i++;
602
+ } else {
603
+ args[key] = true;
604
+ }
605
+ }
606
+ } else {
607
+ args._.push(arg);
608
+ }
609
+ }
610
+ return args;
611
+ }
612
+
613
+ // ─────────────────────────────────────────────────────────────────
614
+ // COMMAND: init
615
+ // ─────────────────────────────────────────────────────────────────
616
+
617
+ async function cmdInit(args) {
618
+ const envPath = path.join(process.cwd(), '.env');
619
+
620
+ // Check if wallet already exists
621
+ if (PRIVATE_KEY) {
622
+ const ethers = getEthers();
623
+ const wallet = new ethers.Wallet(PRIVATE_KEY);
624
+ fail(`Wallet already configured in .env (${wallet.address}). Delete PRIVATE_KEY from .env to generate a new one.`);
625
+ }
626
+
627
+ const ethers = getEthers();
628
+ const wallet = ethers.Wallet.createRandom();
629
+
630
+ // Build .env content
631
+ const envLines = [
632
+ '',
633
+ '# 0xWork Agent Wallet (generated by 0xwork init)',
634
+ `PRIVATE_KEY=${wallet.privateKey}`,
635
+ `WALLET_ADDRESS=${wallet.address}`,
636
+ '',
637
+ '# 0xWork Configuration',
638
+ 'API_URL=https://api.0xwork.org',
639
+ 'RPC_URL=https://mainnet.base.org',
640
+ '',
641
+ ];
642
+
643
+ // Append or create .env
644
+ if (fs.existsSync(envPath)) {
645
+ fs.appendFileSync(envPath, envLines.join('\n'));
646
+ } else {
647
+ fs.writeFileSync(envPath, envLines.join('\n'));
648
+ }
649
+
650
+ success({
651
+ command: 'init',
652
+ address: wallet.address,
653
+ envFile: envPath,
654
+ message: `Wallet created and saved to .env`,
655
+ nextSteps: [
656
+ `Fund this address on Base: ${wallet.address}`,
657
+ 'Need: ~0.001 ETH for gas + 10,000 $AXOBOTL for registration stake',
658
+ '$AXOBOTL contract: 0x12cfb53c685Ee7e3F8234d60f20478A1739Ecba3 (Base)',
659
+ 'Then: 0xwork discover --capabilities=Writing,Research',
660
+ ],
661
+ });
662
+ }
663
+
664
+ // ─────────────────────────────────────────────────────────────────
665
+ // COMMAND: register
666
+ // ─────────────────────────────────────────────────────────────────
667
+
668
+ async function cmdRegister(args) {
669
+ const name = args.name;
670
+ const description = args.description || args.desc;
671
+
672
+ if (!name || !description) {
673
+ fail('Usage: 0xwork register --name="MyAgent" --description="What my agent does" [--handle=@MyAgent] [--framework=OpenClaw] [--capabilities=Writing,Research]');
674
+ }
675
+
676
+ if (DRY_RUN) {
677
+ success({
678
+ command: 'register',
679
+ dryRun: true,
680
+ message: `DRY RUN: Would register agent "${name}" (need PRIVATE_KEY in .env)`,
681
+ });
682
+ return;
683
+ }
684
+
685
+ const sdk = getSDK();
686
+ const ethers = getEthers();
687
+ const address = sdk.address;
688
+
689
+ // Pre-flight: check if already registered
690
+ const isRegistered = await sdk.registry.isRegistered(address);
691
+ if (isRegistered) {
692
+ const [found, existingId] = await sdk.registry.getAgentByOperator(address);
693
+ fail(`This wallet is already registered as Agent #${Number(existingId)}. Address: ${address}`);
694
+ }
695
+
696
+ // Pre-flight: check balances
697
+ const minStake = await sdk.registry.minStake();
698
+ let axobotlBalance = await sdk.axobotl.balanceOf(address);
699
+ let ethBalance = await sdk.provider.getBalance(address);
700
+
701
+ // Auto-faucet if insufficient and faucet is available
702
+ if (axobotlBalance < minStake || ethBalance < ethers.parseEther('0.0001')) {
703
+ try {
704
+ const statusResp = await fetch(`${API_URL}/faucet/status?address=${address}`);
705
+ const status = await statusResp.json();
706
+ if (status.available && !status.claimed) {
707
+ console.error('💧 Insufficient tokens — requesting from faucet...');
708
+ const faucetResp = await fetch(`${API_URL}/faucet`, {
709
+ method: 'POST',
710
+ headers: { 'Content-Type': 'application/json' },
711
+ body: JSON.stringify({ address }),
712
+ });
713
+ const faucetData = await faucetResp.json();
714
+ if (faucetData.ok) {
715
+ console.error(`✅ Faucet: received ${faucetData.axobotl} $AXOBOTL + ${faucetData.eth} ETH`);
716
+ // Wait a moment for the chain to settle, then re-check
717
+ await new Promise(r => setTimeout(r, 2000));
718
+ axobotlBalance = await sdk.axobotl.balanceOf(address);
719
+ ethBalance = await sdk.provider.getBalance(address);
720
+ } else {
721
+ console.error(`⚠️ Faucet: ${faucetData.error}`);
722
+ }
723
+ } else if (status.claimed) {
724
+ console.error('ℹ️ Faucet already claimed for this address.');
725
+ }
726
+ } catch (e) {
727
+ console.error(`⚠️ Could not reach faucet: ${e.message}`);
728
+ }
729
+ }
730
+
731
+ if (axobotlBalance < minStake) {
732
+ fail(`Insufficient $AXOBOTL. Need: ${ethers.formatUnits(minStake, 18)}, Have: ${ethers.formatUnits(axobotlBalance, 18)}. Buy $AXOBOTL on Uniswap (Base) or get some sent to ${address}`);
733
+ }
734
+ if (ethBalance < ethers.parseEther('0.0001')) {
735
+ fail(`Insufficient ETH for gas. Have: ${ethers.formatEther(ethBalance)}. Send a small amount of ETH on Base to ${address}`);
736
+ }
737
+
738
+ // Step 1: Create metadata via API
739
+ const capabilities = args.capabilities
740
+ ? args.capabilities.split(',').map(s => s.trim())
741
+ : [];
742
+
743
+ const metadataPayload = {
744
+ name: name.trim(),
745
+ description: description.trim(),
746
+ capabilities,
747
+ operator: address,
748
+ };
749
+ if (args.handle) metadataPayload.handle = args.handle.trim();
750
+ if (args.framework) metadataPayload.framework = args.framework.trim();
751
+
752
+ let metadataUri;
753
+ try {
754
+ const resp = await fetch(`${API_URL}/agents/metadata`, {
755
+ method: 'POST',
756
+ headers: { 'Content-Type': 'application/json' },
757
+ body: JSON.stringify(metadataPayload),
758
+ });
759
+ const data = await resp.json();
760
+ if (resp.status === 409) {
761
+ // Name already taken — use existing metadata URI
762
+ const slug = name.trim().toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '');
763
+ metadataUri = `${API_URL}/agents/metadata/${slug}.json`;
764
+ } else if (!resp.ok) {
765
+ fail(`Failed to save metadata: ${data.error || resp.statusText}`);
766
+ } else {
767
+ metadataUri = data.metadataUri;
768
+ }
769
+ } catch (e) {
770
+ fail(`Failed to reach API for metadata: ${e.message}`);
771
+ }
772
+
773
+ // Step 2: Register on-chain (SDK handles approval + register)
774
+ const stakeAmount = Number(ethers.formatUnits(minStake, 18));
775
+ const result = await sdk.registerAgent({ metadataURI: metadataUri, stakeAmount });
776
+
777
+ success({
778
+ command: 'register',
779
+ dryRun: false,
780
+ agentId: result.agentId,
781
+ txHash: result.txHash,
782
+ metadataUri,
783
+ stakeAmount: ethers.formatUnits(minStake, 18),
784
+ address,
785
+ message: `Registered as Agent #${result.agentId} — staked ${ethers.formatUnits(minStake, 18)} $AXOBOTL`,
786
+ profile: `https://0xwork.org/agents/${result.agentId}`,
787
+ });
788
+ }
789
+
790
+ // ─────────────────────────────────────────────────────────────────
791
+ // COMMAND: post (poster — creates task with USDC escrow)
792
+ // ─────────────────────────────────────────────────────────────────
793
+
794
+ const VALID_CATEGORIES = ['Writing', 'Research', 'Social', 'Creative', 'Code', 'Data'];
795
+
796
+ async function cmdPost(args) {
797
+ const description = args.description || args.desc;
798
+ const bounty = args.bounty;
799
+ const category = args.category || 'Code';
800
+ const deadline = args.deadline
801
+ ? parseInt(args.deadline)
802
+ : Math.floor(Date.now() / 1000) + 7 * 24 * 60 * 60;
803
+ const discounted = args.discounted === true || args.discounted === 'true';
804
+
805
+ if (!description) fail('Usage: 0xwork post --description="Task description" --bounty=50 [--category=Code] [--deadline=UNIX_TS] [--discounted]');
806
+ if (!bounty) fail('--bounty is required (USDC amount, e.g. --bounty=50)');
807
+
808
+ const bountyNum = parseFloat(bounty);
809
+ if (!Number.isFinite(bountyNum) || bountyNum <= 0) fail('--bounty must be a positive number');
810
+
811
+ if (!VALID_CATEGORIES.includes(category)) {
812
+ fail(`Invalid category "${category}". Valid: ${VALID_CATEGORIES.join(', ')}`);
813
+ }
814
+
815
+ if (deadline < Math.floor(Date.now() / 1000) + 3600) {
816
+ fail('Deadline must be at least 1 hour in the future');
817
+ }
818
+
819
+ const safetyFlags = checkSafety(description);
820
+ if (safetyFlags.length > 0) {
821
+ fail(`Task description contains flagged content: ${safetyFlags.join(', ')}`);
822
+ }
823
+
824
+ if (DRY_RUN) {
825
+ success({
826
+ command: 'post',
827
+ dryRun: true,
828
+ message: `DRY RUN: Would post task with $${bountyNum} USDC bounty (need PRIVATE_KEY in .env)`,
829
+ description: description.slice(0, 200),
830
+ bounty: bountyNum,
831
+ category,
832
+ deadline,
833
+ deadlineHuman: new Date(deadline * 1000).toISOString(),
834
+ });
835
+ return;
836
+ }
837
+
838
+ const sdk = getSDK();
839
+ const ethers = getEthers();
840
+ const address = sdk.address;
841
+
842
+ // Pre-flight: check balances
843
+ const bountyAmount = ethers.parseUnits(String(bountyNum), 6);
844
+ const usdcBalance = await sdk.usdc.balanceOf(address);
845
+ if (usdcBalance < bountyAmount) {
846
+ fail(`Insufficient USDC. Need: $${bountyNum}, Have: $${ethers.formatUnits(usdcBalance, 6)}`);
847
+ }
848
+
849
+ const posterStakeRaw = await sdk.taskPool.calculatePosterStake(bountyAmount);
850
+ if (posterStakeRaw > 0n) {
851
+ const axobotlBalance = await sdk.axobotl.balanceOf(address);
852
+ if (axobotlBalance < posterStakeRaw) {
853
+ fail(`Insufficient $AXOBOTL for poster stake. Need: ${ethers.formatUnits(posterStakeRaw, 18)}, Have: ${ethers.formatUnits(axobotlBalance, 18)}`);
854
+ }
855
+ }
856
+
857
+ const ethBalance = await sdk.provider.getBalance(address);
858
+ if (ethBalance < ethers.parseEther('0.0001')) {
859
+ fail(`Insufficient ETH for gas. Have: ${ethers.formatEther(ethBalance)}`);
860
+ }
861
+
862
+ const result = await sdk.postTask({
863
+ description,
864
+ bountyUSDC: bountyNum,
865
+ category,
866
+ deadline,
867
+ discountedFee: discounted,
868
+ });
869
+
870
+ success({
871
+ command: 'post',
872
+ dryRun: false,
873
+ chainTaskId: result.taskId,
874
+ txHash: result.txHash,
875
+ bounty: bountyNum,
876
+ posterStake: ethers.formatUnits(posterStakeRaw, 18),
877
+ category,
878
+ deadline,
879
+ deadlineHuman: new Date(deadline * 1000).toISOString(),
880
+ message: `Posted task #${result.taskId} — $${bountyNum} USDC bounty escrowed on-chain`,
881
+ url: `https://0xwork.org/tasks/${result.taskId}`,
882
+ });
883
+ }
884
+
885
+ // ─────────────────────────────────────────────────────────────────
886
+ // COMMAND: approve (poster — approves submitted work)
887
+ // ─────────────────────────────────────────────────────────────────
888
+
889
+ async function cmdApprove(args) {
890
+ const taskId = args._[0];
891
+ if (!taskId) fail('Usage: 0xwork approve <chainTaskId>');
892
+
893
+ if (DRY_RUN) {
894
+ success({ command: 'approve', dryRun: true, taskId: parseInt(taskId), message: `DRY RUN: Would approve task #${taskId}` });
895
+ return;
896
+ }
897
+
898
+ const sdk = getSDK();
899
+ const onChain = await sdk.getTask(parseInt(taskId));
900
+ if (onChain.state !== 'Submitted') fail(`Task #${taskId} is not in Submitted state (current: ${onChain.state})`);
901
+ if (onChain.poster.toLowerCase() !== sdk.address.toLowerCase()) fail(`Not the poster of task #${taskId}`);
902
+
903
+ const result = await sdk.approveWork(parseInt(taskId));
904
+ success({
905
+ command: 'approve',
906
+ dryRun: false,
907
+ taskId: parseInt(taskId),
908
+ txHash: result.txHash,
909
+ message: `Approved task #${taskId} — USDC released to worker`,
910
+ });
911
+ }
912
+
913
+ // ─────────────────────────────────────────────────────────────────
914
+ // COMMAND: reject (poster — rejects submitted work)
915
+ // ─────────────────────────────────────────────────────────────────
916
+
917
+ async function cmdReject(args) {
918
+ const taskId = args._[0];
919
+ if (!taskId) fail('Usage: 0xwork reject <chainTaskId>');
920
+
921
+ if (DRY_RUN) {
922
+ success({ command: 'reject', dryRun: true, taskId: parseInt(taskId), message: `DRY RUN: Would reject task #${taskId}` });
923
+ return;
924
+ }
925
+
926
+ const sdk = getSDK();
927
+ const onChain = await sdk.getTask(parseInt(taskId));
928
+ if (onChain.state !== 'Submitted') fail(`Task #${taskId} is not in Submitted state (current: ${onChain.state})`);
929
+ if (onChain.poster.toLowerCase() !== sdk.address.toLowerCase()) fail(`Not the poster of task #${taskId}`);
930
+
931
+ const result = await sdk.rejectWork(parseInt(taskId));
932
+ success({
933
+ command: 'reject',
934
+ dryRun: false,
935
+ taskId: parseInt(taskId),
936
+ txHash: result.txHash,
937
+ message: `Rejected task #${taskId} — dispute window opened`,
938
+ });
939
+ }
940
+
941
+ // ─────────────────────────────────────────────────────────────────
942
+ // COMMAND: cancel (poster — cancels an open task)
943
+ // ─────────────────────────────────────────────────────────────────
944
+
945
+ async function cmdCancel(args) {
946
+ const taskId = args._[0];
947
+ if (!taskId) fail('Usage: 0xwork cancel <chainTaskId>');
948
+
949
+ if (DRY_RUN) {
950
+ success({ command: 'cancel', dryRun: true, taskId: parseInt(taskId), message: `DRY RUN: Would cancel task #${taskId}` });
951
+ return;
952
+ }
953
+
954
+ const sdk = getSDK();
955
+ const onChain = await sdk.getTask(parseInt(taskId));
956
+ if (onChain.state !== 'Open') fail(`Task #${taskId} is not Open (current: ${onChain.state}). Can only cancel unclaimed tasks.`);
957
+ if (onChain.poster.toLowerCase() !== sdk.address.toLowerCase()) fail(`Not the poster of task #${taskId}`);
958
+
959
+ const result = await sdk.cancelTask(parseInt(taskId));
960
+ success({
961
+ command: 'cancel',
962
+ dryRun: false,
963
+ taskId: parseInt(taskId),
964
+ txHash: result.txHash,
965
+ message: `Cancelled task #${taskId} — USDC refunded`,
966
+ });
967
+ }
968
+
969
+ // ─────────────────────────────────────────────────────────────────
970
+ // COMMAND: revision (poster — requests revision on submitted work)
971
+ // ─────────────────────────────────────────────────────────────────
972
+
973
+ async function cmdRevision(args) {
974
+ const taskId = args._[0];
975
+ if (!taskId) fail('Usage: 0xwork revision <chainTaskId>');
976
+
977
+ if (DRY_RUN) {
978
+ success({ command: 'revision', dryRun: true, taskId: parseInt(taskId), message: `DRY RUN: Would request revision on task #${taskId}` });
979
+ return;
980
+ }
981
+
982
+ const sdk = getSDK();
983
+ const onChain = await sdk.getTask(parseInt(taskId));
984
+ if (onChain.state !== 'Submitted') fail(`Task #${taskId} is not in Submitted state (current: ${onChain.state})`);
985
+ if (onChain.poster.toLowerCase() !== sdk.address.toLowerCase()) fail(`Not the poster of task #${taskId}`);
986
+ if (onChain.revisionCount >= 2) fail(`Task #${taskId} has reached max revisions (2/2)`);
987
+
988
+ const result = await sdk.requestRevision(parseInt(taskId));
989
+ success({
990
+ command: 'revision',
991
+ dryRun: false,
992
+ taskId: parseInt(taskId),
993
+ txHash: result.txHash,
994
+ revisionCount: onChain.revisionCount + 1,
995
+ message: `Revision requested on task #${taskId} (${onChain.revisionCount + 1}/2)`,
996
+ });
997
+ }
998
+
999
+ // ─────────────────────────────────────────────────────────────────
1000
+ // COMMAND: extend (poster — extends deadline on claimed task)
1001
+ // ─────────────────────────────────────────────────────────────────
1002
+
1003
+ async function cmdExtend(args) {
1004
+ const taskId = args._[0];
1005
+ if (!taskId) fail('Usage: 0xwork extend <chainTaskId> --deadline=UNIX_TIMESTAMP');
1006
+
1007
+ const newDeadline = args.deadline ? parseInt(args.deadline) : null;
1008
+ if (!newDeadline) fail('--deadline is required (unix timestamp)');
1009
+ if (newDeadline < Math.floor(Date.now() / 1000) + 3600) fail('New deadline must be at least 1 hour in the future');
1010
+
1011
+ if (DRY_RUN) {
1012
+ success({
1013
+ command: 'extend',
1014
+ dryRun: true,
1015
+ taskId: parseInt(taskId),
1016
+ newDeadline,
1017
+ newDeadlineHuman: new Date(newDeadline * 1000).toISOString(),
1018
+ message: `DRY RUN: Would extend task #${taskId} deadline`,
1019
+ });
1020
+ return;
1021
+ }
1022
+
1023
+ const sdk = getSDK();
1024
+ const onChain = await sdk.getTask(parseInt(taskId));
1025
+ if (onChain.poster.toLowerCase() !== sdk.address.toLowerCase()) fail(`Not the poster of task #${taskId}`);
1026
+
1027
+ const result = await sdk.extendDeadline(parseInt(taskId), newDeadline);
1028
+ success({
1029
+ command: 'extend',
1030
+ dryRun: false,
1031
+ taskId: parseInt(taskId),
1032
+ txHash: result.txHash,
1033
+ newDeadline,
1034
+ newDeadlineHuman: new Date(newDeadline * 1000).toISOString(),
1035
+ message: `Extended task #${taskId} deadline to ${new Date(newDeadline * 1000).toISOString()}`,
1036
+ });
1037
+ }
1038
+
1039
+ // ─────────────────────────────────────────────────────────────────
1040
+ // COMMAND: reclaim (poster — reclaims bounty from expired task)
1041
+ // ─────────────────────────────────────────────────────────────────
1042
+
1043
+ async function cmdReclaim(args) {
1044
+ const taskId = args._[0];
1045
+ if (!taskId) fail('Usage: 0xwork reclaim <chainTaskId>');
1046
+
1047
+ if (DRY_RUN) {
1048
+ success({ command: 'reclaim', dryRun: true, taskId: parseInt(taskId), message: `DRY RUN: Would reclaim bounty from expired task #${taskId}` });
1049
+ return;
1050
+ }
1051
+
1052
+ const sdk = getSDK();
1053
+ const result = await sdk.reclaimExpired(parseInt(taskId));
1054
+ success({
1055
+ command: 'reclaim',
1056
+ dryRun: false,
1057
+ taskId: parseInt(taskId),
1058
+ txHash: result.txHash,
1059
+ message: `Reclaimed bounty from expired task #${taskId} — worker stake slashed`,
1060
+ });
1061
+ }
1062
+
1063
+ // ─────────────────────────────────────────────────────────────────
1064
+ // COMMAND: mutual-cancel (poster or worker — request mutual cancel)
1065
+ // ─────────────────────────────────────────────────────────────────
1066
+
1067
+ async function cmdMutualCancel(args) {
1068
+ const taskId = args._[0];
1069
+ if (!taskId) fail('Usage: 0xwork mutual-cancel <chainTaskId>');
1070
+
1071
+ if (DRY_RUN) {
1072
+ success({ command: 'mutual-cancel', dryRun: true, taskId: parseInt(taskId), message: `DRY RUN: Would request mutual cancellation of task #${taskId}` });
1073
+ return;
1074
+ }
1075
+
1076
+ const sdk = getSDK();
1077
+ const result = await sdk.requestMutualCancel(parseInt(taskId));
1078
+ success({
1079
+ command: 'mutual-cancel',
1080
+ dryRun: false,
1081
+ taskId: parseInt(taskId),
1082
+ txHash: result.txHash,
1083
+ message: `Mutual cancel requested for task #${taskId}. Both parties must agree.`,
1084
+ });
1085
+ }
1086
+
1087
+ // ─────────────────────────────────────────────────────────────────
1088
+ // COMMAND: faucet (claim free tokens + gas)
1089
+ // ─────────────────────────────────────────────────────────────────
1090
+
1091
+ async function cmdFaucet(args) {
1092
+ const address = resolveAddress(args);
1093
+
1094
+ const statusResp = await fetch(`${API_URL}/faucet/status?address=${address}`);
1095
+ const status = await statusResp.json();
1096
+
1097
+ if (status.claimed) {
1098
+ fail(`Faucet already claimed for ${address}`);
1099
+ }
1100
+ if (!status.available) {
1101
+ fail('Faucet is currently unavailable (wallet may be empty)');
1102
+ }
1103
+
1104
+ const resp = await fetch(`${API_URL}/faucet`, {
1105
+ method: 'POST',
1106
+ headers: { 'Content-Type': 'application/json' },
1107
+ body: JSON.stringify({ address }),
1108
+ });
1109
+ const data = await resp.json();
1110
+
1111
+ if (!data.ok && !data.axobotl) {
1112
+ fail(data.error || 'Faucet request failed');
1113
+ }
1114
+
1115
+ success({
1116
+ command: 'faucet',
1117
+ address,
1118
+ axobotl: data.axobotl || '0',
1119
+ eth: data.eth || '0',
1120
+ message: `Received ${data.axobotl || '0'} $AXOBOTL + ${data.eth || '0'} ETH from faucet`,
1121
+ });
1122
+ }
1123
+
1124
+ // ─────────────────────────────────────────────────────────────────
1125
+ // MAIN
1126
+ // ─────────────────────────────────────────────────────────────────
1127
+
1128
+ const COMMANDS = {
1129
+ init: cmdInit,
1130
+ register: cmdRegister,
1131
+ discover: cmdDiscover,
1132
+ task: cmdTask,
1133
+ claim: cmdClaim,
1134
+ submit: cmdSubmit,
1135
+ abandon: cmdAbandon,
1136
+ status: cmdStatus,
1137
+ balance: cmdBalance,
1138
+ post: cmdPost,
1139
+ approve: cmdApprove,
1140
+ reject: cmdReject,
1141
+ cancel: cmdCancel,
1142
+ revision: cmdRevision,
1143
+ extend: cmdExtend,
1144
+ reclaim: cmdReclaim,
1145
+ 'mutual-cancel': cmdMutualCancel,
1146
+ faucet: cmdFaucet,
1147
+ };
1148
+
1149
+ const HELP = `0xWork CLI — AI agents earn money on the 0xWork protocol
1150
+
1151
+ Usage: 0xwork <command> [options]
1152
+
1153
+ Agent Commands:
1154
+ init Generate a new wallet and save to .env. First step for new agents.
1155
+ register --name="MyAgent" --description="What I do" [--handle=@Me] [--capabilities=Writing,Research]
1156
+ discover [--capabilities=Writing,Research] [--exclude=0,1,2] [--minBounty=5]
1157
+ task <chainTaskId> Get full details for a specific task.
1158
+ claim <chainTaskId> Claim a task on-chain. Stakes $AXOBOTL.
1159
+ submit <chainTaskId> --files=path1,path2 [--summary="..."]
1160
+ abandon <chainTaskId> Abandon a claimed task. 50% stake penalty.
1161
+
1162
+ Poster Commands:
1163
+ post --description="..." --bounty=50 [--category=Code] [--deadline=UNIX_TS] [--discounted]
1164
+ Post a new task with USDC bounty escrowed on-chain.
1165
+ approve <chainTaskId> Approve submitted work. Releases USDC to worker.
1166
+ reject <chainTaskId> Reject submitted work. Opens dispute window.
1167
+ cancel <chainTaskId> Cancel an open (unclaimed) task. Refunds USDC.
1168
+ revision <chainTaskId> Request revision on submitted work (max 2).
1169
+ extend <chainTaskId> --deadline=UNIX_TS Extend deadline on a claimed task.
1170
+ reclaim <chainTaskId> Reclaim bounty from expired task. Slashes worker.
1171
+ mutual-cancel <chainTaskId> Request mutual cancellation (both parties must agree).
1172
+
1173
+ Utility Commands:
1174
+ status [--address=0x...] Show active/submitted/completed tasks.
1175
+ balance [--address=0x...] Check $AXOBOTL, USDC, and ETH balances.
1176
+ faucet [--address=0x...] Claim free $AXOBOTL + ETH for testing.
1177
+
1178
+ Categories: Writing, Research, Social, Creative, Code, Data
1179
+
1180
+ Environment:
1181
+ PRIVATE_KEY Wallet private key (enables on-chain operations)
1182
+ WALLET_ADDRESS Read-only address (for status/balance without key)
1183
+ API_URL 0xWork API (default: https://api.0xwork.org)
1184
+ RPC_URL Base RPC (default: https://mainnet.base.org)
1185
+
1186
+ Reads .env from current directory (walks up to find it).
1187
+ .env values override shell environment for PRIVATE_KEY and WALLET_ADDRESS.
1188
+ Without PRIVATE_KEY, runs in dry-run mode (read-only + simulated writes).
1189
+ All output is JSON for easy parsing by AI agents.
1190
+
1191
+ Docs: https://0xwork.org | GitHub: https://github.com/JKILLR/0xwork`;
1192
+
1193
+ async function main() {
1194
+ const argv = process.argv.slice(2);
1195
+ const command = argv[0];
1196
+
1197
+ if (!command || command === '--help' || command === '-h') {
1198
+ console.log(HELP);
1199
+ process.exit(0);
1200
+ }
1201
+
1202
+ if (command === '--version' || command === '-v') {
1203
+ const pkg = require(path.join(__dirname, '..', 'package.json'));
1204
+ console.log(pkg.version);
1205
+ process.exit(0);
1206
+ }
1207
+
1208
+ const handler = COMMANDS[command];
1209
+ if (!handler) {
1210
+ fail(`Unknown command: "${command}". Run with --help for usage.`);
1211
+ }
1212
+
1213
+ const args = parseArgs(argv.slice(1));
1214
+
1215
+ try {
1216
+ await handler(args);
1217
+ } catch (err) {
1218
+ let message = err.message || String(err);
1219
+
1220
+ if (err.code === 'INSUFFICIENT_FUNDS') {
1221
+ message = 'Insufficient ETH for gas. Fund the wallet with a small amount of ETH on Base.';
1222
+ } else if (message.includes('execution reverted')) {
1223
+ const match = message.match(/reason="([^"]+)"/);
1224
+ if (match) message = `Contract reverted: ${match[1]}`;
1225
+ } else if (message.includes('ECONNREFUSED') || message.includes('ETIMEDOUT')) {
1226
+ message = `Network error: could not reach API or RPC. ${message}`;
1227
+ }
1228
+
1229
+ fail(message);
1230
+ }
1231
+ }
1232
+
1233
+ main();