@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,203 @@
1
+ 'use strict';
2
+
3
+ const chalk = require('chalk');
4
+ const { requireSDK, getEthers, isDryRun, normalizeError } = require('../sdk');
5
+ const { success, fail, header, keyValue, blank, hint, warn } = require('../output');
6
+ const { createSpinner } = require('../spinner');
7
+ const { fmtBounty, fmtDeadline, fmtAxobotl } = require('../format');
8
+ const { fetchWithTimeout } = require('../http');
9
+ const { API_URL } = require('../config');
10
+
11
+ const VALID_CATEGORIES = ['Writing', 'Research', 'Code', 'Creative', 'Data', 'Social'];
12
+
13
+ function register(program) {
14
+ program
15
+ .command('post')
16
+ .description('Post a new task with USDC bounty')
17
+ .requiredOption('--description <text>', 'Task description')
18
+ .requiredOption('--bounty <amount>', 'Bounty in USDC (e.g. 10)')
19
+ .option('--category <cat>', 'Category: Writing, Research, Code, Creative, Data, Social', 'Code')
20
+ .option('--deadline <dur>', 'Deadline: 7d, 24h, 30m (default: 7d)', '7d')
21
+ .option('--skip-duplicate-check', 'Skip the duplicate task check')
22
+ .action(async (opts) => {
23
+ try {
24
+ await run(opts);
25
+ } catch (err) {
26
+ fail(normalizeError(err));
27
+ }
28
+ });
29
+ }
30
+
31
+ async function run(opts) {
32
+ const bountyNum = parseFloat(opts.bounty);
33
+ if (isNaN(bountyNum) || bountyNum <= 0) fail('Bounty must be a positive number');
34
+
35
+ if (!VALID_CATEGORIES.includes(opts.category)) {
36
+ fail(`Invalid category: "${opts.category}"`, { suggestion: `Valid: ${VALID_CATEGORIES.join(', ')}` });
37
+ }
38
+
39
+ // Parse deadline
40
+ let deadlineSecs;
41
+ const match = opts.deadline.match(/^(\d+)([dhm])$/);
42
+ if (match) {
43
+ const val = parseInt(match[1]);
44
+ if (val <= 0) fail('Deadline duration must be positive (e.g. 7d, 24h, 30m)');
45
+ const mult = { d: 86400, h: 3600, m: 60 };
46
+ deadlineSecs = Math.floor(Date.now() / 1000) + val * mult[match[2]];
47
+ } else {
48
+ // Try ISO date
49
+ const parsed = Date.parse(opts.deadline);
50
+ if (!isNaN(parsed)) {
51
+ deadlineSecs = Math.floor(parsed / 1000);
52
+ if (deadlineSecs <= Math.floor(Date.now() / 1000)) {
53
+ fail('Deadline must be in the future');
54
+ }
55
+ } else {
56
+ fail(`Invalid deadline: "${opts.deadline}". Use: 7d, 24h, 30m, or ISO date (2026-03-15)`);
57
+ }
58
+ }
59
+
60
+ if (isDryRun()) {
61
+ return success({
62
+ command: 'post',
63
+ dryRun: true,
64
+ description: opts.description.slice(0, 200),
65
+ bounty: bountyNum,
66
+ category: opts.category,
67
+ deadline: deadlineSecs,
68
+ deadlineHuman: new Date(deadlineSecs * 1000).toISOString(),
69
+ message: `DRY RUN: Would post task with ${fmtBounty(bountyNum)} bounty`,
70
+ }, () => {
71
+ header('📝', 'Post Task — Dry Run');
72
+ keyValue('Bounty', chalk.green.bold(fmtBounty(bountyNum)));
73
+ keyValue('Category', opts.category);
74
+ keyValue('Deadline', fmtDeadline(deadlineSecs, true));
75
+ blank();
76
+ hint('Set PRIVATE_KEY in .env to post tasks.');
77
+ blank();
78
+ });
79
+ }
80
+
81
+ const sdk = requireSDK();
82
+ const ethers = getEthers();
83
+ const address = sdk.address;
84
+
85
+ // ── Duplicate detection ─────────────────────────────────────────────
86
+ if (!opts.skipDuplicateCheck) {
87
+ try {
88
+ const resp = await fetchWithTimeout(`${API_URL}/tasks?poster=${address}&status=Open&limit=50`);
89
+ if (resp.ok) {
90
+ const data = await resp.json();
91
+ const tasks = data.tasks || [];
92
+ const descNorm = opts.description.trim().toLowerCase();
93
+ const dupes = tasks.filter(t =>
94
+ t.description && t.description.trim().toLowerCase() === descNorm
95
+ );
96
+ if (dupes.length > 0) {
97
+ const dupeIds = dupes.map(t => `#${t.chain_task_id}`).join(', ');
98
+ fail(`Duplicate detected — you already have an open task with this exact description (${dupeIds})`, {
99
+ suggestion: 'Use --skip-duplicate-check to post anyway, or cancel the existing task first.',
100
+ });
101
+ }
102
+ // Also warn if posting same category with similar bounty (likely retry)
103
+ const sameCategory = tasks.filter(t =>
104
+ t.category === opts.category &&
105
+ Math.abs(Number(t.bounty_amount) - bountyNum) < 0.01
106
+ );
107
+ if (sameCategory.length >= 3) {
108
+ warn(`You already have ${sameCategory.length} open "${opts.category}" tasks with similar bounties. Use --skip-duplicate-check to post anyway.`);
109
+ fail('Too many similar open tasks — likely a retry loop', {
110
+ suggestion: `Check existing tasks: 0xwork status`,
111
+ });
112
+ }
113
+ }
114
+ } catch { /* API check failed — continue anyway */ }
115
+ }
116
+
117
+ // Pre-flight checks
118
+ const s1 = createSpinner('Checking balances…');
119
+ s1.start();
120
+
121
+ const bountyAmount = ethers.parseUnits(String(bountyNum), 6);
122
+ const usdcBalance = await sdk.usdc.balanceOf(address);
123
+ if (usdcBalance < bountyAmount) {
124
+ s1.fail('Insufficient USDC');
125
+ fail(`Need ${fmtBounty(bountyNum)} USDC`, { suggestion: `Have: ${fmtBounty(ethers.formatUnits(usdcBalance, 6))}. Send USDC on Base to ${address}` });
126
+ }
127
+
128
+ const posterStake = await sdk.taskPool.calculatePosterStake(bountyAmount);
129
+ if (posterStake > 0n) {
130
+ const axobotlBalance = await sdk.axobotl.balanceOf(address);
131
+ if (axobotlBalance < posterStake) {
132
+ s1.fail('Insufficient AXOBOTL');
133
+ fail(`Need ${fmtAxobotl(ethers.formatUnits(posterStake, 18))} $AXOBOTL for poster stake`,
134
+ { suggestion: `Have: ${fmtAxobotl(ethers.formatUnits(axobotlBalance, 18))}` });
135
+ }
136
+ }
137
+
138
+ const ethBalance = await sdk.provider.getBalance(address);
139
+ if (ethBalance < ethers.parseEther('0.0001')) {
140
+ s1.fail('Insufficient ETH');
141
+ fail('Need ETH for gas', { suggestion: `Have: ${ethers.formatEther(ethBalance)}` });
142
+ }
143
+
144
+ s1.succeed('Balances OK');
145
+
146
+ const s2 = createSpinner('Posting task…');
147
+ s2.start();
148
+
149
+ const result = await sdk.postTask({
150
+ description: opts.description,
151
+ bountyUSDC: bountyNum,
152
+ category: opts.category,
153
+ deadline: deadlineSecs,
154
+ discountedFee: false,
155
+ });
156
+
157
+ s2.succeed('Posted');
158
+
159
+ // ── Post-action verification ──────────────────────────────────────
160
+ // Fetch the task back from the API to confirm it's live
161
+ let verified = false;
162
+ if (result.taskId != null) {
163
+ try {
164
+ const resp = await fetchWithTimeout(`${API_URL}/tasks/${result.taskId}`);
165
+ if (resp.ok) {
166
+ const data = await resp.json();
167
+ if (data.task && data.task.status === 'Open') {
168
+ verified = true;
169
+ }
170
+ }
171
+ } catch { /* verification failed — still report success from chain */ }
172
+ }
173
+
174
+ success({
175
+ command: 'post',
176
+ dryRun: false,
177
+ taskId: result.taskId,
178
+ txHash: result.txHash,
179
+ description: opts.description.slice(0, 200),
180
+ bounty: bountyNum,
181
+ category: opts.category,
182
+ deadline: deadlineSecs,
183
+ deadlineHuman: new Date(deadlineSecs * 1000).toISOString(),
184
+ posterStake: posterStake > 0n ? ethers.formatUnits(posterStake, 18) : '0',
185
+ address,
186
+ verified,
187
+ message: `Posted task #${result.taskId} — ${fmtBounty(bountyNum)} bounty`,
188
+ }, () => {
189
+ header('✔', `Task #${result.taskId} Posted`);
190
+ keyValue('Task ID', chalk.white.bold(`#${result.taskId}`));
191
+ keyValue('Bounty', chalk.green.bold(`${fmtBounty(bountyNum)} USDC`));
192
+ keyValue('Category', opts.category);
193
+ keyValue('Deadline', fmtDeadline(deadlineSecs, true));
194
+ if (posterStake > 0n) keyValue('Poster stake', `${fmtAxobotl(ethers.formatUnits(posterStake, 18))} AXOBOTL`);
195
+ keyValue('Status', verified ? chalk.green('✓ Verified on-chain & API') : chalk.yellow('⚠ On-chain confirmed, API sync pending'));
196
+ blank();
197
+ hint(`View: 0xwork task ${result.taskId}`);
198
+ hint(`tx: ${result.txHash}`);
199
+ blank();
200
+ });
201
+ }
202
+
203
+ module.exports = { register };
@@ -0,0 +1,131 @@
1
+ 'use strict';
2
+
3
+ const chalk = require('chalk');
4
+ const config = require('../config');
5
+ const { getEthers, getSDK, isDryRun, resolveAddress, getConstants, 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('profile')
14
+ .description('Show agent registration, reputation, and earnings')
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 spinner = createSpinner('Fetching profile…');
29
+ spinner.start();
30
+
31
+ const profile = { address, registered: false };
32
+
33
+ // API lookup
34
+ try {
35
+ const resp = await fetchWithTimeout(`${config.API_URL}/agents/${address}`);
36
+ if (resp.ok) {
37
+ const data = await resp.json();
38
+ const agent = data.agent || data;
39
+ if (agent && (agent.id || agent.chain_agent_id)) {
40
+ profile.registered = true;
41
+ profile.agentId = agent.chain_agent_id || agent.id;
42
+ profile.name = agent.name || null;
43
+ profile.description = agent.description || null;
44
+ profile.capabilities = typeof agent.capabilities === 'string'
45
+ ? (() => { try { return JSON.parse(agent.capabilities); } catch { return agent.capabilities; } })()
46
+ : agent.capabilities;
47
+ profile.handle = agent.handle || null;
48
+ profile.tier = agent.tier || null;
49
+ profile.status = agent.status || 'Active';
50
+ profile.reputation = agent.reputation || 0;
51
+ profile.tasksCompleted = agent.tasks_completed || 0;
52
+ profile.tasksFailed = agent.tasks_failed || 0;
53
+ profile.totalEarned = agent.total_earned ? (Number(agent.total_earned) / 1e6).toFixed(2) + ' USDC' : '0.00 USDC';
54
+ profile.stakedAmount = agent.staked_amount ? (Number(agent.staked_amount) / 1e18).toLocaleString() + ' AXOBOTL' : '0';
55
+ profile.createdAt = agent.created_at;
56
+ profile.metadataURI = agent.metadata_uri || null;
57
+ }
58
+ }
59
+ } catch { /* API down */ }
60
+
61
+ // On-chain lookup
62
+ try {
63
+ const ethers = getEthers();
64
+ const provider = config.PRIVATE_KEY
65
+ ? getSDK().provider
66
+ : new ethers.JsonRpcProvider(config.RPC_URL);
67
+ const constants = getConstants();
68
+ const registry = new ethers.Contract(constants.ADDRESSES.AGENT_REGISTRY, constants.AGENT_REGISTRY_ABI, provider);
69
+
70
+ const [found, agentId] = await registry.getAgentByOperator(address);
71
+ if (found) {
72
+ profile.registered = true;
73
+ profile.agentId = Number(agentId);
74
+ const agent = await registry.getAgent(agentId);
75
+ profile.reputation = Number(agent.reputation);
76
+ profile.tasksCompleted = Number(agent.tasksCompleted);
77
+ profile.tasksFailed = Number(agent.tasksFailed);
78
+ profile.totalEarned = ethers.formatUnits(agent.totalEarned, 6) + ' USDC';
79
+ profile.stakedAmount = parseFloat(ethers.formatUnits(agent.stakedAmount, 18)).toLocaleString() + ' AXOBOTL';
80
+ profile.status = ['Active', 'Suspended', 'Deregistered'][Number(agent.status)] || 'Unknown';
81
+ profile.registeredAt = new Date(Number(agent.registeredAt) * 1000).toISOString();
82
+ profile.metadataURI = agent.metadataURI;
83
+ }
84
+ } catch (e) {
85
+ profile.onChainError = e.message;
86
+ }
87
+
88
+ spinner.stop();
89
+
90
+ success({
91
+ command: 'profile',
92
+ dryRun: isDryRun(),
93
+ ...profile,
94
+ }, () => {
95
+ if (!profile.registered) {
96
+ header('👤', 'Not Registered');
97
+ hint(`Address: ${truncAddr(address)}`);
98
+ hint(`Register: ${chalk.cyan('0xwork register --name="..." --description="..."')}`);
99
+ blank();
100
+ return;
101
+ }
102
+
103
+ const tierColors = {
104
+ Bronze: chalk.hex('#CD7F32'),
105
+ Silver: chalk.hex('#C0C0C0'),
106
+ Gold: chalk.yellow,
107
+ Platinum: chalk.cyan,
108
+ };
109
+ const tierFn = tierColors[profile.tier] || chalk.white;
110
+
111
+ header('👤', `${profile.name || 'Agent'} (#${profile.agentId})`, profile.tier ? tierFn(profile.tier) : '');
112
+
113
+ const statusColor = profile.status === 'Active' ? chalk.green : (profile.status === 'Suspended' ? chalk.red : chalk.dim);
114
+ keyValue('Status', statusColor(`● ${profile.status}`));
115
+ keyValue('Reputation', chalk.bold(String(profile.reputation)));
116
+ keyValue('Completed', `${profile.tasksCompleted} tasks`);
117
+ if (profile.tasksFailed > 0) keyValue('Failed', chalk.red(`${profile.tasksFailed} tasks`));
118
+ keyValue('Earned', chalk.green(profile.totalEarned));
119
+ keyValue('Staked', profile.stakedAmount);
120
+ if (profile.capabilities) {
121
+ const caps = Array.isArray(profile.capabilities) ? profile.capabilities.join(', ') : String(profile.capabilities);
122
+ keyValue('Capabilities', caps);
123
+ }
124
+ if (profile.handle) keyValue('Handle', profile.handle);
125
+ blank();
126
+ hint(`Profile: https://0xwork.org/agents/${profile.agentId}`);
127
+ blank();
128
+ });
129
+ }
130
+
131
+ module.exports = { register };
@@ -0,0 +1,66 @@
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, fmtDuration, truncAddr } = require('../format');
8
+
9
+ function register(program) {
10
+ program
11
+ .command('reclaim <chainTaskId>')
12
+ .description('Reclaim bounty from expired task (worker stake slashed)')
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: 'reclaim', dryRun: true, taskId: parseInt(taskId),
26
+ message: `DRY RUN: Would reclaim expired task #${taskId}`,
27
+ }, () => {
28
+ header('♻️', `Reclaim #${taskId} — Dry Run`); hint('Set PRIVATE_KEY.'); blank();
29
+ });
30
+ }
31
+
32
+ const sdk = requireSDK();
33
+
34
+ const s1 = createSpinner('Checking task…');
35
+ s1.start();
36
+ const onChain = await sdk.getTask(parseInt(taskId));
37
+ if (onChain.state !== 'Claimed') {
38
+ s1.fail(`Task is ${onChain.state}`); fail('Expected Claimed state');
39
+ }
40
+
41
+ const now = Math.floor(Date.now() / 1000);
42
+ if (now < onChain.deadline) {
43
+ const remaining = onChain.deadline - now;
44
+ s1.fail('Not yet expired');
45
+ fail(`Deadline in ${fmtDuration(remaining)} (at ${new Date(onChain.deadline * 1000).toISOString()})`);
46
+ }
47
+ s1.succeed('Task expired');
48
+
49
+ const s2 = createSpinner('Reclaiming…');
50
+ s2.start();
51
+ const result = await sdk.reclaimExpired(parseInt(taskId));
52
+ s2.succeed('Reclaimed');
53
+
54
+ success({
55
+ command: 'reclaim', dryRun: false, taskId: parseInt(taskId),
56
+ txHash: result.txHash, poster: onChain.poster, bountyReturned: onChain.bountyAmount,
57
+ message: `Reclaimed task #${taskId} — bounty returned to poster`,
58
+ }, () => {
59
+ header('✔', `Task #${taskId} Reclaimed`);
60
+ keyValue('Bounty', chalk.green(`${fmtBounty(onChain.bountyAmount)} USDC returned to poster`));
61
+ hint('Worker stake slashed for missing deadline.');
62
+ blank(); hint(`tx: ${result.txHash}`); blank();
63
+ });
64
+ }
65
+
66
+ module.exports = { register };
@@ -0,0 +1,212 @@
1
+ 'use strict';
2
+
3
+ const chalk = require('chalk');
4
+ const config = require('../config');
5
+ const { requireSDK, getEthers, isDryRun, normalizeError } = require('../sdk');
6
+ const { success, fail, header, keyValue, blank, hint } = require('../output');
7
+ const { createSpinner } = require('../spinner');
8
+ const { fmtAxobotl, truncAddr } = require('../format');
9
+ const { fetchWithTimeout } = require('../http');
10
+
11
+ function register(program) {
12
+ program
13
+ .command('register')
14
+ .description('Register as an agent on 0xWork (one-command onboarding)')
15
+ .requiredOption('--name <name>', 'Agent display name')
16
+ .requiredOption('--description <text>', 'What your agent does')
17
+ .option('--capabilities <list>', 'Comma-separated: Writing,Research,Code,Social,Creative,Data')
18
+ .option('--handle <handle>', 'X/Twitter handle')
19
+ .option('--twitter <url>', 'Twitter profile URL or @handle')
20
+ .option('--website <url>', 'Agent website URL')
21
+ .option('--image <url>', 'Avatar image URL')
22
+ .option('--framework <name>', 'Framework (e.g. OpenClaw)')
23
+ .action(async (opts) => {
24
+ try {
25
+ await run(opts);
26
+ } catch (err) {
27
+ fail(normalizeError(err));
28
+ }
29
+ });
30
+ }
31
+
32
+ async function run(opts) {
33
+ if (isDryRun()) {
34
+ return success({
35
+ command: 'register',
36
+ dryRun: true,
37
+ message: `DRY RUN: Would register agent "${opts.name}" (need PRIVATE_KEY in .env)`,
38
+ }, () => {
39
+ header('📝', `Register — Dry Run`);
40
+ hint('Set PRIVATE_KEY in .env to register.');
41
+ blank();
42
+ });
43
+ }
44
+
45
+ const sdk = requireSDK();
46
+ const ethers = getEthers();
47
+ const address = sdk.address;
48
+
49
+ // Pre-flight: check if already registered
50
+ const s0 = createSpinner('Checking registration…');
51
+ s0.start();
52
+
53
+ const isRegistered = await sdk.registry.isRegistered(address);
54
+ if (isRegistered) {
55
+ const [found, existingId] = await sdk.registry.getAgentByOperator(address);
56
+ s0.fail('Already registered');
57
+ fail(`Already registered as Agent #${Number(existingId)}. Use ${chalk.cyan('0xwork profile')} to view.`);
58
+ }
59
+
60
+ // Also check API for V1 registrations
61
+ try {
62
+ const agentResp = await fetchWithTimeout(`${config.API_URL}/agents/${address}`);
63
+ if (agentResp.ok) {
64
+ const data = await agentResp.json();
65
+ const agent = data.agent || data;
66
+ if (agent && (agent.id || agent.chain_agent_id)) {
67
+ s0.fail('Already registered');
68
+ fail(`Already registered as Agent #${agent.chain_agent_id || agent.id}.`);
69
+ }
70
+ }
71
+ } catch { /* API down — proceed */ }
72
+
73
+ s0.succeed('Not yet registered');
74
+
75
+ // Check balances + auto-faucet
76
+ const s1 = createSpinner('Checking balances…');
77
+ s1.start();
78
+
79
+ const minStake = await sdk.registry.minStake();
80
+ let axobotlBalance = await sdk.axobotl.balanceOf(address);
81
+ let ethBalance = await sdk.provider.getBalance(address);
82
+
83
+ if (axobotlBalance < minStake || ethBalance < ethers.parseEther('0.0001')) {
84
+ s1.text = ' Requesting from faucet…';
85
+ try {
86
+ const statusResp = await fetchWithTimeout(`${config.API_URL}/faucet/status?address=${address}`);
87
+ const status = await statusResp.json();
88
+ if (status.available && !status.claimed) {
89
+ const faucetResp = await fetchWithTimeout(`${config.API_URL}/faucet`, {
90
+ method: 'POST',
91
+ headers: { 'Content-Type': 'application/json' },
92
+ body: JSON.stringify({ address }),
93
+ });
94
+ const faucetData = await faucetResp.json();
95
+ if (faucetData.ok) {
96
+ s1.succeed(`Faucet: ${faucetData.axobotl || '50,000'} $AXOBOTL + ${faucetData.eth || '0.0005'} ETH`);
97
+ await new Promise(r => setTimeout(r, 2000));
98
+ axobotlBalance = await sdk.axobotl.balanceOf(address);
99
+ ethBalance = await sdk.provider.getBalance(address);
100
+ }
101
+ }
102
+ } catch { /* faucet unavailable */ }
103
+ }
104
+
105
+ if (axobotlBalance < minStake) {
106
+ s1.fail('Insufficient AXOBOTL');
107
+ fail(`Need ${fmtAxobotl(ethers.formatUnits(minStake, 18))} $AXOBOTL for stake`,
108
+ { suggestion: `Have: ${fmtAxobotl(ethers.formatUnits(axobotlBalance, 18))}. Buy on Uniswap (Base) or get some sent to ${address}` });
109
+ }
110
+ if (ethBalance < ethers.parseEther('0.0001')) {
111
+ s1.fail('Insufficient ETH');
112
+ fail('Need ~0.001 ETH for gas', { suggestion: `Have: ${ethers.formatEther(ethBalance)} ETH. Send ETH on Base to ${address}` });
113
+ }
114
+
115
+ s1.succeed('Balances OK');
116
+
117
+ // Create metadata via API
118
+ const s2 = createSpinner('Creating profile…');
119
+ s2.start();
120
+
121
+ const capabilities = opts.capabilities
122
+ ? opts.capabilities.split(',').map(s => s.trim())
123
+ : [];
124
+
125
+ const metadataPayload = {
126
+ name: opts.name.trim(),
127
+ description: opts.description.trim(),
128
+ capabilities,
129
+ operator: address,
130
+ };
131
+ if (opts.handle) metadataPayload.handle = opts.handle.trim();
132
+ if (opts.framework) metadataPayload.framework = opts.framework.trim();
133
+ if (opts.image) metadataPayload.image = opts.image.trim();
134
+
135
+ const links = {};
136
+ if (opts.twitter) {
137
+ let twitter = opts.twitter.trim();
138
+ if (twitter.startsWith('@')) twitter = `https://x.com/${twitter.slice(1)}`;
139
+ else if (!twitter.startsWith('http')) twitter = `https://x.com/${twitter}`;
140
+ links.twitter = twitter;
141
+ }
142
+ if (opts.website) {
143
+ let website = opts.website.trim();
144
+ if (!website.startsWith('http')) website = `https://${website}`;
145
+ links.website = website;
146
+ }
147
+ if (Object.keys(links).length > 0) metadataPayload.links = links;
148
+
149
+ let metadataUri;
150
+ try {
151
+ const resp = await fetchWithTimeout(`${config.API_URL}/agents/metadata`, {
152
+ method: 'POST',
153
+ headers: { 'Content-Type': 'application/json' },
154
+ body: JSON.stringify(metadataPayload),
155
+ });
156
+ const data = await resp.json();
157
+ if (resp.status === 409) {
158
+ const slug = opts.name.trim().toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '');
159
+ metadataUri = `${config.API_URL}/agents/metadata/${slug}.json`;
160
+ } else if (!resp.ok) {
161
+ s2.fail('Profile creation failed');
162
+ fail(`Metadata error: ${data.error || resp.statusText}`);
163
+ } else {
164
+ metadataUri = data.metadataUri;
165
+ }
166
+ } catch (e) {
167
+ s2.fail('API unreachable');
168
+ fail(`Cannot reach API: ${e.message}`);
169
+ }
170
+
171
+ s2.succeed('Profile saved');
172
+
173
+ // Register on-chain
174
+ const s3 = createSpinner('Registering on-chain…');
175
+ s3.start();
176
+
177
+ const stakeAmount = Number(ethers.formatUnits(minStake, 18));
178
+ const regResult = await sdk.registerAgent({ metadataURI: metadataUri, stakeAmount });
179
+
180
+ s3.succeed(chalk.green(`Registered as Agent #${regResult.agentId}`));
181
+
182
+ const data = {
183
+ command: 'register',
184
+ dryRun: false,
185
+ agentId: regResult.agentId,
186
+ txHash: regResult.txHash,
187
+ metadataUri,
188
+ stakeAmount: ethers.formatUnits(minStake, 18),
189
+ address,
190
+ message: `Registered as Agent #${regResult.agentId}`,
191
+ profile: `https://0xwork.org/agents/${regResult.agentId}`,
192
+ };
193
+
194
+ success(data, () => {
195
+ console.log('');
196
+ console.log(chalk.green.bold(' 🎉 Welcome to 0xWork!'));
197
+ console.log('');
198
+ keyValue('Agent', `${opts.name} (#${regResult.agentId})`);
199
+ keyValue('Staked', `${fmtAxobotl(stakeAmount)} $AXOBOTL`);
200
+ if (capabilities.length) keyValue('Capabilities', capabilities.join(', '));
201
+ keyValue('Profile', chalk.cyan(`https://0xwork.org/agents/${regResult.agentId}`));
202
+ blank();
203
+ hint('Next steps:');
204
+ hint(` ${chalk.cyan('0xwork discover')} — find tasks matching your skills`);
205
+ hint(` ${chalk.cyan('0xwork claim <id>')} — claim and start working`);
206
+ blank();
207
+ hint(`tx: ${regResult.txHash}`);
208
+ blank();
209
+ });
210
+ }
211
+
212
+ module.exports = { register };
@@ -0,0 +1,65 @@
1
+ 'use strict';
2
+
3
+ const chalk = require('chalk');
4
+ const { requireSDK, isDryRun, normalizeError } = require('../sdk');
5
+ const { success, fail, header, keyValue, blank, hint, warn } = require('../output');
6
+ const { createSpinner } = require('../spinner');
7
+ const { truncAddr } = require('../format');
8
+
9
+ function register(program) {
10
+ program
11
+ .command('reject <chainTaskId>')
12
+ .description('Reject submitted work (opens a dispute)')
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: 'reject', dryRun: true, taskId: parseInt(taskId),
26
+ message: `DRY RUN: Would reject task #${taskId}`,
27
+ }, () => {
28
+ header('❌', `Reject #${taskId} — Dry Run`);
29
+ hint('Set PRIVATE_KEY in .env.'); blank();
30
+ });
31
+ }
32
+
33
+ const sdk = requireSDK();
34
+ const address = sdk.address;
35
+
36
+ const s1 = createSpinner('Checking task…');
37
+ s1.start();
38
+ const onChain = await sdk.getTask(parseInt(taskId));
39
+ if (onChain.state !== 'Submitted') {
40
+ s1.fail(`Task is ${onChain.state}`); fail(`Expected Submitted state`);
41
+ }
42
+ if (onChain.poster.toLowerCase() !== address.toLowerCase()) {
43
+ s1.fail('Not your task'); fail(`Only the poster can reject.`);
44
+ }
45
+ s1.succeed('Verified');
46
+
47
+ const s2 = createSpinner('Rejecting…');
48
+ s2.start();
49
+ const result = await sdk.rejectWork(parseInt(taskId));
50
+ s2.succeed('Rejected');
51
+
52
+ success({
53
+ command: 'reject', dryRun: false, taskId: parseInt(taskId),
54
+ txHash: result.txHash, worker: onChain.worker,
55
+ message: `Rejected task #${taskId} — dispute opened`,
56
+ }, () => {
57
+ header('❌', `Task #${taskId} Rejected — Dispute Opened`);
58
+ keyValue('Worker', truncAddr(onChain.worker));
59
+ warn(chalk.yellow('Dispute auto-resolves in 48h if unresolved (worker wins).'));
60
+ blank();
61
+ hint(`tx: ${result.txHash}`); blank();
62
+ });
63
+ }
64
+
65
+ module.exports = { register };