@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/src/format.js ADDED
@@ -0,0 +1,157 @@
1
+ 'use strict';
2
+
3
+ const chalk = require('chalk');
4
+
5
+ /**
6
+ * Truncate address: 0x84631A26Fab...9C → 0x8463...Dc9C
7
+ */
8
+ function truncAddr(addr) {
9
+ if (!addr || addr.length < 12) return addr || '—';
10
+ return `${addr.slice(0, 6)}…${addr.slice(-4)}`;
11
+ }
12
+
13
+ /**
14
+ * Truncate tx hash: 0xabc123...def → 0xabc123…def4
15
+ */
16
+ function truncTx(hash) {
17
+ if (!hash || hash.length < 16) return hash || '—';
18
+ return `${hash.slice(0, 10)}…${hash.slice(-4)}`;
19
+ }
20
+
21
+ /**
22
+ * Format bounty amount: "50.0" → "$50.00"
23
+ */
24
+ function fmtBounty(amount) {
25
+ if (amount == null) return '—';
26
+ const n = typeof amount === 'string' ? parseFloat(amount) : amount;
27
+ if (isNaN(n)) return '—';
28
+ return `$${n.toFixed(2)}`;
29
+ }
30
+
31
+ /**
32
+ * Format bounty amount with chalk green.
33
+ */
34
+ function fmtBountyColored(amount) {
35
+ return chalk.green(fmtBounty(amount));
36
+ }
37
+
38
+ /**
39
+ * Format AXOBOTL token amount: 440626.5 → "440,627"
40
+ */
41
+ function fmtAxobotl(amount) {
42
+ if (amount == null) return '—';
43
+ const n = typeof amount === 'string' ? parseFloat(amount) : amount;
44
+ if (isNaN(n)) return '—';
45
+ return Math.round(n).toLocaleString('en-US');
46
+ }
47
+
48
+ /**
49
+ * Format deadline from unix timestamp.
50
+ * @param {number} ts — unix seconds
51
+ * @param {boolean} showRemaining — include time remaining
52
+ */
53
+ function fmtDeadline(ts, showRemaining = false) {
54
+ if (!ts) return '—';
55
+ const date = new Date(ts * 1000);
56
+ const months = ['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec'];
57
+ const str = `${months[date.getMonth()]} ${date.getDate()}`;
58
+
59
+ if (!showRemaining) return str;
60
+
61
+ const now = Math.floor(Date.now() / 1000);
62
+ const diff = ts - now;
63
+ if (diff <= 0) return `${str} ${chalk.red('(expired)')}`;
64
+ return `${str} (${fmtDuration(diff)} remaining)`;
65
+ }
66
+
67
+ /**
68
+ * Format seconds as human duration: 5d 4h, 48h 30m, 2h 15m, 45m
69
+ */
70
+ function fmtDuration(seconds) {
71
+ if (seconds <= 0) return '0m';
72
+ const d = Math.floor(seconds / 86400);
73
+ const h = Math.floor((seconds % 86400) / 3600);
74
+ const m = Math.floor((seconds % 3600) / 60);
75
+ if (d > 0) return `${d}d ${h}h`;
76
+ if (h > 0) return `${h}h ${m}m`;
77
+ return `${m}m`;
78
+ }
79
+
80
+ /**
81
+ * Color-coded task status.
82
+ */
83
+ function fmtStatus(status) {
84
+ const map = {
85
+ Open: chalk.green('● Open'),
86
+ Claimed: chalk.yellow('● Claimed'),
87
+ Submitted: chalk.blue('● Submitted'),
88
+ Completed: chalk.green('● Completed'),
89
+ Disputed: chalk.red('● Disputed'),
90
+ Cancelled: chalk.dim('● Cancelled'),
91
+ };
92
+ return map[status] || status;
93
+ }
94
+
95
+ /**
96
+ * Color-coded category.
97
+ */
98
+ function fmtCategory(cat) {
99
+ return chalk.magenta(cat || '—');
100
+ }
101
+
102
+ /**
103
+ * Format a description for table display (truncate).
104
+ */
105
+ function fmtDesc(desc, maxLen = 38) {
106
+ if (!desc) return '—';
107
+ const oneLine = desc.replace(/\n/g, ' ').trim();
108
+ if (oneLine.length <= maxLen) return oneLine;
109
+ return oneLine.slice(0, maxLen - 1) + '…';
110
+ }
111
+
112
+ /**
113
+ * Format a brief task object for status command.
114
+ */
115
+ function formatTaskBrief(t) {
116
+ return {
117
+ chainTaskId: t.chain_task_id,
118
+ description: t.description ? t.description.slice(0, 200) : null,
119
+ category: t.category,
120
+ bounty: t.bounty_amount,
121
+ status: t.status,
122
+ deadline: t.deadline,
123
+ deadlineHuman: t.deadline ? new Date(t.deadline * 1000).toISOString() : null,
124
+ };
125
+ }
126
+
127
+ /**
128
+ * Text extension set for file type detection.
129
+ */
130
+ const TEXT_EXTENSIONS = new Set([
131
+ '.md', '.txt', '.json', '.js', '.ts', '.jsx', '.tsx', '.py', '.rb',
132
+ '.html', '.css', '.csv', '.xml', '.yaml', '.yml', '.toml', '.sh',
133
+ '.sol', '.rs', '.go', '.java', '.c', '.cpp', '.h', '.hpp', '.sql',
134
+ '.env', '.gitignore', '.dockerfile', '.makefile', '.cfg', '.ini',
135
+ ]);
136
+
137
+ function isTextFile(filePath) {
138
+ const path = require('path');
139
+ const ext = path.extname(filePath).toLowerCase();
140
+ return TEXT_EXTENSIONS.has(ext) || ext === '';
141
+ }
142
+
143
+ module.exports = {
144
+ truncAddr,
145
+ truncTx,
146
+ fmtBounty,
147
+ fmtBountyColored,
148
+ fmtAxobotl,
149
+ fmtDeadline,
150
+ fmtDuration,
151
+ fmtStatus,
152
+ fmtCategory,
153
+ fmtDesc,
154
+ formatTaskBrief,
155
+ isTextFile,
156
+ TEXT_EXTENSIONS,
157
+ };
package/src/http.js ADDED
@@ -0,0 +1,38 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Fetch wrapper with timeout + consistent error messages.
5
+ * Drop-in replacement for global fetch() across the CLI.
6
+ */
7
+
8
+ const DEFAULT_TIMEOUT_MS = 15000; // 15s for API calls
9
+ const PRICE_TIMEOUT_MS = 10000; // 10s for price lookups
10
+
11
+ /**
12
+ * Fetch with AbortController timeout.
13
+ * @param {string} url
14
+ * @param {object} [opts] — standard fetch options
15
+ * @param {number} [opts.timeoutMs] — override timeout (ms)
16
+ * @returns {Promise<Response>}
17
+ */
18
+ async function fetchWithTimeout(url, opts = {}) {
19
+ const { timeoutMs, ...fetchOpts } = opts;
20
+ const ms = timeoutMs || DEFAULT_TIMEOUT_MS;
21
+
22
+ const controller = new AbortController();
23
+ const timer = setTimeout(() => controller.abort(), ms);
24
+
25
+ try {
26
+ const resp = await fetch(url, { ...fetchOpts, signal: controller.signal });
27
+ return resp;
28
+ } catch (err) {
29
+ if (err.name === 'AbortError') {
30
+ throw new Error(`Request timed out after ${Math.round(ms / 1000)}s: ${url}`);
31
+ }
32
+ throw err;
33
+ } finally {
34
+ clearTimeout(timer);
35
+ }
36
+ }
37
+
38
+ module.exports = { fetchWithTimeout, DEFAULT_TIMEOUT_MS, PRICE_TIMEOUT_MS };
package/src/index.js ADDED
@@ -0,0 +1,82 @@
1
+ 'use strict';
2
+
3
+ const { Command } = require('commander');
4
+ const chalk = require('chalk');
5
+ const { setMode } = require('./output');
6
+ const pkg = require('../package.json');
7
+
8
+ const program = new Command();
9
+
10
+ program
11
+ .name('0xwork')
12
+ .description(chalk.bold('0xWork CLI') + ' — AI agents earn money on the 0xWork protocol')
13
+ .version(pkg.version, '-v, --version')
14
+ .option('--json', 'Output raw JSON (for AI agents and scripting)')
15
+ .option('--quiet', 'Minimal output (success/fail + tx hash)')
16
+ .option('--no-color', 'Disable colored output')
17
+ .hook('preAction', (thisCommand) => {
18
+ const opts = thisCommand.optsWithGlobals();
19
+ if (opts.json) setMode('json');
20
+ else if (opts.quiet) setMode('quiet');
21
+ if (opts.color === false) {
22
+ // chalk v4 respects FORCE_COLOR=0
23
+ process.env.FORCE_COLOR = '0';
24
+ }
25
+ });
26
+
27
+ // ─── Setup ──────────────────────────────────────────────────────────
28
+ require('./commands/init').register(program);
29
+ require('./commands/register').register(program);
30
+
31
+ // ─── Worker Flow ────────────────────────────────────────────────────
32
+ require('./commands/discover').register(program);
33
+ require('./commands/task').register(program);
34
+ require('./commands/claim').register(program);
35
+ require('./commands/submit').register(program);
36
+ require('./commands/abandon').register(program);
37
+
38
+ // ─── Poster Flow ────────────────────────────────────────────────────
39
+ require('./commands/post').register(program);
40
+ require('./commands/approve').register(program);
41
+ require('./commands/reject').register(program);
42
+ require('./commands/revision').register(program);
43
+ require('./commands/cancel').register(program);
44
+ require('./commands/extend').register(program);
45
+
46
+ // ─── V3 Fairness (anyone can trigger) ──────────────────────────────
47
+ require('./commands/claim-approval').register(program);
48
+ require('./commands/auto-resolve').register(program);
49
+ require('./commands/mutual-cancel').register(program);
50
+ require('./commands/retract-cancel').register(program);
51
+ require('./commands/reclaim').register(program);
52
+
53
+ // ─── Info ───────────────────────────────────────────────────────────
54
+ require('./commands/status').register(program);
55
+ require('./commands/balance').register(program);
56
+ require('./commands/profile').register(program);
57
+ require('./commands/faucet').register(program);
58
+
59
+ // ─── Custom help ────────────────────────────────────────────────────
60
+ program.addHelpText('after', `
61
+ ${chalk.dim('Examples:')}
62
+ ${chalk.dim('$')} 0xwork init ${chalk.dim('# Generate wallet')}
63
+ ${chalk.dim('$')} 0xwork register --name="Bot" --description="…" ${chalk.dim('# Register on-chain')}
64
+ ${chalk.dim('$')} 0xwork discover --capabilities=Writing ${chalk.dim('# Find matching tasks')}
65
+ ${chalk.dim('$')} 0xwork claim 45 ${chalk.dim('# Claim a task')}
66
+ ${chalk.dim('$')} 0xwork submit 45 --proof="https://…" ${chalk.dim('# Submit deliverables')}
67
+ ${chalk.dim('$')} 0xwork balance ${chalk.dim('# Check balances')}
68
+ ${chalk.dim('$')} 0xwork discover --json ${chalk.dim('# JSON output for agents')}
69
+
70
+ ${chalk.dim('Environment:')}
71
+ ${chalk.dim('PRIVATE_KEY')} Wallet private key (enables on-chain operations)
72
+ ${chalk.dim('WALLET_ADDRESS')} Read-only address (for status/balance without key)
73
+ ${chalk.dim('API_URL')} 0xWork API (default: https://api.0xwork.org)
74
+ ${chalk.dim('RPC_URL')} Base RPC (default: https://mainnet.base.org)
75
+
76
+ ${chalk.dim('Without PRIVATE_KEY, runs in dry-run mode (read-only).')}
77
+ ${chalk.dim('Reads .env from current directory automatically.')}
78
+
79
+ ${chalk.dim('Docs:')} https://0xwork.org ${chalk.dim('|')} ${chalk.dim('GitHub:')} https://github.com/JKILLR/0xwork
80
+ `);
81
+
82
+ module.exports = program;
package/src/output.js ADDED
@@ -0,0 +1,182 @@
1
+ 'use strict';
2
+
3
+ const chalk = require('chalk');
4
+
5
+ let _mode = 'human'; // 'human' | 'json' | 'quiet'
6
+
7
+ function setMode(mode) {
8
+ _mode = mode;
9
+ }
10
+
11
+ function getMode() {
12
+ return _mode;
13
+ }
14
+
15
+ function isHuman() { return _mode === 'human'; }
16
+ function isJson() { return _mode === 'json'; }
17
+ function isQuiet() { return _mode === 'quiet'; }
18
+
19
+ /**
20
+ * Output success.
21
+ * @param {Object} data — JSON-serializable data (always used for --json)
22
+ * @param {Function} [humanFn] — Called in human mode with (data). Print formatted output.
23
+ */
24
+ function success(data, humanFn) {
25
+ if (_mode === 'json') {
26
+ process.stdout.write(JSON.stringify({ ok: true, ...data }) + '\n');
27
+ process.exit(0);
28
+ }
29
+ if (_mode === 'quiet') {
30
+ const msg = data.txHash || data.message || 'OK';
31
+ console.log(msg);
32
+ process.exit(0);
33
+ }
34
+ // Human mode
35
+ if (humanFn) {
36
+ humanFn(data);
37
+ }
38
+ process.exit(0);
39
+ }
40
+
41
+ /**
42
+ * Output failure and exit with code 1.
43
+ */
44
+ function fail(error, extra = {}) {
45
+ if (_mode === 'json') {
46
+ process.stdout.write(JSON.stringify({ ok: false, error: String(error), ...extra }) + '\n');
47
+ process.exit(1);
48
+ }
49
+ if (_mode === 'quiet') {
50
+ console.error(String(error));
51
+ process.exit(1);
52
+ }
53
+ console.error('');
54
+ console.error(chalk.red(` ✖ ${error}`));
55
+ if (extra.suggestion) {
56
+ console.error(chalk.dim(` ${extra.suggestion}`));
57
+ }
58
+ console.error('');
59
+ process.exit(1);
60
+ }
61
+
62
+ /**
63
+ * Print a styled header line.
64
+ */
65
+ function header(emoji, text, right) {
66
+ if (_mode !== 'human') return;
67
+ let line = ` ${emoji} ${chalk.bold(text)}`;
68
+ if (right) {
69
+ line += chalk.dim(` ${right}`);
70
+ }
71
+ console.log('');
72
+ console.log(line);
73
+ console.log('');
74
+ }
75
+
76
+ /**
77
+ * Print a key-value pair with consistent alignment.
78
+ */
79
+ function keyValue(key, value, indent = 4) {
80
+ if (_mode !== 'human') return;
81
+ const pad = ' '.repeat(indent);
82
+ const label = chalk.dim(`${key}:`.padEnd(16));
83
+ console.log(`${pad}${label}${value}`);
84
+ }
85
+
86
+ /**
87
+ * Print a blank line (human mode only).
88
+ */
89
+ function blank() {
90
+ if (_mode === 'human') console.log('');
91
+ }
92
+
93
+ /**
94
+ * Print dim helper text. Text is automatically dimmed —
95
+ * callers can pass chalk-styled text for partial emphasis within the dim line.
96
+ */
97
+ function hint(text, indent = 4) {
98
+ if (_mode !== 'human') return;
99
+ const pad = ' '.repeat(indent);
100
+ console.log(`${pad}${chalk.dim(text)}`);
101
+ }
102
+
103
+ /**
104
+ * Print a warning line. Unlike hint(), NOT dimmed — warnings stay visible.
105
+ */
106
+ function warn(text, indent = 4) {
107
+ if (_mode !== 'human') return;
108
+ const pad = ' '.repeat(indent);
109
+ console.log(`${pad}${text}`);
110
+ }
111
+
112
+ /**
113
+ * Print a section divider.
114
+ */
115
+ function divider(width = 40, indent = 4) {
116
+ if (_mode !== 'human') return;
117
+ const pad = ' '.repeat(indent);
118
+ console.log(chalk.dim(`${pad}${'─'.repeat(width)}`));
119
+ }
120
+
121
+ /**
122
+ * Box drawing for detail views.
123
+ */
124
+ function box(title, lines, width = 60) {
125
+ if (_mode !== 'human') return;
126
+ const top = ` ╔${'═'.repeat(width)}╗`;
127
+ const sep = ` ╠${'═'.repeat(width)}╣`;
128
+ const bottom = ` ╚${'═'.repeat(width)}╝`;
129
+
130
+ // Pad title by visible length, not raw string length (ANSI-safe)
131
+ const titleText = ` ${title}`;
132
+ const titleVisible = stripAnsi(titleText);
133
+ const titlePad = Math.max(0, width - titleVisible.length);
134
+
135
+ console.log('');
136
+ console.log(chalk.cyan(top));
137
+ console.log(chalk.cyan(' ║') + chalk.bold(titleText) + ' '.repeat(titlePad) + chalk.cyan('║'));
138
+ console.log(chalk.cyan(sep));
139
+ for (const line of lines) {
140
+ const visible = stripAnsi(line);
141
+ const padding = Math.max(0, width - visible.length - 2);
142
+ console.log(chalk.cyan(' ║') + ` ${line}${' '.repeat(padding)}` + chalk.cyan('║'));
143
+ }
144
+ console.log(chalk.cyan(bottom));
145
+ console.log('');
146
+ }
147
+
148
+ /**
149
+ * Standard table character set. Import and spread into cli-table3 options.
150
+ */
151
+ const TABLE_CHARS = {
152
+ 'top': '─', 'top-mid': '┬', 'top-left': '┌', 'top-right': '┐',
153
+ 'bottom': '─', 'bottom-mid': '┴', 'bottom-left': '└', 'bottom-right': '┘',
154
+ 'left': '│', 'left-mid': '├', 'mid': '─', 'mid-mid': '┼',
155
+ 'right': '│', 'right-mid': '┤', 'middle': '│',
156
+ };
157
+
158
+ /**
159
+ * Strip ANSI escape codes for length calculation.
160
+ */
161
+ function stripAnsi(str) {
162
+ return str.replace(/\x1B\[[0-9;]*[a-zA-Z]/g, '');
163
+ }
164
+
165
+ module.exports = {
166
+ setMode,
167
+ getMode,
168
+ isHuman,
169
+ isJson,
170
+ isQuiet,
171
+ success,
172
+ fail,
173
+ header,
174
+ keyValue,
175
+ blank,
176
+ hint,
177
+ divider,
178
+ box,
179
+ warn,
180
+ stripAnsi,
181
+ TABLE_CHARS,
182
+ };
package/src/price.js ADDED
@@ -0,0 +1,41 @@
1
+ 'use strict';
2
+
3
+ const { fetchWithTimeout, PRICE_TIMEOUT_MS } = require('./http');
4
+
5
+ let _cachedPrice = null;
6
+ let _priceTs = 0;
7
+ const PRICE_TTL_MS = 5 * 60 * 1000; // 5 min cache
8
+
9
+ const AXOBOTL_ADDRESS = '0x12cfb53c685Ee7e3F8234d60f20478A1739Ecba3';
10
+
11
+ /**
12
+ * Fetch $AXOBOTL price in USD from DEXScreener (cached 5 min).
13
+ */
14
+ async function getAxobotlPrice() {
15
+ if (_cachedPrice && Date.now() - _priceTs < PRICE_TTL_MS) return _cachedPrice;
16
+ try {
17
+ const resp = await fetchWithTimeout(`https://api.dexscreener.com/latest/dex/tokens/${AXOBOTL_ADDRESS}`, { timeoutMs: PRICE_TIMEOUT_MS });
18
+ if (!resp.ok) return null;
19
+ const data = await resp.json();
20
+ const pair = data.pairs && data.pairs[0];
21
+ if (pair && pair.priceUsd) {
22
+ _cachedPrice = parseFloat(pair.priceUsd);
23
+ _priceTs = Date.now();
24
+ return _cachedPrice;
25
+ }
26
+ } catch { /* best effort */ }
27
+ return null;
28
+ }
29
+
30
+ /**
31
+ * Format an $AXOBOTL amount as USD string.
32
+ */
33
+ function formatAxobotlUsd(amount, price) {
34
+ if (!price || amount == null) return null;
35
+ const num = typeof amount === 'string' ? parseFloat(amount) : amount;
36
+ if (isNaN(num)) return null;
37
+ const usd = num * price;
38
+ return usd < 0.01 ? '<$0.01' : `$${usd.toFixed(2)}`;
39
+ }
40
+
41
+ module.exports = { getAxobotlPrice, formatAxobotlUsd };
package/src/resolve.js ADDED
@@ -0,0 +1,32 @@
1
+ 'use strict';
2
+
3
+ const config = require('./config');
4
+ const { fetchWithTimeout } = require('./http');
5
+
6
+ /**
7
+ * Resolve a task by chain_task_id.
8
+ * Tries direct API lookup first, then falls back to scanning all tasks.
9
+ * This handles the case where DB id != chain_task_id.
10
+ */
11
+ async function resolveTask(taskId) {
12
+ const url = config.API_URL;
13
+
14
+ // Try direct lookup first
15
+ let resp = await fetchWithTimeout(`${url}/tasks/${taskId}`);
16
+ if (resp.ok) {
17
+ const data = await resp.json();
18
+ const t = data.task || data;
19
+ if (t && t.id && String(t.chain_task_id) === String(taskId)) return t;
20
+ }
21
+
22
+ // Fallback: scan all tasks to find by chain_task_id
23
+ resp = await fetchWithTimeout(`${url}/tasks?status=all&limit=200`);
24
+ if (!resp.ok) throw new Error(`API error: ${resp.status} ${resp.statusText}`);
25
+ const data = await resp.json();
26
+ const tasks = data.tasks || data;
27
+ const t = tasks.find(task => String(task.chain_task_id) === String(taskId));
28
+ if (!t) throw new Error(`Task #${taskId} not found (checked both DB id and chain_task_id)`);
29
+ return t;
30
+ }
31
+
32
+ module.exports = { resolveTask };
package/src/safety.js ADDED
@@ -0,0 +1,21 @@
1
+ 'use strict';
2
+
3
+ const REJECT_KEYWORDS = [
4
+ 'personal information', 'doxx', 'hack', 'exploit', 'steal',
5
+ 'impersonate', 'pretend to be', 'fake identity', 'scam',
6
+ 'password', 'private key', 'seed phrase', 'credentials',
7
+ 'illegal', 'nsfw', 'adult content', 'violence',
8
+ 'real-world', 'physical', 'in person', 'meet me',
9
+ ];
10
+
11
+ /**
12
+ * Check task description for safety flags.
13
+ * Returns array of matched keywords (empty = safe).
14
+ */
15
+ function checkSafety(description) {
16
+ if (!description) return [];
17
+ const desc = description.toLowerCase();
18
+ return REJECT_KEYWORDS.filter(kw => desc.includes(kw));
19
+ }
20
+
21
+ module.exports = { checkSafety, REJECT_KEYWORDS };