@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.
- package/README.md +157 -29
- package/bin/0xwork.js +1273 -4
- 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 +203 -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 +156 -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,61 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const chalk = require('chalk');
|
|
4
|
+
const { requireSDK, isDryRun, normalizeError } = require('../sdk');
|
|
5
|
+
const { success, fail, header, blank, hint } = require('../output');
|
|
6
|
+
const { createSpinner } = require('../spinner');
|
|
7
|
+
|
|
8
|
+
function register(program) {
|
|
9
|
+
program
|
|
10
|
+
.command('retract-cancel <chainTaskId>')
|
|
11
|
+
.description('Retract a pending mutual cancel request')
|
|
12
|
+
.action(async (chainTaskId) => {
|
|
13
|
+
try {
|
|
14
|
+
await run(chainTaskId);
|
|
15
|
+
} catch (err) {
|
|
16
|
+
fail(normalizeError(err));
|
|
17
|
+
}
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
async function run(taskId) {
|
|
22
|
+
if (isDryRun()) {
|
|
23
|
+
return success({
|
|
24
|
+
command: 'retract-cancel', dryRun: true, taskId: parseInt(taskId),
|
|
25
|
+
message: `DRY RUN: Would retract cancel request on task #${taskId}`,
|
|
26
|
+
}, () => {
|
|
27
|
+
header('↩️', `Retract Cancel #${taskId} — Dry Run`); hint('Set PRIVATE_KEY.'); blank();
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const sdk = requireSDK();
|
|
32
|
+
const address = sdk.address;
|
|
33
|
+
|
|
34
|
+
const s1 = createSpinner('Checking task…');
|
|
35
|
+
s1.start();
|
|
36
|
+
const onChain = await sdk.getTask(parseInt(taskId));
|
|
37
|
+
if (!onChain.cancelRequestedBy) {
|
|
38
|
+
s1.fail('No pending request'); fail('No pending cancel request to retract.');
|
|
39
|
+
}
|
|
40
|
+
if (onChain.cancelRequestedBy.toLowerCase() !== address.toLowerCase()) {
|
|
41
|
+
s1.fail('Not your request');
|
|
42
|
+
fail(`Only the requester can retract. Requested by: ${onChain.cancelRequestedBy}`);
|
|
43
|
+
}
|
|
44
|
+
s1.succeed('Verified');
|
|
45
|
+
|
|
46
|
+
const s2 = createSpinner('Retracting…');
|
|
47
|
+
s2.start();
|
|
48
|
+
const result = await sdk.retractCancelRequest(parseInt(taskId));
|
|
49
|
+
s2.succeed('Retracted');
|
|
50
|
+
|
|
51
|
+
success({
|
|
52
|
+
command: 'retract-cancel', dryRun: false, taskId: parseInt(taskId),
|
|
53
|
+
txHash: result.txHash,
|
|
54
|
+
message: `Retracted cancel request on task #${taskId}`,
|
|
55
|
+
}, () => {
|
|
56
|
+
header('✔', `Cancel Request Retracted — Task #${taskId}`);
|
|
57
|
+
hint(`tx: ${result.txHash}`); blank();
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
module.exports = { register };
|
|
@@ -0,0 +1,73 @@
|
|
|
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 { truncAddr } = require('../format');
|
|
8
|
+
|
|
9
|
+
function register(program) {
|
|
10
|
+
program
|
|
11
|
+
.command('revision <chainTaskId>')
|
|
12
|
+
.description('Request revision on submitted work (max 2 per task)')
|
|
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: 'revision', dryRun: true, taskId: parseInt(taskId),
|
|
26
|
+
message: `DRY RUN: Would request revision on task #${taskId}`,
|
|
27
|
+
}, () => {
|
|
28
|
+
header('🔄', `Revision #${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 request revision.`);
|
|
44
|
+
}
|
|
45
|
+
if (onChain.revisionCount >= 2) {
|
|
46
|
+
s1.fail('Max revisions reached'); fail('Maximum of 2 revisions reached. You must approve or reject.');
|
|
47
|
+
}
|
|
48
|
+
s1.succeed('Verified');
|
|
49
|
+
|
|
50
|
+
const s2 = createSpinner('Requesting revision…');
|
|
51
|
+
s2.start();
|
|
52
|
+
const result = await sdk.requestRevision(parseInt(taskId));
|
|
53
|
+
s2.succeed('Revision requested');
|
|
54
|
+
|
|
55
|
+
const used = onChain.revisionCount + 1;
|
|
56
|
+
const remaining = 2 - used;
|
|
57
|
+
|
|
58
|
+
success({
|
|
59
|
+
command: 'revision', dryRun: false, taskId: parseInt(taskId),
|
|
60
|
+
txHash: result.txHash, worker: onChain.worker,
|
|
61
|
+
revisionsUsed: used, revisionsRemaining: remaining,
|
|
62
|
+
message: `Revision requested on task #${taskId} (${used}/2 used)`,
|
|
63
|
+
}, () => {
|
|
64
|
+
header('🔄', `Revision Requested — Task #${taskId}`);
|
|
65
|
+
keyValue('Worker', truncAddr(onChain.worker));
|
|
66
|
+
keyValue('Revisions', `${used}/2 used (${remaining} remaining)`);
|
|
67
|
+
hint("Worker's deadline extended by 48h.");
|
|
68
|
+
blank();
|
|
69
|
+
hint(`tx: ${result.txHash}`); blank();
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
module.exports = { register };
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const chalk = require('chalk');
|
|
4
|
+
const Table = require('cli-table3');
|
|
5
|
+
const config = require('../config');
|
|
6
|
+
const { isDryRun, resolveAddress, normalizeError } = require('../sdk');
|
|
7
|
+
const { success, fail, header, keyValue, blank, divider, TABLE_CHARS } = require('../output');
|
|
8
|
+
const { createSpinner } = require('../spinner');
|
|
9
|
+
const { fmtBounty, fmtDeadline, fmtStatus, fmtCategory, fmtDesc, formatTaskBrief, truncAddr } = require('../format');
|
|
10
|
+
const { fetchWithTimeout } = require('../http');
|
|
11
|
+
|
|
12
|
+
function register(program) {
|
|
13
|
+
program
|
|
14
|
+
.command('status')
|
|
15
|
+
.description('Show your active tasks')
|
|
16
|
+
.option('--address <addr>', 'Wallet address (defaults to .env)')
|
|
17
|
+
.action(async (opts) => {
|
|
18
|
+
try {
|
|
19
|
+
await run(opts);
|
|
20
|
+
} catch (err) {
|
|
21
|
+
fail(normalizeError(err));
|
|
22
|
+
}
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
async function run(opts) {
|
|
27
|
+
const address = resolveAddress(opts.address);
|
|
28
|
+
|
|
29
|
+
const spinner = createSpinner('Fetching tasks…');
|
|
30
|
+
spinner.start();
|
|
31
|
+
|
|
32
|
+
const resp = await fetchWithTimeout(`${config.API_URL}/tasks/worker/${address}`);
|
|
33
|
+
if (!resp.ok) {
|
|
34
|
+
spinner.stop();
|
|
35
|
+
fail(`API error: ${resp.status}`);
|
|
36
|
+
}
|
|
37
|
+
const data = await resp.json();
|
|
38
|
+
const tasks = data.tasks || data || [];
|
|
39
|
+
|
|
40
|
+
const active = tasks.filter(t => t.status === 'Claimed');
|
|
41
|
+
const submitted = tasks.filter(t => t.status === 'Submitted');
|
|
42
|
+
const completed = tasks.filter(t => t.status === 'Completed');
|
|
43
|
+
const disputed = tasks.filter(t => t.status === 'Disputed');
|
|
44
|
+
|
|
45
|
+
const totalEarned = completed.reduce((sum, t) => {
|
|
46
|
+
const bounty = parseFloat(t.bounty_amount || '0');
|
|
47
|
+
const fee = t.discounted_fee ? bounty * 0.02 : bounty * 0.05;
|
|
48
|
+
return sum + (bounty - fee);
|
|
49
|
+
}, 0);
|
|
50
|
+
|
|
51
|
+
spinner.stop();
|
|
52
|
+
|
|
53
|
+
const result = {
|
|
54
|
+
command: 'status',
|
|
55
|
+
dryRun: isDryRun(),
|
|
56
|
+
address,
|
|
57
|
+
summary: {
|
|
58
|
+
active: active.length,
|
|
59
|
+
submitted: submitted.length,
|
|
60
|
+
completed: completed.length,
|
|
61
|
+
disputed: disputed.length,
|
|
62
|
+
totalEarned: totalEarned.toFixed(2),
|
|
63
|
+
},
|
|
64
|
+
tasks: {
|
|
65
|
+
active: active.map(formatTaskBrief),
|
|
66
|
+
submitted: submitted.map(formatTaskBrief),
|
|
67
|
+
completed: completed.map(formatTaskBrief),
|
|
68
|
+
disputed: disputed.map(formatTaskBrief),
|
|
69
|
+
},
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
success(result, () => {
|
|
73
|
+
header('📋', `Task Status`, chalk.dim(truncAddr(address)));
|
|
74
|
+
|
|
75
|
+
// Summary
|
|
76
|
+
const counts = [
|
|
77
|
+
active.length ? chalk.yellow(`${active.length} active`) : null,
|
|
78
|
+
submitted.length ? chalk.blue(`${submitted.length} submitted`) : null,
|
|
79
|
+
completed.length ? chalk.green(`${completed.length} completed`) : null,
|
|
80
|
+
disputed.length ? chalk.red(`${disputed.length} disputed`) : null,
|
|
81
|
+
].filter(Boolean);
|
|
82
|
+
|
|
83
|
+
if (counts.length === 0) {
|
|
84
|
+
console.log(chalk.dim(' No tasks found for this wallet.'));
|
|
85
|
+
blank();
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
console.log(` ${counts.join(' · ')} · ${chalk.green(fmtBounty(totalEarned))} earned`);
|
|
90
|
+
blank();
|
|
91
|
+
|
|
92
|
+
// Active tasks table
|
|
93
|
+
const allActive = [...active, ...submitted, ...disputed];
|
|
94
|
+
if (allActive.length > 0) {
|
|
95
|
+
const table = new Table({
|
|
96
|
+
chars: TABLE_CHARS,
|
|
97
|
+
style: { head: ['cyan'], 'padding-left': 1, 'padding-right': 1 },
|
|
98
|
+
head: ['ID', 'Bounty', 'Status', 'Category', 'Deadline'],
|
|
99
|
+
colWidths: [7, 10, 14, 11, 11],
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
for (const t of allActive) {
|
|
103
|
+
table.push([
|
|
104
|
+
chalk.cyan(`#${t.chain_task_id}`),
|
|
105
|
+
chalk.green(fmtBounty(t.bounty_amount)),
|
|
106
|
+
fmtStatus(t.status),
|
|
107
|
+
fmtCategory(t.category),
|
|
108
|
+
fmtDeadline(t.deadline),
|
|
109
|
+
]);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
console.log(' ' + table.toString().split('\n').join('\n '));
|
|
113
|
+
blank();
|
|
114
|
+
}
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
module.exports = { register };
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const chalk = require('chalk');
|
|
6
|
+
const { requireSDK, isDryRun, normalizeError } = require('../sdk');
|
|
7
|
+
const { success, fail, header, keyValue, blank, hint } = require('../output');
|
|
8
|
+
const { createSpinner } = require('../spinner');
|
|
9
|
+
const { isTextFile } = require('../format');
|
|
10
|
+
|
|
11
|
+
function register(program) {
|
|
12
|
+
program
|
|
13
|
+
.command('submit <chainTaskId>')
|
|
14
|
+
.description('Submit completed work with deliverables')
|
|
15
|
+
.option('--proof <url>', 'URL or hash of deliverable')
|
|
16
|
+
.option('--summary <text>', 'Description of what was delivered')
|
|
17
|
+
.option('--files <paths>', 'Comma-separated file paths to upload')
|
|
18
|
+
.action(async (chainTaskId, opts) => {
|
|
19
|
+
try {
|
|
20
|
+
await run(chainTaskId, opts);
|
|
21
|
+
} catch (err) {
|
|
22
|
+
fail(normalizeError(err));
|
|
23
|
+
}
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
async function run(taskId, opts) {
|
|
28
|
+
if (!opts.proof && !opts.files) {
|
|
29
|
+
fail('Provide --proof=<url> and/or --files=<paths>',
|
|
30
|
+
{ suggestion: 'Example: 0xwork submit 45 --proof="https://x.com/..." or --files=report.md,data.csv' });
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Read files if provided
|
|
34
|
+
const files = [];
|
|
35
|
+
if (opts.files) {
|
|
36
|
+
const filePaths = opts.files.split(',').map(s => s.trim());
|
|
37
|
+
for (const fp of filePaths) {
|
|
38
|
+
const resolved = path.isAbsolute(fp) ? fp : path.resolve(fp);
|
|
39
|
+
if (!fs.existsSync(resolved)) fail(`File not found: ${resolved}`);
|
|
40
|
+
|
|
41
|
+
const content = fs.readFileSync(resolved);
|
|
42
|
+
const name = path.basename(resolved);
|
|
43
|
+
const binary = !isTextFile(resolved);
|
|
44
|
+
|
|
45
|
+
files.push({
|
|
46
|
+
name,
|
|
47
|
+
content: binary ? content.toString('base64') : content.toString('utf-8'),
|
|
48
|
+
binary,
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (isDryRun()) {
|
|
54
|
+
return success({
|
|
55
|
+
command: 'submit',
|
|
56
|
+
dryRun: true,
|
|
57
|
+
taskId: parseInt(taskId),
|
|
58
|
+
message: `DRY RUN: Would submit to task #${taskId}`,
|
|
59
|
+
proof: opts.proof || null,
|
|
60
|
+
files: files.map(f => f.name),
|
|
61
|
+
summary: opts.summary || '',
|
|
62
|
+
txHash: null,
|
|
63
|
+
proofHash: null,
|
|
64
|
+
}, () => {
|
|
65
|
+
header('📤', `Submit Task #${taskId} — Dry Run`);
|
|
66
|
+
hint('Set PRIVATE_KEY in .env to submit work.');
|
|
67
|
+
blank();
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const sdk = requireSDK();
|
|
72
|
+
|
|
73
|
+
// Pre-flight: check task state
|
|
74
|
+
const s1 = createSpinner('Verifying task state…');
|
|
75
|
+
s1.start();
|
|
76
|
+
|
|
77
|
+
const onChain = await sdk.getTask(parseInt(taskId));
|
|
78
|
+
if (onChain.state !== 'Claimed' && onChain.state !== 'Submitted') {
|
|
79
|
+
s1.fail(`Task is ${onChain.state}`);
|
|
80
|
+
fail(`Task #${taskId} is ${onChain.state}, expected Claimed`);
|
|
81
|
+
}
|
|
82
|
+
if (onChain.worker.toLowerCase() !== sdk.address.toLowerCase()) {
|
|
83
|
+
s1.fail('Not your task');
|
|
84
|
+
fail(`Task #${taskId} is claimed by ${onChain.worker}, not ${sdk.address}`);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
s1.succeed('Task verified');
|
|
88
|
+
|
|
89
|
+
// Submit with file upload (SDK handles proof hash generation)
|
|
90
|
+
if (files.length > 0) {
|
|
91
|
+
const s2 = createSpinner(`Uploading ${files.length} file(s)…`);
|
|
92
|
+
s2.start();
|
|
93
|
+
|
|
94
|
+
const result = await sdk.submitDeliverable(parseInt(taskId), {
|
|
95
|
+
files,
|
|
96
|
+
summary: opts.summary || '',
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
s2.succeed('Submitted');
|
|
100
|
+
|
|
101
|
+
return success({
|
|
102
|
+
command: 'submit',
|
|
103
|
+
dryRun: false,
|
|
104
|
+
taskId: parseInt(taskId),
|
|
105
|
+
txHash: result.txHash,
|
|
106
|
+
proofHash: result.proofHash,
|
|
107
|
+
proof: opts.proof || null,
|
|
108
|
+
files: files.map(f => f.name),
|
|
109
|
+
summary: opts.summary || '',
|
|
110
|
+
message: `Submitted ${files.length} file(s) to task #${taskId}`,
|
|
111
|
+
}, () => {
|
|
112
|
+
header('✔', `Task #${taskId} Submitted`);
|
|
113
|
+
keyValue('Files', files.map(f => f.name).join(', '));
|
|
114
|
+
if (opts.proof) keyValue('Proof', opts.proof);
|
|
115
|
+
if (opts.summary) keyValue('Summary', opts.summary);
|
|
116
|
+
keyValue('Proof hash', chalk.dim(result.proofHash));
|
|
117
|
+
blank();
|
|
118
|
+
hint('Waiting for poster approval (auto-approves after 7 days)');
|
|
119
|
+
blank();
|
|
120
|
+
hint(`tx: ${result.txHash}`);
|
|
121
|
+
blank();
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Simple submit with proof URL/hash
|
|
126
|
+
const s2 = createSpinner('Submitting work…');
|
|
127
|
+
s2.start();
|
|
128
|
+
|
|
129
|
+
const result = await sdk.submitWork(parseInt(taskId), opts.proof);
|
|
130
|
+
|
|
131
|
+
s2.succeed('Submitted');
|
|
132
|
+
|
|
133
|
+
success({
|
|
134
|
+
command: 'submit',
|
|
135
|
+
dryRun: false,
|
|
136
|
+
taskId: parseInt(taskId),
|
|
137
|
+
txHash: result.txHash,
|
|
138
|
+
proof: opts.proof,
|
|
139
|
+
summary: opts.summary || '',
|
|
140
|
+
message: `Submitted task #${taskId}`,
|
|
141
|
+
}, () => {
|
|
142
|
+
header('✔', `Task #${taskId} Submitted`);
|
|
143
|
+
keyValue('Proof', opts.proof);
|
|
144
|
+
if (opts.summary) keyValue('Summary', opts.summary);
|
|
145
|
+
blank();
|
|
146
|
+
hint('Waiting for poster approval (auto-approves after 7 days)');
|
|
147
|
+
blank();
|
|
148
|
+
hint(`tx: ${result.txHash}`);
|
|
149
|
+
blank();
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
module.exports = { register };
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const chalk = require('chalk');
|
|
4
|
+
const config = require('../config');
|
|
5
|
+
const { getSDK, getEthers, isDryRun, normalizeError } = require('../sdk');
|
|
6
|
+
const { success, fail, box, keyValue, blank, hint } = require('../output');
|
|
7
|
+
const { createSpinner } = require('../spinner');
|
|
8
|
+
const { getAxobotlPrice, formatAxobotlUsd } = require('../price');
|
|
9
|
+
const { fmtBounty, fmtDeadline, fmtStatus, fmtCategory, fmtAxobotl, fmtDuration, truncAddr } = require('../format');
|
|
10
|
+
const { resolveTask } = require('../resolve');
|
|
11
|
+
const { checkSafety } = require('../safety');
|
|
12
|
+
|
|
13
|
+
function register(program) {
|
|
14
|
+
program
|
|
15
|
+
.command('task <chainTaskId>')
|
|
16
|
+
.description('Get full details for a specific task')
|
|
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
|
+
const spinner = createSpinner('Fetching task…');
|
|
28
|
+
spinner.start();
|
|
29
|
+
|
|
30
|
+
const t = await resolveTask(taskId);
|
|
31
|
+
const price = await getAxobotlPrice();
|
|
32
|
+
|
|
33
|
+
const result = {
|
|
34
|
+
chainTaskId: t.chain_task_id || parseInt(taskId),
|
|
35
|
+
dbId: t.id,
|
|
36
|
+
description: t.description,
|
|
37
|
+
category: t.category,
|
|
38
|
+
bounty: t.bounty_amount,
|
|
39
|
+
stakeRaw: t.stake_amount,
|
|
40
|
+
deadline: t.deadline,
|
|
41
|
+
deadlineHuman: t.deadline ? new Date(t.deadline * 1000).toISOString() : null,
|
|
42
|
+
status: t.status,
|
|
43
|
+
poster: t.poster_address,
|
|
44
|
+
worker: t.worker_address,
|
|
45
|
+
requirements: t.requirements,
|
|
46
|
+
proofHash: t.proof_hash,
|
|
47
|
+
createdAt: t.created_at,
|
|
48
|
+
updatedAt: t.updated_at,
|
|
49
|
+
safetyFlags: checkSafety(t.description),
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
// On-chain enrichment
|
|
53
|
+
if (!isDryRun()) {
|
|
54
|
+
try {
|
|
55
|
+
const sdk = getSDK();
|
|
56
|
+
const ethers = getEthers();
|
|
57
|
+
const onChain = await sdk.getTask(parseInt(taskId));
|
|
58
|
+
result.onChain = {
|
|
59
|
+
state: onChain.state,
|
|
60
|
+
bounty: onChain.bountyAmount,
|
|
61
|
+
stake: onChain.stakeAmount,
|
|
62
|
+
deadline: onChain.deadline,
|
|
63
|
+
};
|
|
64
|
+
const stakeCalc = await sdk.taskPool.calculateStake(
|
|
65
|
+
ethers.parseUnits(String(onChain.bountyAmount), 6)
|
|
66
|
+
);
|
|
67
|
+
result.currentStakeRequired = ethers.formatUnits(stakeCalc, 18);
|
|
68
|
+
} catch (e) {
|
|
69
|
+
result.onChainError = e.message;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Price enrichment
|
|
74
|
+
if (price) {
|
|
75
|
+
if (result.currentStakeRequired) {
|
|
76
|
+
result.currentStakeRequiredUsd = formatAxobotlUsd(result.currentStakeRequired, price);
|
|
77
|
+
}
|
|
78
|
+
if (result.stakeRaw) {
|
|
79
|
+
const ethers = getEthers();
|
|
80
|
+
const stakeNum = parseFloat(ethers.formatUnits(result.stakeRaw, 18));
|
|
81
|
+
result.stakeUsd = formatAxobotlUsd(stakeNum, price);
|
|
82
|
+
}
|
|
83
|
+
result.axobotlPriceUsd = price;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
spinner.stop();
|
|
87
|
+
|
|
88
|
+
success({ command: 'task', task: result }, () => {
|
|
89
|
+
const stakeStr = result.currentStakeRequired
|
|
90
|
+
? `${fmtAxobotl(result.currentStakeRequired)} AXOBOTL`
|
|
91
|
+
: (result.stakeRaw ? `${fmtAxobotl(parseFloat(getEthers().formatUnits(result.stakeRaw, 18)))} AXOBOTL` : '—');
|
|
92
|
+
const stakeUsdStr = result.currentStakeRequiredUsd || result.stakeUsd || '';
|
|
93
|
+
|
|
94
|
+
const lines = [
|
|
95
|
+
'',
|
|
96
|
+
`${chalk.dim('Bounty:'.padEnd(14))} ${chalk.green.bold(fmtBounty(result.bounty) + ' USDC')}`,
|
|
97
|
+
`${chalk.dim('Deadline:'.padEnd(14))} ${fmtDeadline(result.deadline, true)}`,
|
|
98
|
+
`${chalk.dim('Status:'.padEnd(14))} ${fmtStatus(result.status)}`,
|
|
99
|
+
`${chalk.dim('Category:'.padEnd(14))} ${fmtCategory(result.category)}`,
|
|
100
|
+
`${chalk.dim('Poster:'.padEnd(14))} ${truncAddr(result.poster)}`,
|
|
101
|
+
`${chalk.dim('Worker:'.padEnd(14))} ${result.worker ? truncAddr(result.worker) : '—'}`,
|
|
102
|
+
'',
|
|
103
|
+
`${chalk.dim('Stake:'.padEnd(14))} ${stakeStr}${stakeUsdStr ? chalk.dim(` (~${stakeUsdStr})`) : ''}`,
|
|
104
|
+
];
|
|
105
|
+
|
|
106
|
+
if (result.proofHash) {
|
|
107
|
+
lines.push(`${chalk.dim('Proof:'.padEnd(14))} ${chalk.dim(result.proofHash)}`);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
lines.push('');
|
|
111
|
+
|
|
112
|
+
// Description (word-wrapped)
|
|
113
|
+
if (result.description) {
|
|
114
|
+
lines.push(chalk.dim('Description:'));
|
|
115
|
+
const descLines = result.description.split('\n');
|
|
116
|
+
for (const dl of descLines) {
|
|
117
|
+
const words = dl.split(' ');
|
|
118
|
+
let line = '';
|
|
119
|
+
for (const w of words) {
|
|
120
|
+
if (line.length + w.length + 1 > 56) {
|
|
121
|
+
lines.push(` ${line}`);
|
|
122
|
+
line = w;
|
|
123
|
+
} else {
|
|
124
|
+
line = line ? `${line} ${w}` : w;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
if (line) lines.push(` ${line}`);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
if (result.safetyFlags && result.safetyFlags.length > 0) {
|
|
132
|
+
lines.push('');
|
|
133
|
+
lines.push(chalk.yellow(`⚠ Safety flags: ${result.safetyFlags.join(', ')}`));
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
box(`Task #${result.chainTaskId} — ${result.category || 'Task'}`, lines);
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
module.exports = { register };
|
package/src/config.js
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
|
|
6
|
+
// ─── .env loader (zero deps) ───────────────────────────────────────
|
|
7
|
+
// Walks from CWD upward looking for .env files.
|
|
8
|
+
// CWD .env overrides system env (agent's local config wins).
|
|
9
|
+
// Parent .env files are fallback only (don't override).
|
|
10
|
+
|
|
11
|
+
function loadEnv() {
|
|
12
|
+
let dir = process.cwd();
|
|
13
|
+
const root = path.parse(dir).root;
|
|
14
|
+
let first = true;
|
|
15
|
+
while (dir !== root) {
|
|
16
|
+
const envPath = path.join(dir, '.env');
|
|
17
|
+
if (fs.existsSync(envPath)) {
|
|
18
|
+
parseEnvFile(envPath, first);
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
first = false;
|
|
22
|
+
dir = path.dirname(dir);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function parseEnvFile(envPath, override = false) {
|
|
27
|
+
const content = fs.readFileSync(envPath, 'utf-8');
|
|
28
|
+
for (const line of content.split('\n')) {
|
|
29
|
+
const trimmed = line.trim();
|
|
30
|
+
if (!trimmed || trimmed.startsWith('#')) continue;
|
|
31
|
+
const eqIdx = trimmed.indexOf('=');
|
|
32
|
+
if (eqIdx < 0) continue;
|
|
33
|
+
const key = trimmed.slice(0, eqIdx).trim();
|
|
34
|
+
let val = trimmed.slice(eqIdx + 1).trim();
|
|
35
|
+
if ((val.startsWith('"') && val.endsWith('"')) ||
|
|
36
|
+
(val.startsWith("'") && val.endsWith("'"))) {
|
|
37
|
+
val = val.slice(1, -1);
|
|
38
|
+
}
|
|
39
|
+
if (override || !process.env[key]) process.env[key] = val;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Load on require
|
|
44
|
+
loadEnv();
|
|
45
|
+
|
|
46
|
+
// ─── Configuration ──────────────────────────────────────────────────
|
|
47
|
+
|
|
48
|
+
const API_URL = process.env.API_URL || 'https://api.0xwork.org';
|
|
49
|
+
const RPC_URL = process.env.RPC_URL || process.env.BASE_MAINNET_RPC || process.env.BASE_RPC_URL || 'https://mainnet.base.org';
|
|
50
|
+
const PRIVATE_KEY = process.env.PRIVATE_KEY || process.env.DEPLOYER_PRIVATE_KEY || null;
|
|
51
|
+
const WALLET_ADDRESS = process.env.WALLET_ADDRESS || process.env.DEPLOYER_ADDRESS || null;
|
|
52
|
+
|
|
53
|
+
module.exports = {
|
|
54
|
+
API_URL,
|
|
55
|
+
RPC_URL,
|
|
56
|
+
PRIVATE_KEY,
|
|
57
|
+
WALLET_ADDRESS,
|
|
58
|
+
loadEnv,
|
|
59
|
+
};
|