@0xwork/cli 0.2.0 → 1.0.1

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,196 @@
1
+ 'use strict';
2
+
3
+ const chalk = require('chalk');
4
+ const Table = require('cli-table3');
5
+ const config = require('../config');
6
+ const { getSDK, getEthers, isDryRun, normalizeError } = require('../sdk');
7
+ const { success, fail, header, TABLE_CHARS } = require('../output');
8
+ const { createSpinner } = require('../spinner');
9
+ const { getAxobotlPrice, formatAxobotlUsd } = require('../price');
10
+ const { fmtBounty, fmtDeadline, fmtCategory, fmtDesc, fmtAxobotl } = require('../format');
11
+ const { checkSafety } = require('../safety');
12
+ const { fetchWithTimeout } = require('../http');
13
+
14
+ function register(program) {
15
+ program
16
+ .command('discover')
17
+ .description('Find open tasks matching your capabilities')
18
+ .option('--capabilities <list>', 'Filter by category (comma-separated)', 'Writing,Research,Social,Creative,Code,Data')
19
+ .option('--min-bounty <amount>', 'Minimum bounty in USDC')
20
+ .option('--max-bounty <amount>', 'Maximum bounty in USDC')
21
+ .option('--limit <n>', 'Number of results', '20')
22
+ .option('--exclude <ids>', 'Task IDs to skip (comma-separated)')
23
+ .option('--include-locked', 'Show tasks with preferred agent locks')
24
+ .action(async (opts) => {
25
+ try {
26
+ await run(opts);
27
+ } catch (err) {
28
+ fail(normalizeError(err));
29
+ }
30
+ });
31
+ }
32
+
33
+ async function run(opts) {
34
+ const spinner = createSpinner('Searching for tasks…');
35
+ spinner.start();
36
+
37
+ const capabilities = opts.capabilities.split(',').map(s => s.trim());
38
+ const excludeIds = opts.exclude
39
+ ? new Set(opts.exclude.split(',').map(s => s.trim()))
40
+ : new Set();
41
+ const minBounty = opts.minBounty ? parseFloat(opts.minBounty) : undefined;
42
+ const maxBounty = opts.maxBounty ? parseFloat(opts.maxBounty) : undefined;
43
+ const limit = parseInt(opts.limit) || 20;
44
+ const includeLocked = opts.includeLocked === true;
45
+
46
+ // Look up our agent ID for preferred lock filtering
47
+ let myAgentId = null;
48
+ if (!isDryRun()) {
49
+ try {
50
+ const address = getSDK().address;
51
+ const agentResp = await fetchWithTimeout(`${config.API_URL}/agents/${address}`);
52
+ if (agentResp.ok) {
53
+ const agentData = await agentResp.json();
54
+ myAgentId = agentData.agent ? agentData.agent.chain_agent_id : null;
55
+ }
56
+ } catch { /* not registered */ }
57
+ }
58
+
59
+ const allTasks = [];
60
+ const seen = new Set();
61
+ let lockedCount = 0;
62
+
63
+ // Fetch all categories in parallel
64
+ const categoryPromises = capabilities.map(cap => {
65
+ const params = new URLSearchParams({
66
+ status: 'Open',
67
+ category: cap,
68
+ limit: String(limit),
69
+ });
70
+ if (minBounty) params.set('min_bounty', String(minBounty));
71
+ return fetchWithTimeout(`${config.API_URL}/tasks?${params}`)
72
+ .then(resp => resp.ok ? resp.json() : null)
73
+ .catch(() => null);
74
+ });
75
+
76
+ const categoryResults = await Promise.all(categoryPromises);
77
+
78
+ for (const data of categoryResults) {
79
+ if (!data) continue;
80
+ const tasks = data.tasks || data;
81
+ if (!Array.isArray(tasks)) continue;
82
+
83
+ for (const t of tasks) {
84
+ const chainId = String(t.chain_task_id || t.id);
85
+ if (seen.has(chainId)) continue;
86
+ if (excludeIds.has(chainId)) continue;
87
+ seen.add(chainId);
88
+
89
+ // Preferred lock filtering
90
+ const lock = t.preferred_lock;
91
+ if (lock && lock.locked && !includeLocked) {
92
+ if (myAgentId == null || t.preferred_agent_id !== myAgentId) {
93
+ lockedCount++;
94
+ continue;
95
+ }
96
+ }
97
+
98
+ // Max bounty filter
99
+ if (maxBounty && parseFloat(t.bounty_amount) > maxBounty) continue;
100
+
101
+ const taskEntry = {
102
+ chainTaskId: parseInt(chainId),
103
+ dbId: t.id,
104
+ description: t.description,
105
+ category: t.category,
106
+ bounty: t.bounty_amount,
107
+ stakeRaw: t.stake_amount,
108
+ deadline: t.deadline,
109
+ deadlineHuman: new Date(t.deadline * 1000).toISOString(),
110
+ poster: t.poster_address,
111
+ requirements: t.requirements,
112
+ createdAt: t.created_at,
113
+ safetyFlags: checkSafety(t.description),
114
+ };
115
+
116
+ if (lock && lock.locked && myAgentId != null && t.preferred_agent_id === myAgentId) {
117
+ taskEntry.reservedForYou = true;
118
+ taskEntry.lockExpiresAt = lock.expires_at;
119
+ }
120
+
121
+ allTasks.push(taskEntry);
122
+ }
123
+ }
124
+
125
+ allTasks.sort((a, b) => parseFloat(b.bounty) - parseFloat(a.bounty));
126
+
127
+ // Enrich with USD values
128
+ const price = await getAxobotlPrice();
129
+ if (price) {
130
+ const ethers = getEthers();
131
+ for (const t of allTasks) {
132
+ if (t.stakeRaw) {
133
+ const stakeNum = parseFloat(ethers.formatUnits(t.stakeRaw, 18));
134
+ t.stakeUsd = formatAxobotlUsd(stakeNum, price);
135
+ t.stakeFormatted = `${Math.round(stakeNum).toLocaleString()} AXOBOTL`;
136
+ }
137
+ }
138
+ }
139
+
140
+ spinner.stop();
141
+
142
+ const totalBounty = allTasks.reduce((s, t) => s + parseFloat(t.bounty || 0), 0);
143
+
144
+ const data = {
145
+ command: 'discover',
146
+ dryRun: isDryRun(),
147
+ capabilities,
148
+ tasks: allTasks,
149
+ count: allTasks.length,
150
+ ...(lockedCount > 0 ? { lockedTasksHidden: lockedCount } : {}),
151
+ ...(price ? { axobotlPriceUsd: price } : {}),
152
+ };
153
+
154
+ success(data, () => {
155
+ const priceStr = price ? `$AXOBOTL: ${chalk.dim(`$${price.toFixed(4)}`)}` : '';
156
+ header('🔍', `Open Tasks (${allTasks.length} matching)`, priceStr);
157
+
158
+ if (allTasks.length === 0) {
159
+ console.log(chalk.dim(' No matching tasks found.'));
160
+ console.log(chalk.dim(` Capabilities: ${capabilities.join(', ')}`));
161
+ console.log('');
162
+ return;
163
+ }
164
+
165
+ const table = new Table({
166
+ chars: TABLE_CHARS,
167
+ style: { head: ['cyan'], 'padding-left': 1, 'padding-right': 1 },
168
+ head: ['ID', 'Bounty', 'Category', 'Description', 'Deadline'],
169
+ colWidths: [7, 10, 11, 40, 11],
170
+ });
171
+
172
+ for (const t of allTasks.slice(0, limit)) {
173
+ const idStr = t.reservedForYou
174
+ ? chalk.yellow(`#${t.chainTaskId} ★`)
175
+ : chalk.cyan(`#${t.chainTaskId}`);
176
+
177
+ table.push([
178
+ idStr,
179
+ chalk.green(fmtBounty(t.bounty)),
180
+ fmtCategory(t.category),
181
+ fmtDesc(t.description),
182
+ fmtDeadline(t.deadline),
183
+ ]);
184
+ }
185
+
186
+ console.log(' ' + table.toString().split('\n').join('\n '));
187
+ console.log('');
188
+ console.log(chalk.dim(` ${allTasks.length} tasks · ${chalk.green(fmtBounty(totalBounty))} total bounties · ${capabilities.join(', ')}`));
189
+ if (lockedCount > 0) {
190
+ console.log(chalk.dim(` ${lockedCount} tasks hidden (reserved for other agents)`));
191
+ }
192
+ console.log('');
193
+ });
194
+ }
195
+
196
+ module.exports = { register };
@@ -0,0 +1,89 @@
1
+ 'use strict';
2
+
3
+ const chalk = require('chalk');
4
+ const { requireSDK, isDryRun, normalizeError } = require('../sdk');
5
+ const { success, fail, header, keyValue, blank, hint } = require('../output');
6
+ const { createSpinner } = require('../spinner');
7
+ const { fmtDeadline } = require('../format');
8
+
9
+ function register(program) {
10
+ program
11
+ .command('extend <chainTaskId>')
12
+ .description("Extend a claimed task's deadline")
13
+ .option('--by <duration>', 'Extend by duration: 3d, 12h, 30m')
14
+ .option('--until <date>', 'Extend until ISO date: 2026-03-15')
15
+ .action(async (chainTaskId, opts) => {
16
+ try {
17
+ await run(chainTaskId, opts);
18
+ } catch (err) {
19
+ fail(normalizeError(err));
20
+ }
21
+ });
22
+ }
23
+
24
+ async function run(taskId, opts) {
25
+ if (!opts.by && !opts.until) {
26
+ fail('Specify --by=<duration> (e.g. 3d, 12h) or --until=<date> (e.g. 2026-03-15)');
27
+ }
28
+
29
+ if (isDryRun()) {
30
+ return success({
31
+ command: 'extend', dryRun: true, taskId: parseInt(taskId),
32
+ message: `DRY RUN: Would extend deadline on task #${taskId}`,
33
+ }, () => {
34
+ header('⏰', `Extend #${taskId} — Dry Run`); hint('Set PRIVATE_KEY.'); blank();
35
+ });
36
+ }
37
+
38
+ const sdk = requireSDK();
39
+ const address = sdk.address;
40
+
41
+ const s1 = createSpinner('Checking task…');
42
+ s1.start();
43
+ const onChain = await sdk.getTask(parseInt(taskId));
44
+ if (onChain.state !== 'Claimed') {
45
+ s1.fail(`Task is ${onChain.state}`); fail(`Expected Claimed state`);
46
+ }
47
+ if (onChain.poster.toLowerCase() !== address.toLowerCase()) {
48
+ s1.fail('Not your task'); fail('Only the poster can extend deadline.');
49
+ }
50
+ s1.succeed('Verified');
51
+
52
+ let newDeadline;
53
+ if (opts.until) {
54
+ const parsed = Date.parse(opts.until);
55
+ if (isNaN(parsed)) fail(`Invalid date: "${opts.until}". Use ISO format.`);
56
+ newDeadline = Math.floor(parsed / 1000);
57
+ } else {
58
+ const match = opts.by.match(/^(\d+)([dhm])$/);
59
+ if (!match) fail('Invalid duration. Use: 3d, 12h, 30m');
60
+ const val = parseInt(match[1]);
61
+ if (val <= 0) fail('Duration must be positive (e.g. 3d, 12h, 30m)');
62
+ const mult = { d: 86400, h: 3600, m: 60 };
63
+ newDeadline = onChain.deadline + val * mult[match[2]];
64
+ }
65
+
66
+ if (newDeadline <= onChain.deadline) {
67
+ fail(`New deadline must be later than current (${new Date(onChain.deadline * 1000).toISOString()})`);
68
+ }
69
+
70
+ const s2 = createSpinner('Extending deadline…');
71
+ s2.start();
72
+ const result = await sdk.extendDeadline(parseInt(taskId), newDeadline);
73
+ s2.succeed('Extended');
74
+
75
+ success({
76
+ command: 'extend', dryRun: false, taskId: parseInt(taskId),
77
+ txHash: result.txHash,
78
+ oldDeadline: new Date(onChain.deadline * 1000).toISOString(),
79
+ newDeadline: new Date(newDeadline * 1000).toISOString(),
80
+ message: `Extended task #${taskId} deadline`,
81
+ }, () => {
82
+ header('✔', `Task #${taskId} Deadline Extended`);
83
+ keyValue('Old deadline', fmtDeadline(onChain.deadline, true));
84
+ keyValue('New deadline', fmtDeadline(newDeadline, true));
85
+ blank(); hint(`tx: ${result.txHash}`); blank();
86
+ });
87
+ }
88
+
89
+ module.exports = { register };
@@ -0,0 +1,92 @@
1
+ 'use strict';
2
+
3
+ const chalk = require('chalk');
4
+ const config = require('../config');
5
+ const { resolveAddress, normalizeError } = require('../sdk');
6
+ const { success, fail, header, keyValue, blank, hint } = require('../output');
7
+ const { createSpinner } = require('../spinner');
8
+ const { truncAddr } = require('../format');
9
+ const { fetchWithTimeout } = require('../http');
10
+
11
+ function register(program) {
12
+ program
13
+ .command('faucet')
14
+ .description('Request $AXOBOTL + ETH from the faucet (one-time per address)')
15
+ .option('--address <addr>', 'Wallet address (defaults to .env)')
16
+ .action(async (opts) => {
17
+ try {
18
+ await run(opts);
19
+ } catch (err) {
20
+ fail(normalizeError(err));
21
+ }
22
+ });
23
+ }
24
+
25
+ async function run(opts) {
26
+ const address = resolveAddress(opts.address);
27
+
28
+ const s1 = createSpinner('Checking faucet eligibility…');
29
+ s1.start();
30
+
31
+ let status;
32
+ try {
33
+ const resp = await fetchWithTimeout(`${config.API_URL}/faucet/status?address=${address}`);
34
+ if (!resp.ok) { s1.stop(); fail(`Faucet API error: ${resp.status}`); }
35
+ status = await resp.json();
36
+ } catch (e) {
37
+ s1.fail('Faucet unreachable');
38
+ fail(`Cannot reach faucet: ${e.message}`);
39
+ }
40
+
41
+ if (!status.available) {
42
+ s1.fail('Unavailable');
43
+ fail('Faucet is currently unavailable.');
44
+ }
45
+ if (status.claimed) {
46
+ s1.fail('Already claimed');
47
+ fail(`Faucet already claimed for ${truncAddr(address)}. Each address can only claim once.`);
48
+ }
49
+
50
+ s1.succeed('Eligible');
51
+
52
+ const s2 = createSpinner('Requesting tokens…');
53
+ s2.start();
54
+
55
+ let result;
56
+ try {
57
+ const resp = await fetchWithTimeout(`${config.API_URL}/faucet`, {
58
+ method: 'POST',
59
+ headers: { 'Content-Type': 'application/json' },
60
+ body: JSON.stringify({ address }),
61
+ });
62
+ result = await resp.json();
63
+ if (!resp.ok || !result.ok) {
64
+ s2.fail('Failed');
65
+ fail(result.error || `Faucet request failed: ${resp.status}`);
66
+ }
67
+ } catch (e) {
68
+ s2.fail('Failed');
69
+ fail(`Faucet request failed: ${e.message}`);
70
+ }
71
+
72
+ s2.succeed('Tokens sent');
73
+
74
+ success({
75
+ command: 'faucet',
76
+ address,
77
+ axobotl: result.axobotl,
78
+ eth: result.eth,
79
+ txHash: result.txHash || null,
80
+ message: `Faucet: ${result.axobotl || '?'} $AXOBOTL + ${result.eth || '?'} ETH → ${address}`,
81
+ }, () => {
82
+ header('💧', 'Faucet Drip Sent');
83
+ keyValue('$AXOBOTL', chalk.green(result.axobotl || '50,000'));
84
+ keyValue('ETH', result.eth || '0.0005');
85
+ keyValue('To', truncAddr(address));
86
+ blank();
87
+ hint(`Next: ${chalk.cyan('0xwork register --name="..." --description="..."')}`);
88
+ blank();
89
+ });
90
+ }
91
+
92
+ module.exports = { register };
@@ -0,0 +1,78 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const chalk = require('chalk');
6
+ const { getEthers, normalizeError } = require('../sdk');
7
+ const config = require('../config');
8
+ const { success, fail, header, keyValue, blank, hint } = require('../output');
9
+
10
+ function register(program) {
11
+ program
12
+ .command('init')
13
+ .description('Generate a new wallet and save to .env')
14
+ .action(async () => {
15
+ try {
16
+ await run();
17
+ } catch (err) {
18
+ fail(normalizeError(err));
19
+ }
20
+ });
21
+ }
22
+
23
+ async function run() {
24
+ if (config.PRIVATE_KEY) {
25
+ const ethers = getEthers();
26
+ const wallet = new ethers.Wallet(config.PRIVATE_KEY);
27
+ fail(`Wallet already configured (${wallet.address}). Delete PRIVATE_KEY from .env to generate a new one.`);
28
+ }
29
+
30
+ const ethers = getEthers();
31
+ const wallet = ethers.Wallet.createRandom();
32
+ const envPath = path.join(process.cwd(), '.env');
33
+
34
+ const envLines = [
35
+ '',
36
+ '# 0xWork Agent Wallet (generated by 0xwork init)',
37
+ `PRIVATE_KEY=${wallet.privateKey}`,
38
+ `WALLET_ADDRESS=${wallet.address}`,
39
+ '',
40
+ '# 0xWork Configuration',
41
+ 'API_URL=https://api.0xwork.org',
42
+ 'RPC_URL=https://base-mainnet.public.blastapi.io',
43
+ '',
44
+ ];
45
+
46
+ if (fs.existsSync(envPath)) {
47
+ fs.appendFileSync(envPath, envLines.join('\n'));
48
+ } else {
49
+ fs.writeFileSync(envPath, envLines.join('\n'));
50
+ }
51
+
52
+ const data = {
53
+ command: 'init',
54
+ address: wallet.address,
55
+ envFile: envPath,
56
+ message: 'Wallet created and saved to .env',
57
+ nextSteps: [
58
+ `Fund this address on Base: ${wallet.address}`,
59
+ 'Need: ~0.001 ETH for gas + 10,000 $AXOBOTL for registration stake',
60
+ '$AXOBOTL contract: 0x12cfb53c685Ee7e3F8234d60f20478A1739Ecba3 (Base)',
61
+ 'Then: 0xwork register --name="MyAgent" --description="What I do"',
62
+ ],
63
+ };
64
+
65
+ success(data, () => {
66
+ header('🔑', 'Wallet Created');
67
+ keyValue('Address', chalk.cyan(wallet.address));
68
+ keyValue('Saved to', chalk.dim(envPath));
69
+ blank();
70
+ hint('Next steps:');
71
+ hint(` 1. Fund on Base: ${chalk.cyan(wallet.address)}`);
72
+ hint(' 2. Need: ~0.001 ETH (gas) + 10,000 $AXOBOTL (stake)');
73
+ hint(' 3. Then: ' + chalk.cyan('0xwork register --name="MyAgent" --description="..."'));
74
+ blank();
75
+ });
76
+ }
77
+
78
+ module.exports = { register };
@@ -0,0 +1,84 @@
1
+ 'use strict';
2
+
3
+ const chalk = require('chalk');
4
+ const { requireSDK, isDryRun, normalizeError } = require('../sdk');
5
+ const { success, fail, header, keyValue, blank, hint } = require('../output');
6
+ const { createSpinner } = require('../spinner');
7
+ const { fmtBounty } = require('../format');
8
+
9
+ function register(program) {
10
+ program
11
+ .command('mutual-cancel <chainTaskId>')
12
+ .description('Request or confirm mutual cancellation (no penalties)')
13
+ .action(async (chainTaskId) => {
14
+ try {
15
+ await run(chainTaskId);
16
+ } catch (err) {
17
+ fail(normalizeError(err));
18
+ }
19
+ });
20
+ }
21
+
22
+ async function run(taskId) {
23
+ if (isDryRun()) {
24
+ return success({
25
+ command: 'mutual-cancel', dryRun: true, taskId: parseInt(taskId),
26
+ message: `DRY RUN: Would request/confirm mutual cancel on task #${taskId}`,
27
+ }, () => {
28
+ header('🤝', `Mutual Cancel #${taskId} — Dry Run`); hint('Set PRIVATE_KEY.'); blank();
29
+ });
30
+ }
31
+
32
+ const sdk = requireSDK();
33
+ const address = sdk.address;
34
+
35
+ const s1 = createSpinner('Checking task…');
36
+ s1.start();
37
+ const onChain = await sdk.getTask(parseInt(taskId));
38
+ if (!['Claimed', 'Submitted', 'Disputed'].includes(onChain.state)) {
39
+ s1.fail(`Task is ${onChain.state}`);
40
+ fail(`Cannot mutual-cancel in ${onChain.state} state. Valid: Claimed, Submitted, Disputed.`);
41
+ }
42
+
43
+ const isPoster = onChain.poster.toLowerCase() === address.toLowerCase();
44
+ const isWorker = onChain.worker && onChain.worker.toLowerCase() === address.toLowerCase();
45
+ if (!isPoster && !isWorker) {
46
+ s1.fail('Not authorized'); fail('Only the poster or worker can request mutual cancel.');
47
+ }
48
+
49
+ const isConfirming = onChain.cancelRequestedBy &&
50
+ onChain.cancelRequestedBy.toLowerCase() !== address.toLowerCase();
51
+
52
+ s1.succeed('Verified');
53
+
54
+ const s2 = createSpinner(isConfirming ? 'Confirming mutual cancel…' : 'Requesting mutual cancel…');
55
+ s2.start();
56
+ const result = await sdk.requestMutualCancel(parseInt(taskId));
57
+ s2.succeed(isConfirming ? 'Confirmed' : 'Requested');
58
+
59
+ if (isConfirming) {
60
+ success({
61
+ command: 'mutual-cancel', dryRun: false, taskId: parseInt(taskId),
62
+ txHash: result.txHash, action: 'confirmed', bountyReturned: onChain.bountyAmount,
63
+ message: `Mutual cancel confirmed on task #${taskId}`,
64
+ }, () => {
65
+ header('✔', `Mutual Cancel Confirmed — Task #${taskId}`);
66
+ keyValue('Bounty', chalk.green(`${fmtBounty(onChain.bountyAmount)} returned to poster`));
67
+ hint('All stakes returned. No penalties.');
68
+ blank(); hint(`tx: ${result.txHash}`); blank();
69
+ });
70
+ } else {
71
+ const other = isPoster ? 'worker' : 'poster';
72
+ success({
73
+ command: 'mutual-cancel', dryRun: false, taskId: parseInt(taskId),
74
+ txHash: result.txHash, action: 'requested',
75
+ message: `Mutual cancel requested on task #${taskId}. Waiting for ${other}.`,
76
+ }, () => {
77
+ header('🤝', `Mutual Cancel Requested — Task #${taskId}`);
78
+ hint(`Waiting for the ${other} to confirm.`);
79
+ blank(); hint(`tx: ${result.txHash}`); blank();
80
+ });
81
+ }
82
+ }
83
+
84
+ module.exports = { register };