@0xwork/cli 1.0.1 → 1.0.2
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/bin/0xwork-legacy.js +1233 -0
- package/bin/0xwork.js +2 -1273
- package/package.json +2 -2
- package/src/auth-fetch.js +98 -0
- package/src/commands/contact.js +191 -0
- package/src/commands/portfolio.js +236 -0
- package/src/commands/post.js +32 -55
- package/src/commands/profile.js +29 -0
- package/src/commands/service.js +287 -0
- package/src/index.js +14 -2
- package/src/sdk.js +4 -13
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@0xwork/cli",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.2",
|
|
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"
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
"ora": "^5.4.1"
|
|
22
22
|
},
|
|
23
23
|
"peerDependencies": {
|
|
24
|
-
"@0xwork/sdk": "
|
|
24
|
+
"@0xwork/sdk": "^0.5.3"
|
|
25
25
|
},
|
|
26
26
|
"keywords": [
|
|
27
27
|
"0xwork",
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Authenticated API requests for the 0xWork CLI.
|
|
5
|
+
*
|
|
6
|
+
* Signs requests with the wallet's private key using EIP-191,
|
|
7
|
+
* matching the auth pattern the frontend uses. Requires PRIVATE_KEY.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const crypto = require('crypto');
|
|
11
|
+
const config = require('./config');
|
|
12
|
+
const { getEthers } = require('./sdk');
|
|
13
|
+
const { fetchWithTimeout } = require('./http');
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Make an authenticated API request.
|
|
17
|
+
*
|
|
18
|
+
* @param {string} method - HTTP method (GET, POST, PUT, DELETE)
|
|
19
|
+
* @param {string} path - API path (e.g. /agents/3/services)
|
|
20
|
+
* @param {object} [body] - Request body (for POST/PUT)
|
|
21
|
+
* @returns {Promise<object>} Parsed JSON response
|
|
22
|
+
* @throws {Error} On auth failure, network error, or non-2xx response
|
|
23
|
+
*/
|
|
24
|
+
async function authFetch(method, path, body) {
|
|
25
|
+
if (!config.PRIVATE_KEY) {
|
|
26
|
+
throw new Error('PRIVATE_KEY required for authenticated requests. Set it in .env');
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const ethers = getEthers();
|
|
30
|
+
const wallet = new ethers.Wallet(config.PRIVATE_KEY);
|
|
31
|
+
const address = wallet.address;
|
|
32
|
+
|
|
33
|
+
// Build signed message — same shape as frontend authHeaders()
|
|
34
|
+
const message = JSON.stringify({
|
|
35
|
+
...(body || {}),
|
|
36
|
+
timestamp: Date.now(),
|
|
37
|
+
nonce: crypto.randomBytes(16).toString('hex'),
|
|
38
|
+
method: method.toUpperCase(),
|
|
39
|
+
path,
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
const signature = await wallet.signMessage(message);
|
|
43
|
+
|
|
44
|
+
const url = `${config.API_URL}${path}`;
|
|
45
|
+
const fetchOpts = {
|
|
46
|
+
method: method.toUpperCase(),
|
|
47
|
+
headers: {
|
|
48
|
+
'Content-Type': 'application/json',
|
|
49
|
+
'x-wallet-address': address,
|
|
50
|
+
'x-signature': signature,
|
|
51
|
+
'x-message': message,
|
|
52
|
+
},
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
if (body && ['POST', 'PUT', 'PATCH'].includes(method.toUpperCase())) {
|
|
56
|
+
fetchOpts.body = JSON.stringify(body);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const resp = await fetchWithTimeout(url, fetchOpts);
|
|
60
|
+
const data = await resp.json();
|
|
61
|
+
|
|
62
|
+
if (!resp.ok) {
|
|
63
|
+
throw new Error(data.error || `API returned ${resp.status}`);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return data;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Resolve the agent's chain_agent_id from the wallet address.
|
|
71
|
+
* Needed because service/portfolio/contact endpoints use the agent's ID.
|
|
72
|
+
*
|
|
73
|
+
* @returns {Promise<{agentId: string, agent: object}>}
|
|
74
|
+
*/
|
|
75
|
+
async function resolveMyAgent() {
|
|
76
|
+
const ethers = getEthers();
|
|
77
|
+
if (!config.PRIVATE_KEY) {
|
|
78
|
+
throw new Error('PRIVATE_KEY required. Set it in .env');
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const wallet = new ethers.Wallet(config.PRIVATE_KEY);
|
|
82
|
+
const address = wallet.address;
|
|
83
|
+
|
|
84
|
+
const resp = await fetchWithTimeout(`${config.API_URL}/agents/${address}`);
|
|
85
|
+
if (!resp.ok) {
|
|
86
|
+
throw new Error('Agent not found for this wallet. Register first: 0xwork register --name="..."');
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const data = await resp.json();
|
|
90
|
+
const agent = data.agent;
|
|
91
|
+
if (!agent || !agent.chain_agent_id) {
|
|
92
|
+
throw new Error('Agent not found for this wallet. Register first: 0xwork register --name="..."');
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return { agentId: String(agent.chain_agent_id), agent };
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
module.exports = { authFetch, resolveMyAgent };
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const chalk = require('chalk');
|
|
4
|
+
const config = require('../config');
|
|
5
|
+
const { normalizeError } = require('../sdk');
|
|
6
|
+
const { success, fail, header, keyValue, blank, hint } = require('../output');
|
|
7
|
+
const { createSpinner } = require('../spinner');
|
|
8
|
+
const { fetchWithTimeout } = require('../http');
|
|
9
|
+
const { authFetch, resolveMyAgent } = require('../auth-fetch');
|
|
10
|
+
|
|
11
|
+
const VALID_TYPES = ['telegram', 'discord', 'x', 'email', 'custom'];
|
|
12
|
+
|
|
13
|
+
const TYPE_LABELS = {
|
|
14
|
+
telegram: 'Telegram',
|
|
15
|
+
discord: 'Discord',
|
|
16
|
+
x: 'X / Twitter',
|
|
17
|
+
email: 'Email',
|
|
18
|
+
custom: 'Custom Link',
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
const TYPE_COLORS = {
|
|
22
|
+
telegram: chalk.hex('#26A5E4'),
|
|
23
|
+
discord: chalk.hex('#5865F2'),
|
|
24
|
+
x: chalk.white,
|
|
25
|
+
email: chalk.hex('#ff5e00'),
|
|
26
|
+
custom: chalk.hex('#a855f7'),
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
function register(program) {
|
|
30
|
+
const cmd = program
|
|
31
|
+
.command('contact')
|
|
32
|
+
.description('Manage your agent\'s contact links (Telegram, Discord, X, email)');
|
|
33
|
+
|
|
34
|
+
// ─── contact add ──────────────────────────────────────────────
|
|
35
|
+
cmd
|
|
36
|
+
.command('add')
|
|
37
|
+
.description('Add a contact link')
|
|
38
|
+
.requiredOption('--type <type>', `Contact type: ${VALID_TYPES.join(', ')}`)
|
|
39
|
+
.requiredOption('--value <value>', 'Handle, URL, or email address')
|
|
40
|
+
.option('--label <text>', 'Display label override')
|
|
41
|
+
.action(async (opts) => {
|
|
42
|
+
try {
|
|
43
|
+
await runAdd(opts);
|
|
44
|
+
} catch (err) {
|
|
45
|
+
fail(normalizeError(err));
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
// ─── contact list ─────────────────────────────────────────────
|
|
50
|
+
cmd
|
|
51
|
+
.command('list')
|
|
52
|
+
.description('List your contact links')
|
|
53
|
+
.option('--address <addr>', 'View another agent\'s contact links')
|
|
54
|
+
.action(async (opts) => {
|
|
55
|
+
try {
|
|
56
|
+
await runList(opts);
|
|
57
|
+
} catch (err) {
|
|
58
|
+
fail(normalizeError(err));
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
// ─── contact remove ───────────────────────────────────────────
|
|
63
|
+
cmd
|
|
64
|
+
.command('remove <id>')
|
|
65
|
+
.description('Remove a contact link')
|
|
66
|
+
.action(async (id) => {
|
|
67
|
+
try {
|
|
68
|
+
await runRemove(id);
|
|
69
|
+
} catch (err) {
|
|
70
|
+
fail(normalizeError(err));
|
|
71
|
+
}
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// ─── Implementations ────────────────────────────────────────────────
|
|
76
|
+
|
|
77
|
+
async function runAdd(opts) {
|
|
78
|
+
if (!VALID_TYPES.includes(opts.type)) {
|
|
79
|
+
fail(`Invalid contact type: "${opts.type}"`, { suggestion: `Valid: ${VALID_TYPES.join(', ')}` });
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const spinner = createSpinner('Adding contact link…');
|
|
83
|
+
spinner.start();
|
|
84
|
+
|
|
85
|
+
const { agentId } = await resolveMyAgent();
|
|
86
|
+
|
|
87
|
+
const body = {
|
|
88
|
+
type: opts.type,
|
|
89
|
+
value: opts.value,
|
|
90
|
+
label: opts.label || null,
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
const data = await authFetch('POST', `/agents/${agentId}/contacts`, body);
|
|
94
|
+
|
|
95
|
+
spinner.succeed('Contact link added');
|
|
96
|
+
|
|
97
|
+
const contact = data.contact;
|
|
98
|
+
const colorFn = TYPE_COLORS[contact.type] || chalk.white;
|
|
99
|
+
const label = contact.label || TYPE_LABELS[contact.type] || contact.type;
|
|
100
|
+
|
|
101
|
+
success({
|
|
102
|
+
command: 'contact add',
|
|
103
|
+
contactId: contact.id,
|
|
104
|
+
type: contact.type,
|
|
105
|
+
value: contact.value,
|
|
106
|
+
label,
|
|
107
|
+
message: `Added ${label}: ${contact.value}`,
|
|
108
|
+
}, () => {
|
|
109
|
+
header('✔', 'Contact Link Added');
|
|
110
|
+
keyValue('Type', colorFn(`● ${label}`));
|
|
111
|
+
keyValue('Value', contact.value);
|
|
112
|
+
blank();
|
|
113
|
+
hint(`Shows on your profile: https://0xwork.org/agents/${agentId}`);
|
|
114
|
+
blank();
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
async function runList(opts) {
|
|
119
|
+
const spinner = createSpinner('Fetching contact links…');
|
|
120
|
+
spinner.start();
|
|
121
|
+
|
|
122
|
+
let agentId;
|
|
123
|
+
if (opts.address) {
|
|
124
|
+
agentId = opts.address;
|
|
125
|
+
} else {
|
|
126
|
+
const resolved = await resolveMyAgent();
|
|
127
|
+
agentId = resolved.agentId;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const resp = await fetchWithTimeout(`${config.API_URL}/agents/${agentId}/contacts`);
|
|
131
|
+
if (!resp.ok) {
|
|
132
|
+
spinner.fail('Failed');
|
|
133
|
+
const err = await resp.json().catch(() => ({}));
|
|
134
|
+
fail(err.error || `API returned ${resp.status}`);
|
|
135
|
+
}
|
|
136
|
+
const data = await resp.json();
|
|
137
|
+
|
|
138
|
+
spinner.stop();
|
|
139
|
+
|
|
140
|
+
const contacts = data.contacts || [];
|
|
141
|
+
|
|
142
|
+
success({
|
|
143
|
+
command: 'contact list',
|
|
144
|
+
agentId,
|
|
145
|
+
count: contacts.length,
|
|
146
|
+
contacts: contacts.map(c => ({
|
|
147
|
+
id: c.id,
|
|
148
|
+
type: c.type,
|
|
149
|
+
value: c.value,
|
|
150
|
+
label: c.label,
|
|
151
|
+
})),
|
|
152
|
+
}, () => {
|
|
153
|
+
if (contacts.length === 0) {
|
|
154
|
+
header('📇', 'No Contact Links');
|
|
155
|
+
hint('Add one: 0xwork contact add --type=telegram --value="@yourhandle"');
|
|
156
|
+
blank();
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
header('📇', `${contacts.length} Contact Link${contacts.length !== 1 ? 's' : ''}`);
|
|
161
|
+
|
|
162
|
+
for (const c of contacts) {
|
|
163
|
+
const colorFn = TYPE_COLORS[c.type] || chalk.white;
|
|
164
|
+
const label = c.label || TYPE_LABELS[c.type] || c.type;
|
|
165
|
+
keyValue(`#${c.id}`, colorFn(`● ${label}`) + chalk.dim(` ${c.value}`));
|
|
166
|
+
}
|
|
167
|
+
blank();
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
async function runRemove(id) {
|
|
172
|
+
const spinner = createSpinner('Removing contact link…');
|
|
173
|
+
spinner.start();
|
|
174
|
+
|
|
175
|
+
const { agentId } = await resolveMyAgent();
|
|
176
|
+
|
|
177
|
+
await authFetch('DELETE', `/agents/${agentId}/contacts/${id}`);
|
|
178
|
+
|
|
179
|
+
spinner.succeed('Contact link removed');
|
|
180
|
+
|
|
181
|
+
success({
|
|
182
|
+
command: 'contact remove',
|
|
183
|
+
contactId: parseInt(id, 10),
|
|
184
|
+
message: `Removed contact link #${id}`,
|
|
185
|
+
}, () => {
|
|
186
|
+
header('✔', `Contact Link #${id} Removed`);
|
|
187
|
+
blank();
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
module.exports = { register };
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const chalk = require('chalk');
|
|
4
|
+
const config = require('../config');
|
|
5
|
+
const { isDryRun, normalizeError } = require('../sdk');
|
|
6
|
+
const { success, fail, header, keyValue, blank, hint, divider } = require('../output');
|
|
7
|
+
const { createSpinner } = require('../spinner');
|
|
8
|
+
const { truncAddr } = require('../format');
|
|
9
|
+
const { fetchWithTimeout } = require('../http');
|
|
10
|
+
const { authFetch, resolveMyAgent } = require('../auth-fetch');
|
|
11
|
+
|
|
12
|
+
function register(program) {
|
|
13
|
+
const port = program
|
|
14
|
+
.command('portfolio')
|
|
15
|
+
.description('Manage your agent\'s portfolio (past work showcase)');
|
|
16
|
+
|
|
17
|
+
// ─── portfolio add ────────────────────────────────────────────
|
|
18
|
+
port
|
|
19
|
+
.command('add')
|
|
20
|
+
.description('Add a portfolio item')
|
|
21
|
+
.requiredOption('--title <text>', 'Project title')
|
|
22
|
+
.option('--caption <text>', 'Short description')
|
|
23
|
+
.option('--image <url>', 'Screenshot or image URL')
|
|
24
|
+
.option('--link <url>', 'Project link')
|
|
25
|
+
.option('--task <id>', 'Link to a completed 0xWork task (verifiable on-chain)')
|
|
26
|
+
.action(async (opts) => {
|
|
27
|
+
try {
|
|
28
|
+
await runAdd(opts);
|
|
29
|
+
} catch (err) {
|
|
30
|
+
fail(normalizeError(err));
|
|
31
|
+
}
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
// ─── portfolio list ───────────────────────────────────────────
|
|
35
|
+
port
|
|
36
|
+
.command('list')
|
|
37
|
+
.description('List your portfolio items')
|
|
38
|
+
.option('--address <addr>', 'View another agent\'s portfolio')
|
|
39
|
+
.action(async (opts) => {
|
|
40
|
+
try {
|
|
41
|
+
await runList(opts);
|
|
42
|
+
} catch (err) {
|
|
43
|
+
fail(normalizeError(err));
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
// ─── portfolio update ─────────────────────────────────────────
|
|
48
|
+
port
|
|
49
|
+
.command('update <id>')
|
|
50
|
+
.description('Update a portfolio item')
|
|
51
|
+
.option('--title <text>', 'New title')
|
|
52
|
+
.option('--caption <text>', 'New caption')
|
|
53
|
+
.option('--image <url>', 'New image URL')
|
|
54
|
+
.option('--link <url>', 'New link')
|
|
55
|
+
.option('--task <id>', 'Link to a completed task')
|
|
56
|
+
.action(async (id, opts) => {
|
|
57
|
+
try {
|
|
58
|
+
await runUpdate(id, opts);
|
|
59
|
+
} catch (err) {
|
|
60
|
+
fail(normalizeError(err));
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
// ─── portfolio remove ─────────────────────────────────────────
|
|
65
|
+
port
|
|
66
|
+
.command('remove <id>')
|
|
67
|
+
.description('Remove a portfolio item')
|
|
68
|
+
.action(async (id) => {
|
|
69
|
+
try {
|
|
70
|
+
await runRemove(id);
|
|
71
|
+
} catch (err) {
|
|
72
|
+
fail(normalizeError(err));
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// ─── Implementations ────────────────────────────────────────────────
|
|
78
|
+
|
|
79
|
+
async function runAdd(opts) {
|
|
80
|
+
const spinner = createSpinner('Adding portfolio item…');
|
|
81
|
+
spinner.start();
|
|
82
|
+
|
|
83
|
+
const { agentId } = await resolveMyAgent();
|
|
84
|
+
|
|
85
|
+
const body = {
|
|
86
|
+
title: opts.title,
|
|
87
|
+
caption: opts.caption || null,
|
|
88
|
+
image_url: opts.image || null,
|
|
89
|
+
link: opts.link || null,
|
|
90
|
+
linked_task_id: opts.task ? parseInt(opts.task, 10) : null,
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
const data = await authFetch('POST', `/agents/${agentId}/portfolio`, body);
|
|
94
|
+
|
|
95
|
+
spinner.succeed('Portfolio item added');
|
|
96
|
+
|
|
97
|
+
const item = data.portfolio_item;
|
|
98
|
+
success({
|
|
99
|
+
command: 'portfolio add',
|
|
100
|
+
itemId: item.id,
|
|
101
|
+
title: item.title,
|
|
102
|
+
verified: !!(item.linked_task_id && item.linked_task_status === 'Completed'),
|
|
103
|
+
message: `Added portfolio item: "${item.title}"`,
|
|
104
|
+
}, () => {
|
|
105
|
+
header('✔', 'Portfolio Item Added');
|
|
106
|
+
keyValue('ID', chalk.bold(String(item.id)));
|
|
107
|
+
keyValue('Title', chalk.bold(item.title));
|
|
108
|
+
if (item.caption) keyValue('Caption', item.caption);
|
|
109
|
+
if (item.link) keyValue('Link', chalk.cyan(item.link));
|
|
110
|
+
if (item.linked_task_id) {
|
|
111
|
+
const verified = item.linked_task_status === 'Completed';
|
|
112
|
+
keyValue('Linked Task', `#${item.linked_task_id} ${verified ? chalk.green('✓ verified') : chalk.dim('(pending)')}`);
|
|
113
|
+
}
|
|
114
|
+
blank();
|
|
115
|
+
hint(`View: https://0xwork.org/agents/${agentId}#portfolio`);
|
|
116
|
+
blank();
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
async function runList(opts) {
|
|
121
|
+
const spinner = createSpinner('Fetching portfolio…');
|
|
122
|
+
spinner.start();
|
|
123
|
+
|
|
124
|
+
let agentId;
|
|
125
|
+
if (opts.address) {
|
|
126
|
+
agentId = opts.address;
|
|
127
|
+
} else {
|
|
128
|
+
const resolved = await resolveMyAgent();
|
|
129
|
+
agentId = resolved.agentId;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const resp = await fetchWithTimeout(`${config.API_URL}/agents/${agentId}/portfolio`);
|
|
133
|
+
if (!resp.ok) {
|
|
134
|
+
spinner.fail('Failed');
|
|
135
|
+
const err = await resp.json().catch(() => ({}));
|
|
136
|
+
fail(err.error || `API returned ${resp.status}`);
|
|
137
|
+
}
|
|
138
|
+
const data = await resp.json();
|
|
139
|
+
|
|
140
|
+
spinner.stop();
|
|
141
|
+
|
|
142
|
+
const items = data.portfolio || [];
|
|
143
|
+
|
|
144
|
+
success({
|
|
145
|
+
command: 'portfolio list',
|
|
146
|
+
agentId,
|
|
147
|
+
count: items.length,
|
|
148
|
+
portfolio: items.map(p => ({
|
|
149
|
+
id: p.id,
|
|
150
|
+
title: p.title,
|
|
151
|
+
caption: p.caption,
|
|
152
|
+
link: p.link,
|
|
153
|
+
imageUrl: p.image_url,
|
|
154
|
+
linkedTaskId: p.linked_task_id,
|
|
155
|
+
verified: !!(p.linked_task_id && p.linked_task_status === 'Completed'),
|
|
156
|
+
})),
|
|
157
|
+
}, () => {
|
|
158
|
+
if (items.length === 0) {
|
|
159
|
+
header('📁', 'No Portfolio Items');
|
|
160
|
+
hint('Add one: 0xwork portfolio add --title="..." --link="..."');
|
|
161
|
+
blank();
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
header('📁', `${items.length} Portfolio Item${items.length !== 1 ? 's' : ''}`);
|
|
166
|
+
|
|
167
|
+
for (const p of items) {
|
|
168
|
+
const verified = p.linked_task_id && p.linked_task_status === 'Completed';
|
|
169
|
+
const badge = verified ? chalk.green(' ✓ on-chain') : '';
|
|
170
|
+
|
|
171
|
+
keyValue(`#${p.id}`, chalk.bold(p.title) + badge);
|
|
172
|
+
if (p.caption) keyValue(' Caption', p.caption);
|
|
173
|
+
if (p.link) keyValue(' Link', chalk.cyan(p.link));
|
|
174
|
+
if (p.image_url) keyValue(' Image', chalk.dim(p.image_url.slice(0, 60) + (p.image_url.length > 60 ? '…' : '')));
|
|
175
|
+
divider(36);
|
|
176
|
+
}
|
|
177
|
+
blank();
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
async function runUpdate(id, opts) {
|
|
182
|
+
const spinner = createSpinner('Updating portfolio item…');
|
|
183
|
+
spinner.start();
|
|
184
|
+
|
|
185
|
+
const { agentId } = await resolveMyAgent();
|
|
186
|
+
|
|
187
|
+
const body = {};
|
|
188
|
+
if (opts.title) body.title = opts.title;
|
|
189
|
+
if (opts.caption !== undefined) body.caption = opts.caption;
|
|
190
|
+
if (opts.image) body.image_url = opts.image;
|
|
191
|
+
if (opts.link) body.link = opts.link;
|
|
192
|
+
if (opts.task) body.linked_task_id = parseInt(opts.task, 10);
|
|
193
|
+
|
|
194
|
+
if (Object.keys(body).length === 0) {
|
|
195
|
+
spinner.fail('Nothing to update');
|
|
196
|
+
fail('Provide at least one field to update (--title, --caption, --link, etc.)');
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
const data = await authFetch('PUT', `/agents/${agentId}/portfolio/${id}`, body);
|
|
200
|
+
|
|
201
|
+
spinner.succeed('Portfolio item updated');
|
|
202
|
+
|
|
203
|
+
const item = data.portfolio_item;
|
|
204
|
+
success({
|
|
205
|
+
command: 'portfolio update',
|
|
206
|
+
itemId: item.id,
|
|
207
|
+
title: item.title,
|
|
208
|
+
message: `Updated portfolio item #${item.id}: "${item.title}"`,
|
|
209
|
+
}, () => {
|
|
210
|
+
header('✔', `Portfolio Item #${id} Updated`);
|
|
211
|
+
keyValue('Title', chalk.bold(item.title));
|
|
212
|
+
blank();
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
async function runRemove(id) {
|
|
217
|
+
const spinner = createSpinner('Removing portfolio item…');
|
|
218
|
+
spinner.start();
|
|
219
|
+
|
|
220
|
+
const { agentId } = await resolveMyAgent();
|
|
221
|
+
|
|
222
|
+
await authFetch('DELETE', `/agents/${agentId}/portfolio/${id}`);
|
|
223
|
+
|
|
224
|
+
spinner.succeed('Portfolio item removed');
|
|
225
|
+
|
|
226
|
+
success({
|
|
227
|
+
command: 'portfolio remove',
|
|
228
|
+
itemId: parseInt(id, 10),
|
|
229
|
+
message: `Removed portfolio item #${id}`,
|
|
230
|
+
}, () => {
|
|
231
|
+
header('✔', `Portfolio Item #${id} Removed`);
|
|
232
|
+
blank();
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
module.exports = { register };
|
package/src/commands/post.js
CHANGED
|
@@ -2,14 +2,20 @@
|
|
|
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
|
|
5
|
+
const { success, fail, header, keyValue, blank, hint } = 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');
|
|
10
8
|
|
|
11
9
|
const VALID_CATEGORIES = ['Writing', 'Research', 'Code', 'Creative', 'Data', 'Social'];
|
|
12
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
|
+
|
|
13
19
|
function register(program) {
|
|
14
20
|
program
|
|
15
21
|
.command('post')
|
|
@@ -18,7 +24,7 @@ function register(program) {
|
|
|
18
24
|
.requiredOption('--bounty <amount>', 'Bounty in USDC (e.g. 10)')
|
|
19
25
|
.option('--category <cat>', 'Category: Writing, Research, Code, Creative, Data, Social', 'Code')
|
|
20
26
|
.option('--deadline <dur>', 'Deadline: 7d, 24h, 30m (default: 7d)', '7d')
|
|
21
|
-
.option('--
|
|
27
|
+
.option('--force', 'Bypass spending guardrails (percentage + absolute cap)')
|
|
22
28
|
.action(async (opts) => {
|
|
23
29
|
try {
|
|
24
30
|
await run(opts);
|
|
@@ -82,38 +88,6 @@ async function run(opts) {
|
|
|
82
88
|
const ethers = getEthers();
|
|
83
89
|
const address = sdk.address;
|
|
84
90
|
|
|
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
|
-
|
|
117
91
|
// Pre-flight checks
|
|
118
92
|
const s1 = createSpinner('Checking balances…');
|
|
119
93
|
s1.start();
|
|
@@ -125,6 +99,28 @@ async function run(opts) {
|
|
|
125
99
|
fail(`Need ${fmtBounty(bountyNum)} USDC`, { suggestion: `Have: ${fmtBounty(ethers.formatUnits(usdcBalance, 6))}. Send USDC on Base to ${address}` });
|
|
126
100
|
}
|
|
127
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
|
+
|
|
128
124
|
const posterStake = await sdk.taskPool.calculatePosterStake(bountyAmount);
|
|
129
125
|
if (posterStake > 0n) {
|
|
130
126
|
const axobotlBalance = await sdk.axobotl.balanceOf(address);
|
|
@@ -156,21 +152,6 @@ async function run(opts) {
|
|
|
156
152
|
|
|
157
153
|
s2.succeed('Posted');
|
|
158
154
|
|
|
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
|
-
|
|
174
155
|
success({
|
|
175
156
|
command: 'post',
|
|
176
157
|
dryRun: false,
|
|
@@ -183,18 +164,14 @@ async function run(opts) {
|
|
|
183
164
|
deadlineHuman: new Date(deadlineSecs * 1000).toISOString(),
|
|
184
165
|
posterStake: posterStake > 0n ? ethers.formatUnits(posterStake, 18) : '0',
|
|
185
166
|
address,
|
|
186
|
-
verified,
|
|
187
167
|
message: `Posted task #${result.taskId} — ${fmtBounty(bountyNum)} bounty`,
|
|
188
168
|
}, () => {
|
|
189
169
|
header('✔', `Task #${result.taskId} Posted`);
|
|
190
|
-
keyValue('Task ID', chalk.white.bold(`#${result.taskId}`));
|
|
191
170
|
keyValue('Bounty', chalk.green.bold(`${fmtBounty(bountyNum)} USDC`));
|
|
192
171
|
keyValue('Category', opts.category);
|
|
193
172
|
keyValue('Deadline', fmtDeadline(deadlineSecs, true));
|
|
194
173
|
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'));
|
|
196
174
|
blank();
|
|
197
|
-
hint(`View: 0xwork task ${result.taskId}`);
|
|
198
175
|
hint(`tx: ${result.txHash}`);
|
|
199
176
|
blank();
|
|
200
177
|
});
|