@0xwork/cli 1.0.0 → 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 -7
- 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 +31 -0
- package/src/commands/profile.js +29 -0
- package/src/commands/service.js +287 -0
- package/src/index.js +14 -2
package/bin/0xwork.js
CHANGED
|
@@ -1,10 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
'use strict';
|
|
3
3
|
|
|
4
|
-
const program = require('../src/index');
|
|
5
|
-
|
|
6
|
-
const { fail } = require('../src/output');
|
|
7
|
-
|
|
8
|
-
program.parseAsync(process.argv).catch((err) => {
|
|
9
|
-
fail(normalizeError(err));
|
|
10
|
-
});
|
|
4
|
+
const program = require('../src/index.js');
|
|
5
|
+
program.parseAsync(process.argv);
|
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
|
@@ -8,6 +8,14 @@ const { fmtBounty, fmtDeadline, fmtAxobotl } = require('../format');
|
|
|
8
8
|
|
|
9
9
|
const VALID_CATEGORIES = ['Writing', 'Research', 'Code', 'Creative', 'Data', 'Social'];
|
|
10
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
|
+
|
|
11
19
|
function register(program) {
|
|
12
20
|
program
|
|
13
21
|
.command('post')
|
|
@@ -16,6 +24,7 @@ function register(program) {
|
|
|
16
24
|
.requiredOption('--bounty <amount>', 'Bounty in USDC (e.g. 10)')
|
|
17
25
|
.option('--category <cat>', 'Category: Writing, Research, Code, Creative, Data, Social', 'Code')
|
|
18
26
|
.option('--deadline <dur>', 'Deadline: 7d, 24h, 30m (default: 7d)', '7d')
|
|
27
|
+
.option('--force', 'Bypass spending guardrails (percentage + absolute cap)')
|
|
19
28
|
.action(async (opts) => {
|
|
20
29
|
try {
|
|
21
30
|
await run(opts);
|
|
@@ -90,6 +99,28 @@ async function run(opts) {
|
|
|
90
99
|
fail(`Need ${fmtBounty(bountyNum)} USDC`, { suggestion: `Have: ${fmtBounty(ethers.formatUnits(usdcBalance, 6))}. Send USDC on Base to ${address}` });
|
|
91
100
|
}
|
|
92
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
|
+
|
|
93
124
|
const posterStake = await sdk.taskPool.calculatePosterStake(bountyAmount);
|
|
94
125
|
if (posterStake > 0n) {
|
|
95
126
|
const axobotlBalance = await sdk.axobotl.balanceOf(address);
|
package/src/commands/profile.js
CHANGED
|
@@ -54,6 +54,26 @@ async function run(opts) {
|
|
|
54
54
|
profile.stakedAmount = agent.staked_amount ? (Number(agent.staked_amount) / 1e18).toLocaleString() + ' AXOBOTL' : '0';
|
|
55
55
|
profile.createdAt = agent.created_at;
|
|
56
56
|
profile.metadataURI = agent.metadata_uri || null;
|
|
57
|
+
|
|
58
|
+
// Fetch storefront data (services, portfolio, contacts)
|
|
59
|
+
const agentIdStr = String(agent.chain_agent_id || agent.id);
|
|
60
|
+
const [svcResp, portResp, contactResp] = await Promise.all([
|
|
61
|
+
fetchWithTimeout(`${config.API_URL}/agents/${agentIdStr}/services`).catch(() => null),
|
|
62
|
+
fetchWithTimeout(`${config.API_URL}/agents/${agentIdStr}/portfolio`).catch(() => null),
|
|
63
|
+
fetchWithTimeout(`${config.API_URL}/agents/${agentIdStr}/contacts`).catch(() => null),
|
|
64
|
+
]);
|
|
65
|
+
if (svcResp?.ok) {
|
|
66
|
+
const d = await svcResp.json();
|
|
67
|
+
profile.services = (d.services || []).length;
|
|
68
|
+
}
|
|
69
|
+
if (portResp?.ok) {
|
|
70
|
+
const d = await portResp.json();
|
|
71
|
+
profile.portfolio = (d.portfolio || []).length;
|
|
72
|
+
}
|
|
73
|
+
if (contactResp?.ok) {
|
|
74
|
+
const d = await contactResp.json();
|
|
75
|
+
profile.contacts = (d.contacts || []).length;
|
|
76
|
+
}
|
|
57
77
|
}
|
|
58
78
|
}
|
|
59
79
|
} catch { /* API down */ }
|
|
@@ -122,6 +142,15 @@ async function run(opts) {
|
|
|
122
142
|
keyValue('Capabilities', caps);
|
|
123
143
|
}
|
|
124
144
|
if (profile.handle) keyValue('Handle', profile.handle);
|
|
145
|
+
|
|
146
|
+
// Storefront summary
|
|
147
|
+
if (profile.services || profile.portfolio || profile.contacts) {
|
|
148
|
+
blank();
|
|
149
|
+
keyValue('Services', profile.services ? chalk.bold(`${profile.services} listed`) : chalk.dim('none'));
|
|
150
|
+
keyValue('Portfolio', profile.portfolio ? chalk.bold(`${profile.portfolio} items`) : chalk.dim('none'));
|
|
151
|
+
keyValue('Contacts', profile.contacts ? chalk.bold(`${profile.contacts} links`) : chalk.dim('none'));
|
|
152
|
+
}
|
|
153
|
+
|
|
125
154
|
blank();
|
|
126
155
|
hint(`Profile: https://0xwork.org/agents/${profile.agentId}`);
|
|
127
156
|
blank();
|