@0xwork/cli 0.1.0 → 1.0.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 +149 -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,76 @@
|
|
|
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('claim-approval <chainTaskId>')
|
|
12
|
+
.description('Trigger auto-approve after poster ghosts 7 days')
|
|
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: 'claim-approval', dryRun: true, taskId: parseInt(taskId),
|
|
26
|
+
message: `DRY RUN: Would trigger auto-approve on task #${taskId}`,
|
|
27
|
+
}, () => {
|
|
28
|
+
header('⏱️', `Claim Approval #${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 !== 'Submitted') {
|
|
38
|
+
s1.fail(`Task is ${onChain.state}`); fail('Expected Submitted state');
|
|
39
|
+
}
|
|
40
|
+
if (!onChain.submitTimestamp) {
|
|
41
|
+
s1.fail('No submission timestamp'); fail('No submission timestamp recorded.');
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const now = Math.floor(Date.now() / 1000);
|
|
45
|
+
const window = 7 * 86400;
|
|
46
|
+
const eligibleAt = onChain.submitTimestamp + window;
|
|
47
|
+
if (now < eligibleAt) {
|
|
48
|
+
const remaining = eligibleAt - now;
|
|
49
|
+
s1.fail('Not yet eligible');
|
|
50
|
+
fail(`Auto-approve eligible in ${fmtDuration(remaining)} (at ${new Date(eligibleAt * 1000).toISOString()})`);
|
|
51
|
+
}
|
|
52
|
+
s1.succeed('Eligible');
|
|
53
|
+
|
|
54
|
+
const s2 = createSpinner('Triggering auto-approve…');
|
|
55
|
+
s2.start();
|
|
56
|
+
const result = await sdk.claimApproval(parseInt(taskId));
|
|
57
|
+
s2.succeed('Auto-approved');
|
|
58
|
+
|
|
59
|
+
const bounty = parseFloat(onChain.bountyAmount);
|
|
60
|
+
const fee = onChain.discountedFee ? bounty * 0.02 : bounty * 0.05;
|
|
61
|
+
const payout = bounty - fee;
|
|
62
|
+
|
|
63
|
+
success({
|
|
64
|
+
command: 'claim-approval', dryRun: false, taskId: parseInt(taskId),
|
|
65
|
+
txHash: result.txHash, worker: onChain.worker, payout: payout.toFixed(2),
|
|
66
|
+
message: `Auto-approved task #${taskId} — ${fmtBounty(payout)} to worker`,
|
|
67
|
+
}, () => {
|
|
68
|
+
header('✔', `Task #${taskId} Auto-Approved`);
|
|
69
|
+
keyValue('Worker', truncAddr(onChain.worker));
|
|
70
|
+
keyValue('Payout', chalk.green.bold(`${fmtBounty(payout)} USDC`));
|
|
71
|
+
hint('Poster stake slashed for inaction.');
|
|
72
|
+
blank(); hint(`tx: ${result.txHash}`); blank();
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
module.exports = { register };
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const chalk = require('chalk');
|
|
4
|
+
const config = require('../config');
|
|
5
|
+
const { getEthers, isDryRun, requireSDK, normalizeError } = require('../sdk');
|
|
6
|
+
const { success, fail, header, keyValue, blank, hint } = require('../output');
|
|
7
|
+
const { createSpinner } = require('../spinner');
|
|
8
|
+
const { fmtBounty, fmtDeadline, fmtAxobotl } = require('../format');
|
|
9
|
+
const { getAxobotlPrice, formatAxobotlUsd } = require('../price');
|
|
10
|
+
const { resolveTask } = require('../resolve');
|
|
11
|
+
const { fetchWithTimeout } = require('../http');
|
|
12
|
+
|
|
13
|
+
function register(program) {
|
|
14
|
+
program
|
|
15
|
+
.command('claim <chainTaskId>')
|
|
16
|
+
.description('Claim a task on-chain (stakes $AXOBOTL as collateral)')
|
|
17
|
+
.action(async (chainTaskId) => {
|
|
18
|
+
try {
|
|
19
|
+
await run(chainTaskId);
|
|
20
|
+
} catch (err) {
|
|
21
|
+
fail(normalizeError(err));
|
|
22
|
+
}
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
async function run(taskId) {
|
|
27
|
+
if (isDryRun()) {
|
|
28
|
+
let t;
|
|
29
|
+
try { t = await resolveTask(taskId); } catch { t = null; }
|
|
30
|
+
return success({
|
|
31
|
+
command: 'claim',
|
|
32
|
+
dryRun: true,
|
|
33
|
+
taskId: parseInt(taskId),
|
|
34
|
+
message: `DRY RUN: Would claim task #${taskId}`,
|
|
35
|
+
bounty: t ? t.bounty_amount : null,
|
|
36
|
+
stakeRaw: t ? t.stake_amount : null,
|
|
37
|
+
txHash: null,
|
|
38
|
+
}, () => {
|
|
39
|
+
header('🔒', `Claim Task #${taskId} — Dry Run`);
|
|
40
|
+
hint('Set PRIVATE_KEY in .env to claim tasks.');
|
|
41
|
+
blank();
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const sdk = requireSDK();
|
|
46
|
+
const ethers = getEthers();
|
|
47
|
+
const address = sdk.address;
|
|
48
|
+
|
|
49
|
+
// Pre-flight: fetch on-chain state
|
|
50
|
+
const s1 = createSpinner('Checking task state…');
|
|
51
|
+
s1.start();
|
|
52
|
+
|
|
53
|
+
let onChain;
|
|
54
|
+
try {
|
|
55
|
+
onChain = await sdk.getTask(parseInt(taskId));
|
|
56
|
+
} catch (e) {
|
|
57
|
+
s1.fail('Task not found on-chain');
|
|
58
|
+
fail(`Task #${taskId} not found on-chain: ${normalizeError(e)}`);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (onChain.poster === '0x0000000000000000000000000000000000000000') {
|
|
62
|
+
s1.fail('Task does not exist');
|
|
63
|
+
fail(`Task #${taskId} does not exist on-chain`);
|
|
64
|
+
}
|
|
65
|
+
if (onChain.state !== 'Open') {
|
|
66
|
+
s1.fail(`Task is ${onChain.state}`);
|
|
67
|
+
fail(`Task #${taskId} is not Open (current: ${onChain.state})`);
|
|
68
|
+
}
|
|
69
|
+
if (onChain.poster.toLowerCase() === address.toLowerCase()) {
|
|
70
|
+
s1.fail('Cannot claim own task');
|
|
71
|
+
fail('Cannot claim your own task');
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Preferred lock check
|
|
75
|
+
try {
|
|
76
|
+
const t = await resolveTask(taskId);
|
|
77
|
+
const lock = t.preferred_lock;
|
|
78
|
+
if (lock && lock.locked && t.preferred_agent_id != null) {
|
|
79
|
+
let myAgentId = null;
|
|
80
|
+
const agentResp = await fetchWithTimeout(`${config.API_URL}/agents/${address}`);
|
|
81
|
+
if (agentResp.ok) {
|
|
82
|
+
const agentData = await agentResp.json();
|
|
83
|
+
myAgentId = agentData.agent ? agentData.agent.chain_agent_id : null;
|
|
84
|
+
}
|
|
85
|
+
if (myAgentId == null || t.preferred_agent_id !== myAgentId) {
|
|
86
|
+
const agentName = t.preferred_agent_name || `Agent #${t.preferred_agent_id}`;
|
|
87
|
+
const hours = Math.ceil(lock.remaining_seconds / 3600);
|
|
88
|
+
s1.fail('Task is reserved');
|
|
89
|
+
fail(`Reserved for ${agentName} for ${hours}h. Try again later.`);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
} catch (e) {
|
|
93
|
+
if (e.message && e.message.startsWith('Reserved')) throw e;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
s1.succeed('Task is Open');
|
|
97
|
+
|
|
98
|
+
// Pre-flight: check balances
|
|
99
|
+
const s2 = createSpinner('Checking balances…');
|
|
100
|
+
s2.start();
|
|
101
|
+
|
|
102
|
+
const stakeRequired = await sdk.taskPool.calculateStake(
|
|
103
|
+
ethers.parseUnits(String(onChain.bountyAmount), 6)
|
|
104
|
+
);
|
|
105
|
+
const axobotlBalance = await sdk.axobotl.balanceOf(address);
|
|
106
|
+
if (axobotlBalance < stakeRequired) {
|
|
107
|
+
s2.fail('Insufficient AXOBOTL');
|
|
108
|
+
fail(
|
|
109
|
+
`Insufficient $AXOBOTL for stake`,
|
|
110
|
+
{ suggestion: `Need: ${fmtAxobotl(ethers.formatUnits(stakeRequired, 18))}, Have: ${fmtAxobotl(ethers.formatUnits(axobotlBalance, 18))}. Buy on Uniswap (Base) or request from faucet.` }
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const ethBalance = await sdk.provider.getBalance(address);
|
|
115
|
+
if (ethBalance < ethers.parseEther('0.0001')) {
|
|
116
|
+
s2.fail('Insufficient ETH');
|
|
117
|
+
fail('Insufficient ETH for gas', { suggestion: `Have: ${ethers.formatEther(ethBalance)} ETH. Send ETH on Base to ${address}` });
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
s2.succeed('Balances OK');
|
|
121
|
+
|
|
122
|
+
// Claim
|
|
123
|
+
const s3 = createSpinner('Claiming task…');
|
|
124
|
+
s3.start();
|
|
125
|
+
|
|
126
|
+
const result = await sdk.claimTask(parseInt(taskId));
|
|
127
|
+
|
|
128
|
+
s3.succeed('Claimed');
|
|
129
|
+
|
|
130
|
+
const stakeHuman = ethers.formatUnits(stakeRequired, 18);
|
|
131
|
+
const price = await getAxobotlPrice();
|
|
132
|
+
const stakeUsd = price ? formatAxobotlUsd(stakeHuman, price) : null;
|
|
133
|
+
|
|
134
|
+
const data = {
|
|
135
|
+
command: 'claim',
|
|
136
|
+
dryRun: false,
|
|
137
|
+
taskId: parseInt(taskId),
|
|
138
|
+
txHash: result.txHash,
|
|
139
|
+
stakeAmount: stakeHuman,
|
|
140
|
+
bounty: onChain.bountyAmount,
|
|
141
|
+
message: `Claimed task #${taskId} — staked ${fmtAxobotl(stakeHuman)} AXOBOTL`,
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
success(data, () => {
|
|
145
|
+
header('✔', `Task #${taskId} Claimed`);
|
|
146
|
+
keyValue('Bounty', chalk.green.bold(`${fmtBounty(onChain.bountyAmount)} USDC`));
|
|
147
|
+
keyValue('Staked', `${fmtAxobotl(stakeHuman)} AXOBOTL${stakeUsd ? chalk.dim(` (~${stakeUsd})`) : ''}`);
|
|
148
|
+
keyValue('Deadline', fmtDeadline(onChain.deadline, true));
|
|
149
|
+
blank();
|
|
150
|
+
hint(`Next: complete the work, then run:`);
|
|
151
|
+
hint(` $ 0xwork submit ${taskId} --proof="https://..."`);
|
|
152
|
+
blank();
|
|
153
|
+
hint(`tx: ${result.txHash}`);
|
|
154
|
+
blank();
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
module.exports = { register };
|
|
@@ -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 };
|