spendos 0.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.
Files changed (90) hide show
  1. package/.dockerignore +4 -0
  2. package/.env.example +30 -0
  3. package/AGENTS.md +212 -0
  4. package/BOOTSTRAP.md +55 -0
  5. package/Dockerfile +52 -0
  6. package/HEARTBEAT.md +7 -0
  7. package/IDENTITY.md +23 -0
  8. package/LICENSE +21 -0
  9. package/README.md +162 -0
  10. package/SOUL.md +202 -0
  11. package/SUBMISSION.md +128 -0
  12. package/TOOLS.md +40 -0
  13. package/USER.md +17 -0
  14. package/acp-seller/bin/acp.ts +807 -0
  15. package/acp-seller/config.json +34 -0
  16. package/acp-seller/package.json +55 -0
  17. package/acp-seller/src/commands/agent.ts +328 -0
  18. package/acp-seller/src/commands/bounty.ts +1189 -0
  19. package/acp-seller/src/commands/deploy.ts +414 -0
  20. package/acp-seller/src/commands/job.ts +217 -0
  21. package/acp-seller/src/commands/profile.ts +71 -0
  22. package/acp-seller/src/commands/resource.ts +91 -0
  23. package/acp-seller/src/commands/search.ts +327 -0
  24. package/acp-seller/src/commands/sell.ts +883 -0
  25. package/acp-seller/src/commands/serve.ts +258 -0
  26. package/acp-seller/src/commands/setup.ts +399 -0
  27. package/acp-seller/src/commands/token.ts +88 -0
  28. package/acp-seller/src/commands/wallet.ts +123 -0
  29. package/acp-seller/src/lib/api.ts +118 -0
  30. package/acp-seller/src/lib/auth.ts +291 -0
  31. package/acp-seller/src/lib/bounty.ts +257 -0
  32. package/acp-seller/src/lib/client.ts +42 -0
  33. package/acp-seller/src/lib/config.ts +240 -0
  34. package/acp-seller/src/lib/open.ts +41 -0
  35. package/acp-seller/src/lib/openclawCron.ts +138 -0
  36. package/acp-seller/src/lib/output.ts +104 -0
  37. package/acp-seller/src/lib/wallet.ts +81 -0
  38. package/acp-seller/src/seller/offerings/_shared/preTransactionScan.ts +127 -0
  39. package/acp-seller/src/seller/offerings/canonical-catalog.ts +221 -0
  40. package/acp-seller/src/seller/offerings/spendos/spendos_summarize_url/handlers.ts +20 -0
  41. package/acp-seller/src/seller/offerings/spendos/spendos_summarize_url/offering.json +18 -0
  42. package/acp-seller/src/seller/offerings/spendos/spendos_translate/handlers.ts +21 -0
  43. package/acp-seller/src/seller/offerings/spendos/spendos_translate/offering.json +22 -0
  44. package/acp-seller/src/seller/offerings/spendos/spendos_tweet_gen/handlers.ts +20 -0
  45. package/acp-seller/src/seller/offerings/spendos/spendos_tweet_gen/offering.json +18 -0
  46. package/acp-seller/src/seller/runtime/acpSocket.ts +413 -0
  47. package/acp-seller/src/seller/runtime/logger.ts +36 -0
  48. package/acp-seller/src/seller/runtime/offeringTypes.ts +52 -0
  49. package/acp-seller/src/seller/runtime/offerings.ts +277 -0
  50. package/acp-seller/src/seller/runtime/paymentVerification.test.ts +207 -0
  51. package/acp-seller/src/seller/runtime/paymentVerification.ts +363 -0
  52. package/acp-seller/src/seller/runtime/seller.onchain.test.ts +220 -0
  53. package/acp-seller/src/seller/runtime/seller.test.ts +823 -0
  54. package/acp-seller/src/seller/runtime/seller.ts +1041 -0
  55. package/acp-seller/src/seller/runtime/sellerApi.ts +71 -0
  56. package/acp-seller/src/seller/runtime/startup.ts +270 -0
  57. package/acp-seller/src/seller/runtime/types.ts +62 -0
  58. package/acp-seller/tsconfig.json +20 -0
  59. package/bin/spendos.js +23 -0
  60. package/contracts/SpendOSAudit.sol +29 -0
  61. package/dist/mcp-server.mjs +153 -0
  62. package/jobs/translate.json +7 -0
  63. package/jobs/tweet-gen.json +7 -0
  64. package/openclaw.json +41 -0
  65. package/package.json +49 -0
  66. package/plugins/spendos-events/index.ts +78 -0
  67. package/plugins/spendos-events/package.json +14 -0
  68. package/policies/enforce-bounds.mjs +71 -0
  69. package/public/index.html +509 -0
  70. package/public/landing.html +241 -0
  71. package/railway.json +12 -0
  72. package/railway.toml +12 -0
  73. package/scripts/deploy.ts +48 -0
  74. package/scripts/test-x402-mainnet.ts +30 -0
  75. package/scripts/xmtp-listener.ts +61 -0
  76. package/setup.sh +278 -0
  77. package/skills/spendos/skill.md +26 -0
  78. package/src/agent.ts +152 -0
  79. package/src/audit.ts +166 -0
  80. package/src/governance.ts +367 -0
  81. package/src/job-registry.ts +306 -0
  82. package/src/mcp-public.ts +145 -0
  83. package/src/mcp-server.ts +171 -0
  84. package/src/opportunity-scanner.ts +138 -0
  85. package/src/server.ts +870 -0
  86. package/src/venice-x402.ts +234 -0
  87. package/src/xmtp.ts +109 -0
  88. package/src/zerion.ts +58 -0
  89. package/start.sh +168 -0
  90. package/tsconfig.json +14 -0
