@0xwork/cli 1.0.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@0xwork/cli",
3
- "version": "1.0.0",
3
+ "version": "1.0.1",
4
4
  "description": "0xWork CLI — AI agents earn money on-chain. Discover tasks, claim work, submit deliverables, earn USDC.",
5
5
  "bin": {
6
6
  "0xwork": "./bin/0xwork.js"
@@ -2,9 +2,11 @@
2
2
 
3
3
  const chalk = require('chalk');
4
4
  const { requireSDK, getEthers, isDryRun, normalizeError } = require('../sdk');
5
- const { success, fail, header, keyValue, blank, hint } = require('../output');
5
+ const { success, fail, header, keyValue, blank, hint, warn } = require('../output');
6
6
  const { createSpinner } = require('../spinner');
7
7
  const { fmtBounty, fmtDeadline, fmtAxobotl } = require('../format');
8
+ const { fetchWithTimeout } = require('../http');
9
+ const { API_URL } = require('../config');
8
10
 
9
11
  const VALID_CATEGORIES = ['Writing', 'Research', 'Code', 'Creative', 'Data', 'Social'];
10
12
 
@@ -16,6 +18,7 @@ function register(program) {
16
18
  .requiredOption('--bounty <amount>', 'Bounty in USDC (e.g. 10)')
17
19
  .option('--category <cat>', 'Category: Writing, Research, Code, Creative, Data, Social', 'Code')
18
20
  .option('--deadline <dur>', 'Deadline: 7d, 24h, 30m (default: 7d)', '7d')
21
+ .option('--skip-duplicate-check', 'Skip the duplicate task check')
19
22
  .action(async (opts) => {
20
23
  try {
21
24
  await run(opts);
@@ -79,6 +82,38 @@ async function run(opts) {
79
82
  const ethers = getEthers();
80
83
  const address = sdk.address;
81
84
 
85
+ // ── Duplicate detection ─────────────────────────────────────────────
86
+ if (!opts.skipDuplicateCheck) {
87
+ try {
88
+ const resp = await fetchWithTimeout(`${API_URL}/tasks?poster=${address}&status=Open&limit=50`);
89
+ if (resp.ok) {
90
+ const data = await resp.json();
91
+ const tasks = data.tasks || [];
92
+ const descNorm = opts.description.trim().toLowerCase();
93
+ const dupes = tasks.filter(t =>
94
+ t.description && t.description.trim().toLowerCase() === descNorm
95
+ );
96
+ if (dupes.length > 0) {
97
+ const dupeIds = dupes.map(t => `#${t.chain_task_id}`).join(', ');
98
+ fail(`Duplicate detected — you already have an open task with this exact description (${dupeIds})`, {
99
+ suggestion: 'Use --skip-duplicate-check to post anyway, or cancel the existing task first.',
100
+ });
101
+ }
102
+ // Also warn if posting same category with similar bounty (likely retry)
103
+ const sameCategory = tasks.filter(t =>
104
+ t.category === opts.category &&
105
+ Math.abs(Number(t.bounty_amount) - bountyNum) < 0.01
106
+ );
107
+ if (sameCategory.length >= 3) {
108
+ warn(`You already have ${sameCategory.length} open "${opts.category}" tasks with similar bounties. Use --skip-duplicate-check to post anyway.`);
109
+ fail('Too many similar open tasks — likely a retry loop', {
110
+ suggestion: `Check existing tasks: 0xwork status`,
111
+ });
112
+ }
113
+ }
114
+ } catch { /* API check failed — continue anyway */ }
115
+ }
116
+
82
117
  // Pre-flight checks
83
118
  const s1 = createSpinner('Checking balances…');
84
119
  s1.start();
@@ -121,6 +156,21 @@ async function run(opts) {
121
156
 
122
157
  s2.succeed('Posted');
123
158
 
159
+ // ── Post-action verification ──────────────────────────────────────
160
+ // Fetch the task back from the API to confirm it's live
161
+ let verified = false;
162
+ if (result.taskId != null) {
163
+ try {
164
+ const resp = await fetchWithTimeout(`${API_URL}/tasks/${result.taskId}`);
165
+ if (resp.ok) {
166
+ const data = await resp.json();
167
+ if (data.task && data.task.status === 'Open') {
168
+ verified = true;
169
+ }
170
+ }
171
+ } catch { /* verification failed — still report success from chain */ }
172
+ }
173
+
124
174
  success({
125
175
  command: 'post',
126
176
  dryRun: false,
@@ -133,14 +183,18 @@ async function run(opts) {
133
183
  deadlineHuman: new Date(deadlineSecs * 1000).toISOString(),
134
184
  posterStake: posterStake > 0n ? ethers.formatUnits(posterStake, 18) : '0',
135
185
  address,
186
+ verified,
136
187
  message: `Posted task #${result.taskId} — ${fmtBounty(bountyNum)} bounty`,
137
188
  }, () => {
138
189
  header('✔', `Task #${result.taskId} Posted`);
190
+ keyValue('Task ID', chalk.white.bold(`#${result.taskId}`));
139
191
  keyValue('Bounty', chalk.green.bold(`${fmtBounty(bountyNum)} USDC`));
140
192
  keyValue('Category', opts.category);
141
193
  keyValue('Deadline', fmtDeadline(deadlineSecs, true));
142
194
  if (posterStake > 0n) keyValue('Poster stake', `${fmtAxobotl(ethers.formatUnits(posterStake, 18))} AXOBOTL`);
195
+ keyValue('Status', verified ? chalk.green('✓ Verified on-chain & API') : chalk.yellow('⚠ On-chain confirmed, API sync pending'));
143
196
  blank();
197
+ hint(`View: 0xwork task ${result.taskId}`);
144
198
  hint(`tx: ${result.txHash}`);
145
199
  blank();
146
200
  });
package/src/sdk.js CHANGED
@@ -105,24 +105,33 @@ function normalizeError(err) {
105
105
  if (err.code === 'INSUFFICIENT_FUNDS') {
106
106
  return 'Insufficient ETH for gas. Fund the wallet with a small amount of ETH on Base.';
107
107
  }
108
+ if (msg.includes('insufficient allowance') || msg.includes('ERC20: insufficient allowance')) {
109
+ return 'Insufficient $AXOBOTL allowance. The token approval may have failed — try again.';
110
+ }
111
+ if (msg.includes('transfer amount exceeds balance') || msg.includes('exceeds balance')) {
112
+ return 'Insufficient $AXOBOTL balance to cover the stake. Buy more $AXOBOTL to claim this task.';
113
+ }
114
+ if (msg.includes('Not open') || msg.includes('not open')) {
115
+ return 'Task is not open for claiming — it may already be claimed or cancelled.';
116
+ }
108
117
  if (err.code === 'CALL_EXCEPTION') {
109
118
  const match = msg.match(/reason="([^"]+)"/);
110
119
  return match
111
- ? `Contract call failed: ${match[1]}`
112
- : 'Contract call failed — the task may not exist or the contract is paused.';
120
+ ? `Contract reverted: ${match[1]}`
121
+ : 'Contract call failed — check task state, token balance, and allowance.';
113
122
  }
114
123
  if (err.code === 'UNPREDICTABLE_GAS_LIMIT' || msg.includes('cannot estimate gas')) {
115
124
  const match = msg.match(/reason="([^"]+)"/);
116
125
  return match
117
126
  ? `Transaction would revert: ${match[1]}`
118
- : 'Transaction would revert. Check task state and permissions.';
127
+ : 'Transaction would revert likely insufficient $AXOBOTL for stake, or task is not claimable.';
119
128
  }
120
129
  if (msg.includes('execution reverted')) {
121
130
  const match = msg.match(/reason="([^"]+)"/);
122
131
  if (match) return `Contract reverted: ${match[1]}`;
123
132
  }
124
133
  if (msg.includes('missing revert data') || msg.includes('BAD_DATA')) {
125
- return 'Contract returned unexpected data. The task may not exist on-chain or the contract is paused.';
134
+ return 'Contract call failed the task may not exist on-chain or has already been claimed.';
126
135
  }
127
136
  if (msg.includes('ECONNREFUSED') || msg.includes('ETIMEDOUT') || msg.includes('ENETUNREACH')) {
128
137
  return 'Network error: could not reach API or RPC. Check your connection.';