@0xwork/cli 0.2.0 → 1.1.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 +157 -29
- package/bin/0xwork.js +9 -6
- package/package.json +33 -8
- package/src/commands/abandon.js +59 -0
- package/src/commands/approve.js +75 -0
- package/src/commands/auto-resolve.js +76 -0
- package/src/commands/balance.js +116 -0
- package/src/commands/cancel.js +67 -0
- package/src/commands/claim-approval.js +76 -0
- package/src/commands/claim.js +158 -0
- package/src/commands/discover.js +196 -0
- package/src/commands/extend.js +89 -0
- package/src/commands/faucet.js +92 -0
- package/src/commands/init.js +78 -0
- package/src/commands/mutual-cancel.js +84 -0
- package/src/commands/post.js +180 -0
- package/src/commands/profile.js +131 -0
- package/src/commands/reclaim.js +66 -0
- package/src/commands/register.js +212 -0
- package/src/commands/reject.js +65 -0
- package/src/commands/retract-cancel.js +61 -0
- package/src/commands/revision.js +73 -0
- package/src/commands/status.js +118 -0
- package/src/commands/submit.js +153 -0
- package/src/commands/task.js +140 -0
- package/src/config.js +59 -0
- package/src/format.js +157 -0
- package/src/http.js +38 -0
- package/src/index.js +82 -0
- package/src/output.js +182 -0
- package/src/price.js +41 -0
- package/src/resolve.js +32 -0
- package/src/safety.js +21 -0
- package/src/sdk.js +147 -0
- package/src/spinner.js +29 -0
- package/create-agent/README.md +0 -55
- package/create-agent/bin/create-agent.js +0 -159
- package/create-agent/lib/scaffold.js +0 -62
- package/create-agent/lib/templates.js +0 -166
- package/create-agent/package.json +0 -20
|
@@ -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 };
|
|
@@ -0,0 +1,180 @@
|
|
|
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 } = require('../output');
|
|
6
|
+
const { createSpinner } = require('../spinner');
|
|
7
|
+
const { fmtBounty, fmtDeadline, fmtAxobotl } = require('../format');
|
|
8
|
+
|
|
9
|
+
const VALID_CATEGORIES = ['Writing', 'Research', 'Code', 'Creative', 'Data', 'Social'];
|
|
10
|
+
|
|
11
|
+
// ─── Spending Guardrails ────────────────────────────────────────────
|
|
12
|
+
// Two layers of protection for agents interacting with unaudited escrow:
|
|
13
|
+
// 1. Percentage cap: never lock more than MAX_BOUNTY_PERCENT of USDC balance (default 20%)
|
|
14
|
+
// 2. Absolute cap: never post more than MAX_BOUNTY_USDC in a single task (default $50)
|
|
15
|
+
// Both can be overridden via env vars or --force flag for experienced users.
|
|
16
|
+
const DEFAULT_MAX_BOUNTY_PERCENT = parseFloat(process.env.MAX_BOUNTY_PERCENT || '20');
|
|
17
|
+
const DEFAULT_MAX_BOUNTY_USDC = parseFloat(process.env.MAX_BOUNTY_USDC || '50');
|
|
18
|
+
|
|
19
|
+
function register(program) {
|
|
20
|
+
program
|
|
21
|
+
.command('post')
|
|
22
|
+
.description('Post a new task with USDC bounty')
|
|
23
|
+
.requiredOption('--description <text>', 'Task description')
|
|
24
|
+
.requiredOption('--bounty <amount>', 'Bounty in USDC (e.g. 10)')
|
|
25
|
+
.option('--category <cat>', 'Category: Writing, Research, Code, Creative, Data, Social', 'Code')
|
|
26
|
+
.option('--deadline <dur>', 'Deadline: 7d, 24h, 30m (default: 7d)', '7d')
|
|
27
|
+
.option('--force', 'Bypass spending guardrails (percentage + absolute cap)')
|
|
28
|
+
.action(async (opts) => {
|
|
29
|
+
try {
|
|
30
|
+
await run(opts);
|
|
31
|
+
} catch (err) {
|
|
32
|
+
fail(normalizeError(err));
|
|
33
|
+
}
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async function run(opts) {
|
|
38
|
+
const bountyNum = parseFloat(opts.bounty);
|
|
39
|
+
if (isNaN(bountyNum) || bountyNum <= 0) fail('Bounty must be a positive number');
|
|
40
|
+
|
|
41
|
+
if (!VALID_CATEGORIES.includes(opts.category)) {
|
|
42
|
+
fail(`Invalid category: "${opts.category}"`, { suggestion: `Valid: ${VALID_CATEGORIES.join(', ')}` });
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Parse deadline
|
|
46
|
+
let deadlineSecs;
|
|
47
|
+
const match = opts.deadline.match(/^(\d+)([dhm])$/);
|
|
48
|
+
if (match) {
|
|
49
|
+
const val = parseInt(match[1]);
|
|
50
|
+
if (val <= 0) fail('Deadline duration must be positive (e.g. 7d, 24h, 30m)');
|
|
51
|
+
const mult = { d: 86400, h: 3600, m: 60 };
|
|
52
|
+
deadlineSecs = Math.floor(Date.now() / 1000) + val * mult[match[2]];
|
|
53
|
+
} else {
|
|
54
|
+
// Try ISO date
|
|
55
|
+
const parsed = Date.parse(opts.deadline);
|
|
56
|
+
if (!isNaN(parsed)) {
|
|
57
|
+
deadlineSecs = Math.floor(parsed / 1000);
|
|
58
|
+
if (deadlineSecs <= Math.floor(Date.now() / 1000)) {
|
|
59
|
+
fail('Deadline must be in the future');
|
|
60
|
+
}
|
|
61
|
+
} else {
|
|
62
|
+
fail(`Invalid deadline: "${opts.deadline}". Use: 7d, 24h, 30m, or ISO date (2026-03-15)`);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (isDryRun()) {
|
|
67
|
+
return success({
|
|
68
|
+
command: 'post',
|
|
69
|
+
dryRun: true,
|
|
70
|
+
description: opts.description.slice(0, 200),
|
|
71
|
+
bounty: bountyNum,
|
|
72
|
+
category: opts.category,
|
|
73
|
+
deadline: deadlineSecs,
|
|
74
|
+
deadlineHuman: new Date(deadlineSecs * 1000).toISOString(),
|
|
75
|
+
message: `DRY RUN: Would post task with ${fmtBounty(bountyNum)} bounty`,
|
|
76
|
+
}, () => {
|
|
77
|
+
header('📝', 'Post Task — Dry Run');
|
|
78
|
+
keyValue('Bounty', chalk.green.bold(fmtBounty(bountyNum)));
|
|
79
|
+
keyValue('Category', opts.category);
|
|
80
|
+
keyValue('Deadline', fmtDeadline(deadlineSecs, true));
|
|
81
|
+
blank();
|
|
82
|
+
hint('Set PRIVATE_KEY in .env to post tasks.');
|
|
83
|
+
blank();
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const sdk = requireSDK();
|
|
88
|
+
const ethers = getEthers();
|
|
89
|
+
const address = sdk.address;
|
|
90
|
+
|
|
91
|
+
// Pre-flight checks
|
|
92
|
+
const s1 = createSpinner('Checking balances…');
|
|
93
|
+
s1.start();
|
|
94
|
+
|
|
95
|
+
const bountyAmount = ethers.parseUnits(String(bountyNum), 6);
|
|
96
|
+
const usdcBalance = await sdk.usdc.balanceOf(address);
|
|
97
|
+
if (usdcBalance < bountyAmount) {
|
|
98
|
+
s1.fail('Insufficient USDC');
|
|
99
|
+
fail(`Need ${fmtBounty(bountyNum)} USDC`, { suggestion: `Have: ${fmtBounty(ethers.formatUnits(usdcBalance, 6))}. Send USDC on Base to ${address}` });
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// ── Spending guardrails ──
|
|
103
|
+
if (!opts.force) {
|
|
104
|
+
const balanceNum = parseFloat(ethers.formatUnits(usdcBalance, 6));
|
|
105
|
+
|
|
106
|
+
// 1. Absolute cap
|
|
107
|
+
if (bountyNum > DEFAULT_MAX_BOUNTY_USDC) {
|
|
108
|
+
s1.fail('Bounty exceeds safety cap');
|
|
109
|
+
fail(`Bounty $${bountyNum} exceeds max $${DEFAULT_MAX_BOUNTY_USDC} per task`, {
|
|
110
|
+
suggestion: `Set MAX_BOUNTY_USDC in .env to raise the limit, or use --force to bypass.`,
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// 2. Percentage cap
|
|
115
|
+
const pctUsed = (bountyNum / balanceNum) * 100;
|
|
116
|
+
if (pctUsed > DEFAULT_MAX_BOUNTY_PERCENT) {
|
|
117
|
+
s1.fail('Bounty exceeds balance percentage cap');
|
|
118
|
+
fail(`Bounty $${bountyNum} is ${pctUsed.toFixed(1)}% of your $${balanceNum.toFixed(2)} balance (max ${DEFAULT_MAX_BOUNTY_PERCENT}%)`, {
|
|
119
|
+
suggestion: `Set MAX_BOUNTY_PERCENT in .env to raise the limit, or use --force to bypass.`,
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const posterStake = await sdk.taskPool.calculatePosterStake(bountyAmount);
|
|
125
|
+
if (posterStake > 0n) {
|
|
126
|
+
const axobotlBalance = await sdk.axobotl.balanceOf(address);
|
|
127
|
+
if (axobotlBalance < posterStake) {
|
|
128
|
+
s1.fail('Insufficient AXOBOTL');
|
|
129
|
+
fail(`Need ${fmtAxobotl(ethers.formatUnits(posterStake, 18))} $AXOBOTL for poster stake`,
|
|
130
|
+
{ suggestion: `Have: ${fmtAxobotl(ethers.formatUnits(axobotlBalance, 18))}` });
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const ethBalance = await sdk.provider.getBalance(address);
|
|
135
|
+
if (ethBalance < ethers.parseEther('0.0001')) {
|
|
136
|
+
s1.fail('Insufficient ETH');
|
|
137
|
+
fail('Need ETH for gas', { suggestion: `Have: ${ethers.formatEther(ethBalance)}` });
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
s1.succeed('Balances OK');
|
|
141
|
+
|
|
142
|
+
const s2 = createSpinner('Posting task…');
|
|
143
|
+
s2.start();
|
|
144
|
+
|
|
145
|
+
const result = await sdk.postTask({
|
|
146
|
+
description: opts.description,
|
|
147
|
+
bountyUSDC: bountyNum,
|
|
148
|
+
category: opts.category,
|
|
149
|
+
deadline: deadlineSecs,
|
|
150
|
+
discountedFee: false,
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
s2.succeed('Posted');
|
|
154
|
+
|
|
155
|
+
success({
|
|
156
|
+
command: 'post',
|
|
157
|
+
dryRun: false,
|
|
158
|
+
taskId: result.taskId,
|
|
159
|
+
txHash: result.txHash,
|
|
160
|
+
description: opts.description.slice(0, 200),
|
|
161
|
+
bounty: bountyNum,
|
|
162
|
+
category: opts.category,
|
|
163
|
+
deadline: deadlineSecs,
|
|
164
|
+
deadlineHuman: new Date(deadlineSecs * 1000).toISOString(),
|
|
165
|
+
posterStake: posterStake > 0n ? ethers.formatUnits(posterStake, 18) : '0',
|
|
166
|
+
address,
|
|
167
|
+
message: `Posted task #${result.taskId} — ${fmtBounty(bountyNum)} bounty`,
|
|
168
|
+
}, () => {
|
|
169
|
+
header('✔', `Task #${result.taskId} Posted`);
|
|
170
|
+
keyValue('Bounty', chalk.green.bold(`${fmtBounty(bountyNum)} USDC`));
|
|
171
|
+
keyValue('Category', opts.category);
|
|
172
|
+
keyValue('Deadline', fmtDeadline(deadlineSecs, true));
|
|
173
|
+
if (posterStake > 0n) keyValue('Poster stake', `${fmtAxobotl(ethers.formatUnits(posterStake, 18))} AXOBOTL`);
|
|
174
|
+
blank();
|
|
175
|
+
hint(`tx: ${result.txHash}`);
|
|
176
|
+
blank();
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
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 };
|