@@ -0,0 +1,414 @@
1
+ // =============================================================================
2
+ // acp serve deploy railway — Deploy current agent to Railway
3
+ // acp serve deploy railway setup — Create Railway project for current agent
4
+ // acp serve deploy railway status — Show current agent's deployment status
5
+ // acp serve deploy railway logs — Show current agent's deployment logs
6
+ // acp serve deploy railway teardown — Remove current agent's deployment
7
+ // acp serve deploy railway env — List env vars on current agent's project
8
+ // acp serve deploy railway env set — Set an env var
9
+ // acp serve deploy railway env delete — Remove an env var
10
+ //
11
+ // Each agent gets its own Railway project. Switching agents and deploying
12
+ // creates a separate instance — both keep running independently.
13
+ // =============================================================================
14
+
15
+ import * as fs from "fs";
16
+ import * as path from "path";
17
+ import { execSync } from "child_process";
18
+ import readline from "readline";
19
+ import * as output from "../lib/output.js";
20
+ import {
21
+ readConfig,
22
+ writeConfig,
23
+ getActiveAgent,
24
+ sanitizeAgentName,
25
+ ROOT,
26
+ } from "../lib/config.js";
27
+ import type { DeployInfo, AgentEntry } from "../lib/config.js";
28
+ import { generateDockerfile, generateDockerignore } from "../deploy/docker.js";
29
+ import * as railway from "../deploy/railway.js";
30
+ import type { LogFilter } from "../deploy/railway.js";
31
+
32
+ const DOCKERFILE_PATH = path.resolve(ROOT, "Dockerfile");
33
+ const DOCKERIGNORE_PATH = path.resolve(ROOT, ".dockerignore");
34
+
35
+ function getOfferingsRoot(agentName: string): string {
36
+ return path.resolve(
37
+ ROOT,
38
+ "src",
39
+ "seller",
40
+ "offerings",
41
+ sanitizeAgentName(agentName),
42
+ );
43
+ }
44
+
45
+ // -- Helpers --
46
+
47
+ function prompt(question: string): Promise<string> {
48
+ const rl = readline.createInterface({
49
+ input: process.stdin,
50
+ output: process.stdout,
51
+ });
52
+ return new Promise((resolve) =>
53
+ rl.question(question, (answer) => {
54
+ rl.close();
55
+ resolve(answer.trim());
56
+ }),
57
+ );
58
+ }
59
+
60
+ function installRailwayCli(): void {
61
+ output.log(" Installing Railway CLI...\n");
62
+ execSync("npm install -g @railway/cli", {
63
+ cwd: ROOT,
64
+ stdio: "inherit",
65
+ });
66
+ const check = railway.checkCli();
67
+ if (!check.installed) {
68
+ output.fatal(
69
+ "Installation failed. Try manually: npm install -g @railway/cli",
70
+ );
71
+ }
72
+ output.success(`Railway CLI installed (${check.version})`);
73
+ }
74
+
75
+ async function requireCli(): Promise<void> {
76
+ const { installed } = railway.checkCli();
77
+ if (!installed) {
78
+ const answer = await prompt(
79
+ " Railway CLI not found. Install it now? (Y/n): ",
80
+ );
81
+ if (answer.toLowerCase() === "n") {
82
+ output.fatal(
83
+ "Railway CLI is required. Install manually:\n\n" +
84
+ " npm install -g @railway/cli\n",
85
+ );
86
+ }
87
+ installRailwayCli();
88
+ }
89
+ }
90
+
91
+ function requireAgent(): AgentEntry {
92
+ const agent = getActiveAgent();
93
+ if (!agent) {
94
+ output.fatal("No active agent. Run `acp setup` first.");
95
+ }
96
+ return agent;
97
+ }
98
+
99
+ /**
100
+ * Ensure the Railway CLI is pointing at the current agent's project.
101
+ * Reads the stored Railway project config for this agent and writes it
102
+ * to .railway/config.json so all subsequent `railway` commands target it.
103
+ */
104
+ async function linkToCurrentAgent(): Promise<AgentEntry> {
105
+ await requireCli();
106
+ const agent = requireAgent();
107
+ const config = readConfig();
108
+ const deployInfo = config.DEPLOYS?.[agent.id];
109
+
110
+ if (!deployInfo?.railwayConfig) {
111
+ output.fatal(
112
+ `No Railway project for agent "${agent.name}".\n` +
113
+ " Run `acp serve deploy railway setup` first.",
114
+ );
115
+ }
116
+
117
+ railway.writeRailwayConfig(deployInfo.railwayConfig);
118
+ return agent;
119
+ }
120
+
121
+ function getDeployInfo(agentId: string): DeployInfo | undefined {
122
+ const config = readConfig();
123
+ return config.DEPLOYS?.[agentId];
124
+ }
125
+
126
+ function saveDeployInfo(agentId: string, info: DeployInfo): void {
127
+ const config = readConfig();
128
+ if (!config.DEPLOYS) config.DEPLOYS = {};
129
+ config.DEPLOYS[agentId] = info;
130
+ writeConfig(config);
131
+ }
132
+
133
+ function removeDeployInfo(agentId: string): void {
134
+ const config = readConfig();
135
+ if (config.DEPLOYS) {
136
+ delete config.DEPLOYS[agentId];
137
+ if (Object.keys(config.DEPLOYS).length === 0) {
138
+ delete config.DEPLOYS;
139
+ }
140
+ writeConfig(config);
141
+ }
142
+ }
143
+
144
+ function listLocalOfferings(agentName: string): string[] {
145
+ const offeringsRoot = getOfferingsRoot(agentName);
146
+ if (!fs.existsSync(offeringsRoot)) return [];
147
+ const entries = fs.readdirSync(offeringsRoot, { withFileTypes: true });
148
+ return entries
149
+ .filter((entry) => {
150
+ if (!entry.isDirectory()) return false;
151
+ const dir = path.join(offeringsRoot, entry.name);
152
+ return (
153
+ fs.existsSync(path.join(dir, "handlers.ts")) &&
154
+ fs.existsSync(path.join(dir, "offering.json"))
155
+ );
156
+ })
157
+ .map((entry) => entry.name);
158
+ }
159
+
160
+ function ensureDockerFiles(): void {
161
+ if (fs.existsSync(DOCKERFILE_PATH)) {
162
+ output.log(" Using existing Dockerfile.");
163
+ } else {
164
+ fs.writeFileSync(DOCKERFILE_PATH, generateDockerfile());
165
+ output.success("Generated Dockerfile");
166
+ }
167
+
168
+ if (fs.existsSync(DOCKERIGNORE_PATH)) {
169
+ output.log(" Using existing .dockerignore.");
170
+ } else {
171
+ fs.writeFileSync(DOCKERIGNORE_PATH, generateDockerignore());
172
+ output.success("Generated .dockerignore");
173
+ }
174
+ }
175
+
176
+ // -- Commands --
177
+
178
+ export async function setup(): Promise<void> {
179
+ // 1. Check CLI (auto-install if missing)
180
+ await requireCli();
181
+ const { version } = railway.checkCli();
182
+ output.log(` Railway CLI: ${version}`);
183
+
184
+ const agent = requireAgent();
185
+
186
+ // 2. Check if this agent already has a Railway project
187
+ const existing = getDeployInfo(agent.id);
188
+ if (existing) {
189
+ output.log(` Agent "${agent.name}" already has a Railway project.`);
190
+ output.log(" Run `acp serve deploy railway` to deploy.\n");
191
+ return;
192
+ }
193
+
194
+ // 3. Check login
195
+ if (!railway.isLoggedIn()) {
196
+ output.log(" Logging in to Railway...");
197
+ railway.login();
198
+ }
199
+ output.success("Logged in to Railway");
200
+
201
+ // 4. Check if there's already a Railway project linked to this directory
202
+ let railwayConfig = railway.readRailwayConfig();
203
+ if (railwayConfig) {
204
+ output.log(` Found existing Railway project linked to this directory.`);
205
+ } else {
206
+ // Create new Railway project for this agent
207
+ output.log(` Creating Railway project for agent "${agent.name}"...`);
208
+ railway.initProject(`acp-${agent.name}`);
209
+
210
+ railwayConfig = railway.readRailwayConfig();
211
+ if (!railwayConfig) {
212
+ output.fatal(
213
+ "Failed to read Railway project config after init. Try running `railway init` manually.",
214
+ );
215
+ }
216
+ }
217
+
218
+ // 6. Save project config for this agent
219
+ // (API key will be set during deploy, after the service is created)
220
+ saveDeployInfo(agent.id, {
221
+ provider: "railway",
222
+ agentName: agent.name,
223
+ offerings: [],
224
+ deployedAt: "",
225
+ railwayConfig,
226
+ });
227
+
228
+ output.output(
229
+ { provider: "railway", agent: agent.name, status: "setup_complete" },
230
+ () => {
231
+ output.heading("Railway Setup Complete");
232
+ output.field("Agent", agent.name);
233
+ output.field("Railway Project", `acp-${agent.name}`);
234
+ output.log("\n Next steps:");
235
+ output.log(" 1. Create offerings: acp sell init <name>");
236
+ output.log(" 2. Register on ACP: acp sell create <name>");
237
+ output.log(" 3. Deploy: acp serve deploy railway\n");
238
+ },
239
+ );
240
+ }
241
+
242
+ export async function deploy(): Promise<void> {
243
+ const agent = await linkToCurrentAgent();
244
+
245
+ // Check offerings exist locally
246
+ const offerings = listLocalOfferings(agent.name);
247
+ if (offerings.length === 0) {
248
+ output.fatal(
249
+ "No offerings found. Create at least one offering first:\n\n" +
250
+ " acp sell init <name>\n" +
251
+ " acp sell create <name>",
252
+ );
253
+ }
254
+
255
+ // Generate Docker files if missing
256
+ ensureDockerFiles();
257
+
258
+ // Show what's being deployed
259
+ output.log(`\n Agent: ${agent.name}`);
260
+ output.log(` Offerings: ${offerings.join(", ")}`);
261
+ output.log("");
262
+
263
+ // Deploy (this creates the service on first run)
264
+ output.log(" Deploying to Railway...\n");
265
+ railway.up();
266
+
267
+ // Link the service if not already linked (railway up creates it but doesn't auto-link)
268
+ if (!railway.hasLinkedService()) {
269
+ const projectName = `acp-${agent.name}`;
270
+ try {
271
+ railway.linkService(projectName);
272
+ output.success(`Linked service ${projectName}`);
273
+ } catch {
274
+ output.warn(
275
+ "Could not auto-link service. Link manually:\n" +
276
+ ` railway service link ${projectName}`,
277
+ );
278
+ }
279
+ }
280
+
281
+ // Set API key on the service
282
+ const config = readConfig();
283
+ const apiKey = config.LITE_AGENT_API_KEY;
284
+ if (apiKey) {
285
+ try {
286
+ railway.setVariable("LITE_AGENT_API_KEY", apiKey);
287
+ output.success("Set LITE_AGENT_API_KEY on Railway");
288
+ } catch {
289
+ output.warn(
290
+ "Could not set LITE_AGENT_API_KEY. Set it manually:\n" +
291
+ " acp serve deploy railway env set LITE_AGENT_API_KEY=" +
292
+ apiKey,
293
+ );
294
+ }
295
+ }
296
+
297
+ // Update deploy tracking
298
+ const existing = getDeployInfo(agent.id)!;
299
+ saveDeployInfo(agent.id, {
300
+ ...existing,
301
+ offerings,
302
+ deployedAt: new Date().toISOString(),
303
+ });
304
+
305
+ output.output(
306
+ { provider: "railway", status: "deployed", agent: agent.name, offerings },
307
+ () => {
308
+ output.heading("Deployed to Railway");
309
+ output.field("Agent", agent.name);
310
+ output.field("Offerings", offerings.join(", "));
311
+ output.log("");
312
+ output.log(" Check status: acp serve deploy railway status");
313
+ output.log(" View logs: acp serve deploy railway logs --follow");
314
+ output.log(" Tear down: acp serve deploy railway teardown\n");
315
+ },
316
+ );
317
+ }
318
+
319
+ export async function status(): Promise<void> {
320
+ const agent = await linkToCurrentAgent();
321
+ const deployInfo = getDeployInfo(agent.id);
322
+
323
+ output.heading(`Railway Deployment — ${agent.name}`);
324
+ if (deployInfo && deployInfo.deployedAt) {
325
+ output.field("Offerings", deployInfo.offerings.join(", "));
326
+ output.field("Deployed at", deployInfo.deployedAt);
327
+ }
328
+ output.log("");
329
+ const statusText = railway.getStatus();
330
+ output.log(statusText);
331
+ output.log("");
332
+ }
333
+
334
+ export async function logs(
335
+ follow: boolean = false,
336
+ filter: LogFilter = {},
337
+ ): Promise<void> {
338
+ await linkToCurrentAgent();
339
+ railway.streamLogs(follow, filter);
340
+ }
341
+
342
+ export async function teardown(): Promise<void> {
343
+ const agent = await linkToCurrentAgent();
344
+ output.log(` Removing deployment for agent "${agent.name}"...\n`);
345
+ railway.down();
346
+
347
+ removeDeployInfo(agent.id);
348
+
349
+ output.output(
350
+ { provider: "railway", agent: agent.name, status: "torn_down" },
351
+ () => {
352
+ output.success(`Deployment for "${agent.name}" removed.`);
353
+ output.log(
354
+ " The Railway project still exists — re-deploy anytime with:",
355
+ );
356
+ output.log(" acp serve deploy railway\n");
357
+ },
358
+ );
359
+ }
360
+
361
+ export async function env(): Promise<void> {
362
+ const agent = await linkToCurrentAgent();
363
+ output.heading(`Railway Env Vars — ${agent.name}`);
364
+ output.log("");
365
+ const vars = railway.listVariables();
366
+ if (vars) {
367
+ output.log(vars);
368
+ } else {
369
+ output.log(" No environment variables set.");
370
+ }
371
+ output.log("");
372
+ }
373
+
374
+ export async function envSet(keyValue: string): Promise<void> {
375
+ await linkToCurrentAgent();
376
+
377
+ const eqIdx = keyValue.indexOf("=");
378
+ if (eqIdx === -1) {
379
+ output.fatal(
380
+ "Invalid format. Use: acp serve deploy railway env set KEY=value",
381
+ );
382
+ }
383
+
384
+ const key = keyValue.slice(0, eqIdx);
385
+ const value = keyValue.slice(eqIdx + 1);
386
+
387
+ if (!key) {
388
+ output.fatal("Key cannot be empty.");
389
+ }
390
+
391
+ railway.setVariable(key, value);
392
+
393
+ output.output({ action: "set", key }, () => {
394
+ output.success(`Set ${key} on Railway`);
395
+ output.log(" Redeploy for changes to take effect:");
396
+ output.log(" acp serve deploy railway\n");
397
+ });
398
+ }
399
+
400
+ export async function envDelete(key: string): Promise<void> {
401
+ await linkToCurrentAgent();
402
+
403
+ if (!key) {
404
+ output.fatal("Usage: acp serve deploy railway env delete <KEY>");
405
+ }
406
+
407
+ railway.deleteVariable(key);
408
+
409
+ output.output({ action: "deleted", key }, () => {
410
+ output.success(`Deleted ${key} from Railway`);
411
+ output.log(" Redeploy for changes to take effect:");
412
+ output.log(" acp serve deploy railway\n");
413
+ });
414
+ }
@@ -0,0 +1,217 @@
1
+ // =============================================================================
2
+ // acp job create <wallet> <offering> [--requirements '{}']
3
+ // acp job status <jobId>
4
+ // acp job active
5
+ // acp job completed
6
+ // =============================================================================
7
+
8
+ import client from "../lib/client.js";
9
+ import { formatPrice } from "../lib/config.js";
10
+ import * as output from "../lib/output.js";
11
+ import { getBountyByJobId } from "../lib/bounty.js";
12
+
13
+ function renderDeliverable(deliverable: unknown): string {
14
+ if (typeof deliverable === "string") return deliverable;
15
+ return JSON.stringify(deliverable);
16
+ }
17
+
18
+ export async function create(
19
+ agentWalletAddress: string,
20
+ jobOfferingName: string,
21
+ serviceRequirements: Record<string, unknown>,
22
+ ): Promise<void> {
23
+ if (!agentWalletAddress || !jobOfferingName) {
24
+ output.fatal(
25
+ "Usage: acp job create <agentWalletAddress> <jobOfferingName> [--requirements '<json>']",
26
+ );
27
+ }
28
+
29
+ try {
30
+ const job = await client.post<{ data: { jobId: number } }>("/acp/jobs", {
31
+ providerWalletAddress: agentWalletAddress,
32
+ jobOfferingName,
33
+ serviceRequirements,
34
+ });
35
+
36
+ output.output(job.data, (data) => {
37
+ output.heading("Job Created");
38
+ output.field("Job ID", data.data?.jobId ?? data.jobId);
39
+ output.log(
40
+ "\n Job submitted. Use `acp job status <jobId>` to check progress.\n",
41
+ );
42
+ });
43
+ } catch (e) {
44
+ output.fatal(
45
+ `Failed to create job: ${e instanceof Error ? e.message : String(e)}`,
46
+ );
47
+ }
48
+ }
49
+
50
+ export async function status(jobId: string): Promise<void> {
51
+ if (!jobId) {
52
+ output.fatal("Usage: acp job status <jobId>");
53
+ }
54
+
55
+ try {
56
+ const job = await client.get(`/acp/jobs/${jobId}`);
57
+
58
+ if (!job?.data?.data) {
59
+ output.fatal(`Job not found: ${jobId}`);
60
+ }
61
+
62
+ const data = job.data.data;
63
+
64
+ if (job.data.errors && job.data.errors.length > 0) {
65
+ output.output(job.data.errors, (errors) => {
66
+ output.heading(`Job ${jobId} messages`);
67
+ errors.forEach((error: string, i: number) =>
68
+ output.field(`Error ${i + 1}`, error),
69
+ );
70
+ });
71
+ // return;
72
+ }
73
+
74
+ const memoHistory = (data.memos || []).map(
75
+ (memo: {
76
+ nextPhase: string;
77
+ content: string;
78
+ createdAt: string;
79
+ status: string;
80
+ }) => ({
81
+ nextPhase: memo.nextPhase,
82
+ content: memo.content,
83
+ createdAt: memo.createdAt,
84
+ status: memo.status,
85
+ }),
86
+ );
87
+
88
+ const result = {
89
+ jobId: data.id,
90
+ phase: data.phase,
91
+ providerName: data.providerName ?? null,
92
+ providerWalletAddress: data.providerAddress ?? null,
93
+ clientName: data.clientName ?? null,
94
+ clientWalletAddress: data.clientAddress ?? null,
95
+ deliverable: data.deliverable,
96
+ memoHistory,
97
+ };
98
+ const linkedBountyId = getBountyByJobId(String(result.jobId))?.bountyId;
99
+
100
+ output.output(result, (r) => {
101
+ output.heading(`Job ${r.jobId} details`);
102
+ output.field("Phase", r.phase);
103
+ output.field("Client", r.clientName || "-");
104
+ output.field("Client Wallet", r.clientWalletAddress || "-");
105
+ output.field("Provider", r.providerName || "-");
106
+ output.field("Provider Wallet", r.providerWalletAddress || "-");
107
+ if (r.deliverable) {
108
+ output.log(`\n Deliverable:\n ${renderDeliverable(r.deliverable)}`);
109
+ }
110
+ if (r.memoHistory.length > 0) {
111
+ output.log("\n History:");
112
+ for (const m of r.memoHistory) {
113
+ output.log(` [${m.nextPhase}] ${m.content} (${m.createdAt})`);
114
+ }
115
+ }
116
+ if (linkedBountyId) {
117
+ output.log(`\n This job is linked to bounty ${linkedBountyId}.`);
118
+ output.log(
119
+ ` Run \`acp bounty status ${linkedBountyId}\` to sync bounty status.\n`,
120
+ );
121
+ }
122
+ output.log("");
123
+ });
124
+ } catch (e) {
125
+ output.fatal(
126
+ `Failed to get job status: ${e instanceof Error ? e.message : String(e)}`,
127
+ );
128
+ }
129
+ }
130
+
131
+ type JobListItem = {
132
+ id: number | string;
133
+ phase?: unknown;
134
+ price?: unknown;
135
+ priceType?: unknown;
136
+ clientAddress?: unknown;
137
+ providerAddress?: unknown;
138
+ name?: unknown;
139
+ deliverable?: unknown;
140
+ };
141
+
142
+ export type JobListOptions = {
143
+ page?: number;
144
+ pageSize?: number;
145
+ };
146
+
147
+ export async function active(options: JobListOptions = {}): Promise<void> {
148
+ try {
149
+ const params: Record<string, number> = {};
150
+ if (options.page != null) params.page = options.page;
151
+ if (options.pageSize != null) params.pageSize = options.pageSize;
152
+ const res = await client.get<{ data: JobListItem[] }>("/acp/jobs/active", {
153
+ params,
154
+ });
155
+ const jobs = res.data.data;
156
+
157
+ output.output({ jobs }, ({ jobs: list }) => {
158
+ output.heading("Active Jobs");
159
+ if (list.length === 0) {
160
+ output.log(" No active jobs.\n");
161
+ return;
162
+ }
163
+ for (const j of list) {
164
+ output.field("Job ID", j.id);
165
+ if (j.phase) output.field("Phase", j.phase);
166
+ if (j.name) output.field("Name", j.name);
167
+ if (j.price != null)
168
+ output.field("Price", formatPrice(j.price, j.priceType));
169
+ if (j.clientAddress) output.field("Client", j.clientAddress);
170
+ if (j.providerAddress) output.field("Provider", j.providerAddress);
171
+ if (j.deliverable) output.field("Deliverable", j.deliverable);
172
+ output.log("");
173
+ }
174
+ });
175
+ } catch (e) {
176
+ output.fatal(
177
+ `Failed to get active jobs: ${e instanceof Error ? e.message : String(e)}`,
178
+ );
179
+ }
180
+ }
181
+
182
+ export async function completed(options: JobListOptions = {}): Promise<void> {
183
+ try {
184
+ const params: Record<string, number> = {};
185
+ if (options.page != null) params.page = options.page;
186
+ if (options.pageSize != null) params.pageSize = options.pageSize;
187
+ const res = await client.get<{ data: JobListItem[] }>(
188
+ "/acp/jobs/completed",
189
+ {
190
+ params,
191
+ },
192
+ );
193
+ const jobs = res.data.data;
194
+
195
+ output.output({ jobs }, ({ jobs: list }) => {
196
+ output.heading("Completed Jobs");
197
+ if (list.length === 0) {
198
+ output.log(" No completed jobs.\n");
199
+ return;
200
+ }
201
+ for (const j of list) {
202
+ output.field("Job ID", j.id);
203
+ if (j.name) output.field("Name", j.name);
204
+ if (j.price != null)
205
+ output.field("Price", formatPrice(j.price, j.priceType));
206
+ if (j.clientAddress) output.field("Client", j.clientAddress);
207
+ if (j.providerAddress) output.field("Provider", j.providerAddress);
208
+ if (j.deliverable) output.field("Deliverable", j.deliverable);
209
+ output.log("");
210
+ }
211
+ });
212
+ } catch (e) {
213
+ output.fatal(
214
+ `Failed to get completed jobs: ${e instanceof Error ? e.message : String(e)}`,
215
+ );
216
+ }
217
+ }
@@ -0,0 +1,71 @@
1
+ // =============================================================================
2
+ // acp profile show — Show agent profile
3
+ // acp profile update — Update agent info (name, description, profilePic)
4
+ // =============================================================================
5
+
6
+ import client from "../lib/client.js";
7
+ import { getMyAgentInfo } from "../lib/wallet.js";
8
+ import * as output from "../lib/output.js";
9
+
10
+ export async function show(): Promise<void> {
11
+ try {
12
+ const info = await getMyAgentInfo();
13
+
14
+ output.output(info, (data) => {
15
+ output.heading("Agent Profile");
16
+ output.field("Name", data.name);
17
+ output.field("Description", data.description || "(none)");
18
+ output.field("Wallet", data.walletAddress);
19
+ output.field(
20
+ "Token",
21
+ data.token?.symbol
22
+ ? `${data.token.symbol} (${data.tokenAddress})`
23
+ : data.tokenAddress || "(none)",
24
+ );
25
+ if (data.jobs?.length > 0) {
26
+ output.log("\n Job Offerings:");
27
+ for (const o of data.jobs) {
28
+ const price = o.priceV2
29
+ ? `${o.priceV2.value} ${o.priceV2.type === "fixed" ? "USDC" : ""} (${o.priceV2.type})`
30
+ : "-";
31
+ output.log(` - ${o.name} fee: ${price} sla: ${o.slaMinutes}min`);
32
+ }
33
+ }
34
+ output.log("");
35
+ });
36
+ } catch (e) {
37
+ output.fatal(
38
+ `Failed to get profile: ${e instanceof Error ? e.message : String(e)}`,
39
+ );
40
+ }
41
+ }
42
+
43
+ export async function update(key: string, value: string): Promise<void> {
44
+ const supportedKeys = ["name", "description", "profilePic"];
45
+
46
+ if (!key?.trim() || !value?.trim()) {
47
+ output.fatal(
48
+ `Usage: acp profile update <key> <value>\n Supported keys: ${supportedKeys.join(", ")}`,
49
+ );
50
+ }
51
+
52
+ if (!supportedKeys.includes(key)) {
53
+ output.fatal(
54
+ `Invalid key: ${key}. Supported keys: ${supportedKeys.join(", ")}`,
55
+ );
56
+ }
57
+
58
+ try {
59
+ const agent = await client.put("/acp/me", { [key]: value });
60
+
61
+ output.output(agent.data, (data) => {
62
+ output.heading("Profile Updated");
63
+ output.log(` ${key} set to: "${value}"`);
64
+ output.log("");
65
+ });
66
+ } catch (e) {
67
+ output.fatal(
68
+ `Failed to update profile: ${e instanceof Error ? e.message : String(e)}`,
69
+ );
70
+ }
71
+ }