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.
- package/.dockerignore +4 -0
- package/.env.example +30 -0
- package/AGENTS.md +212 -0
- package/BOOTSTRAP.md +55 -0
- package/Dockerfile +52 -0
- package/HEARTBEAT.md +7 -0
- package/IDENTITY.md +23 -0
- package/LICENSE +21 -0
- package/README.md +162 -0
- package/SOUL.md +202 -0
- package/SUBMISSION.md +128 -0
- package/TOOLS.md +40 -0
- package/USER.md +17 -0
- package/acp-seller/bin/acp.ts +807 -0
- package/acp-seller/config.json +34 -0
- package/acp-seller/package.json +55 -0
- package/acp-seller/src/commands/agent.ts +328 -0
- package/acp-seller/src/commands/bounty.ts +1189 -0
- package/acp-seller/src/commands/deploy.ts +414 -0
- package/acp-seller/src/commands/job.ts +217 -0
- package/acp-seller/src/commands/profile.ts +71 -0
- package/acp-seller/src/commands/resource.ts +91 -0
- package/acp-seller/src/commands/search.ts +327 -0
- package/acp-seller/src/commands/sell.ts +883 -0
- package/acp-seller/src/commands/serve.ts +258 -0
- package/acp-seller/src/commands/setup.ts +399 -0
- package/acp-seller/src/commands/token.ts +88 -0
- package/acp-seller/src/commands/wallet.ts +123 -0
- package/acp-seller/src/lib/api.ts +118 -0
- package/acp-seller/src/lib/auth.ts +291 -0
- package/acp-seller/src/lib/bounty.ts +257 -0
- package/acp-seller/src/lib/client.ts +42 -0
- package/acp-seller/src/lib/config.ts +240 -0
- package/acp-seller/src/lib/open.ts +41 -0
- package/acp-seller/src/lib/openclawCron.ts +138 -0
- package/acp-seller/src/lib/output.ts +104 -0
- package/acp-seller/src/lib/wallet.ts +81 -0
- package/acp-seller/src/seller/offerings/_shared/preTransactionScan.ts +127 -0
- package/acp-seller/src/seller/offerings/canonical-catalog.ts +221 -0
- package/acp-seller/src/seller/offerings/spendos/spendos_summarize_url/handlers.ts +20 -0
- package/acp-seller/src/seller/offerings/spendos/spendos_summarize_url/offering.json +18 -0
- package/acp-seller/src/seller/offerings/spendos/spendos_translate/handlers.ts +21 -0
- package/acp-seller/src/seller/offerings/spendos/spendos_translate/offering.json +22 -0
- package/acp-seller/src/seller/offerings/spendos/spendos_tweet_gen/handlers.ts +20 -0
- package/acp-seller/src/seller/offerings/spendos/spendos_tweet_gen/offering.json +18 -0
- package/acp-seller/src/seller/runtime/acpSocket.ts +413 -0
- package/acp-seller/src/seller/runtime/logger.ts +36 -0
- package/acp-seller/src/seller/runtime/offeringTypes.ts +52 -0
- package/acp-seller/src/seller/runtime/offerings.ts +277 -0
- package/acp-seller/src/seller/runtime/paymentVerification.test.ts +207 -0
- package/acp-seller/src/seller/runtime/paymentVerification.ts +363 -0
- package/acp-seller/src/seller/runtime/seller.onchain.test.ts +220 -0
- package/acp-seller/src/seller/runtime/seller.test.ts +823 -0
- package/acp-seller/src/seller/runtime/seller.ts +1041 -0
- package/acp-seller/src/seller/runtime/sellerApi.ts +71 -0
- package/acp-seller/src/seller/runtime/startup.ts +270 -0
- package/acp-seller/src/seller/runtime/types.ts +62 -0
- package/acp-seller/tsconfig.json +20 -0
- package/bin/spendos.js +23 -0
- package/contracts/SpendOSAudit.sol +29 -0
- package/dist/mcp-server.mjs +153 -0
- package/jobs/translate.json +7 -0
- package/jobs/tweet-gen.json +7 -0
- package/openclaw.json +41 -0
- package/package.json +49 -0
- package/plugins/spendos-events/index.ts +78 -0
- package/plugins/spendos-events/package.json +14 -0
- package/policies/enforce-bounds.mjs +71 -0
- package/public/index.html +509 -0
- package/public/landing.html +241 -0
- package/railway.json +12 -0
- package/railway.toml +12 -0
- package/scripts/deploy.ts +48 -0
- package/scripts/test-x402-mainnet.ts +30 -0
- package/scripts/xmtp-listener.ts +61 -0
- package/setup.sh +278 -0
- package/skills/spendos/skill.md +26 -0
- package/src/agent.ts +152 -0
- package/src/audit.ts +166 -0
- package/src/governance.ts +367 -0
- package/src/job-registry.ts +306 -0
- package/src/mcp-public.ts +145 -0
- package/src/mcp-server.ts +171 -0
- package/src/opportunity-scanner.ts +138 -0
- package/src/server.ts +870 -0
- package/src/venice-x402.ts +234 -0
- package/src/xmtp.ts +109 -0
- package/src/zerion.ts +58 -0
- package/start.sh +168 -0
- package/tsconfig.json +14 -0
|
@@ -0,0 +1,1189 @@
|
|
|
1
|
+
// =============================================================================
|
|
2
|
+
// acp bounty create [query]
|
|
3
|
+
// acp bounty list
|
|
4
|
+
// acp bounty status <bountyId>
|
|
5
|
+
// acp bounty select <bountyId>
|
|
6
|
+
// =============================================================================
|
|
7
|
+
|
|
8
|
+
import readline from "readline";
|
|
9
|
+
import client from "../lib/client.js";
|
|
10
|
+
import * as output from "../lib/output.js";
|
|
11
|
+
import { requireActiveAgent } from "../lib/wallet.js";
|
|
12
|
+
import {
|
|
13
|
+
type ActiveBounty,
|
|
14
|
+
type BountyCreateInput,
|
|
15
|
+
createBounty,
|
|
16
|
+
getActiveBounty,
|
|
17
|
+
getBountyDetails,
|
|
18
|
+
getMatchStatus,
|
|
19
|
+
listActiveBounties,
|
|
20
|
+
removeActiveBounty,
|
|
21
|
+
rejectCandidates,
|
|
22
|
+
saveActiveBounty,
|
|
23
|
+
syncBountyJobStatus,
|
|
24
|
+
confirmMatch,
|
|
25
|
+
updateBounty,
|
|
26
|
+
} from "../lib/bounty.js";
|
|
27
|
+
import { ROOT } from "../lib/config.js";
|
|
28
|
+
import {
|
|
29
|
+
ensureBountyPollCron,
|
|
30
|
+
removeBountyPollCronIfUnused,
|
|
31
|
+
} from "../lib/openclawCron.js";
|
|
32
|
+
|
|
33
|
+
function question(rl: readline.Interface, prompt: string): Promise<string> {
|
|
34
|
+
return new Promise((resolve) => rl.question(prompt, resolve));
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function parseCandidateId(raw: unknown): number | null {
|
|
38
|
+
if (typeof raw === "number" && Number.isFinite(raw)) return raw;
|
|
39
|
+
if (typeof raw === "string" && /^\d+$/.test(raw.trim())) {
|
|
40
|
+
return parseInt(raw.trim(), 10);
|
|
41
|
+
}
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function candidateField(candidate: any, names: string[]): string | undefined {
|
|
46
|
+
for (const name of names) {
|
|
47
|
+
const value = candidate?.[name];
|
|
48
|
+
if (typeof value === "string" && value.trim()) return value.trim();
|
|
49
|
+
}
|
|
50
|
+
return undefined;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function candidatePriceDisplay(candidate: Record<string, unknown>): string {
|
|
54
|
+
const rawPrice =
|
|
55
|
+
candidate.price ??
|
|
56
|
+
candidate.job_offering_price ??
|
|
57
|
+
candidate.jobOfferingPrice ??
|
|
58
|
+
candidate.job_fee ??
|
|
59
|
+
candidate.jobFee ??
|
|
60
|
+
candidate.fee;
|
|
61
|
+
const rawType =
|
|
62
|
+
candidate.priceType ??
|
|
63
|
+
candidate.price_type ??
|
|
64
|
+
candidate.jobFeeType ??
|
|
65
|
+
candidate.job_fee_type;
|
|
66
|
+
|
|
67
|
+
if (rawPrice == null) return "Unknown";
|
|
68
|
+
const price = String(rawPrice);
|
|
69
|
+
const type = rawType != null ? String(rawType).toLowerCase() : "";
|
|
70
|
+
if (type === "fixed") return `${price} USDC`;
|
|
71
|
+
if (type === "percentage") return `${price} (${type})`;
|
|
72
|
+
return rawType != null ? `${price} ${String(rawType)}` : price;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
type JsonSchemaProperty = {
|
|
76
|
+
type?: string;
|
|
77
|
+
description?: string;
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
type RequirementSchema = {
|
|
81
|
+
type?: string;
|
|
82
|
+
required?: string[];
|
|
83
|
+
properties?: Record<string, JsonSchemaProperty>;
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
function getCandidateRequirementSchema(
|
|
87
|
+
candidate: Record<string, unknown>,
|
|
88
|
+
): RequirementSchema | null {
|
|
89
|
+
const schemaCandidate =
|
|
90
|
+
candidate.requirementSchema ??
|
|
91
|
+
candidate.requirement_schema ??
|
|
92
|
+
candidate.requirement;
|
|
93
|
+
if (
|
|
94
|
+
!schemaCandidate ||
|
|
95
|
+
typeof schemaCandidate !== "object" ||
|
|
96
|
+
Array.isArray(schemaCandidate)
|
|
97
|
+
) {
|
|
98
|
+
return null;
|
|
99
|
+
}
|
|
100
|
+
return schemaCandidate as RequirementSchema;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
async function collectRequirementsFromSchema(
|
|
104
|
+
rl: readline.Interface,
|
|
105
|
+
schema: RequirementSchema,
|
|
106
|
+
): Promise<Record<string, unknown>> {
|
|
107
|
+
const properties = schema.properties ?? {};
|
|
108
|
+
const requiredSet = new Set(
|
|
109
|
+
(schema.required ?? []).filter((k) => typeof k === "string"),
|
|
110
|
+
);
|
|
111
|
+
const keys = Object.keys(properties);
|
|
112
|
+
const out: Record<string, unknown> = {};
|
|
113
|
+
|
|
114
|
+
if (keys.length === 0) return out;
|
|
115
|
+
|
|
116
|
+
output.log("\n Fill service requirements:");
|
|
117
|
+
for (const key of keys) {
|
|
118
|
+
const prop = properties[key] ?? {};
|
|
119
|
+
const isRequired = requiredSet.has(key);
|
|
120
|
+
const desc =
|
|
121
|
+
typeof prop.description === "string" && prop.description.trim()
|
|
122
|
+
? ` - ${prop.description.trim()}`
|
|
123
|
+
: "";
|
|
124
|
+
while (true) {
|
|
125
|
+
const answer = (
|
|
126
|
+
await question(
|
|
127
|
+
rl,
|
|
128
|
+
` ${key}${isRequired ? " [required]" : " [optional]"}${desc}: `,
|
|
129
|
+
)
|
|
130
|
+
).trim();
|
|
131
|
+
|
|
132
|
+
if (!answer) {
|
|
133
|
+
if (isRequired) {
|
|
134
|
+
output.error(`"${key}" is required.`);
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
out[key] = "";
|
|
138
|
+
break;
|
|
139
|
+
}
|
|
140
|
+
out[key] = answer;
|
|
141
|
+
break;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return out;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export async function createInteractive(
|
|
149
|
+
query?: string,
|
|
150
|
+
sourceChannel?: string,
|
|
151
|
+
): Promise<void> {
|
|
152
|
+
if (output.isJsonMode()) {
|
|
153
|
+
output.fatal(
|
|
154
|
+
"Interactive bounty creation is not supported in --json mode. Use human mode.",
|
|
155
|
+
);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const rl = readline.createInterface({
|
|
159
|
+
input: process.stdin,
|
|
160
|
+
output: process.stdout,
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
try {
|
|
164
|
+
const agent = await requireActiveAgent();
|
|
165
|
+
|
|
166
|
+
const querySeed = query?.trim() || "";
|
|
167
|
+
const defaultTitle = querySeed ? `${querySeed}` : "Need service provider";
|
|
168
|
+
const defaultDescription = querySeed
|
|
169
|
+
? `${querySeed}`
|
|
170
|
+
: "Need a provider to fulfill this request.";
|
|
171
|
+
|
|
172
|
+
const title =
|
|
173
|
+
(await question(rl, ` Title [${defaultTitle}]: `)).trim() ||
|
|
174
|
+
defaultTitle;
|
|
175
|
+
const description =
|
|
176
|
+
(await question(rl, ` Description [${defaultDescription}]: `)).trim() ||
|
|
177
|
+
defaultDescription;
|
|
178
|
+
const budgetRaw = (
|
|
179
|
+
await question(rl, " Budget in USD (number, e.g. 50): ")
|
|
180
|
+
).trim();
|
|
181
|
+
let categoryInput = "digital";
|
|
182
|
+
while (true) {
|
|
183
|
+
const raw = (
|
|
184
|
+
await question(rl, " Category [digital|physical] (default: digital): ")
|
|
185
|
+
)
|
|
186
|
+
.trim()
|
|
187
|
+
.toLowerCase();
|
|
188
|
+
if (!raw) {
|
|
189
|
+
categoryInput = "digital";
|
|
190
|
+
break;
|
|
191
|
+
}
|
|
192
|
+
if (raw === "digital" || raw === "physical") {
|
|
193
|
+
categoryInput = raw;
|
|
194
|
+
break;
|
|
195
|
+
}
|
|
196
|
+
output.error('Invalid category. Enter only "digital" or "physical".');
|
|
197
|
+
}
|
|
198
|
+
const tags =
|
|
199
|
+
(await question(rl, ` Tags comma-separated [defi,web3,ai]: `)).trim() ||
|
|
200
|
+
"";
|
|
201
|
+
|
|
202
|
+
const budget = Number(budgetRaw);
|
|
203
|
+
if (!Number.isFinite(budget) || budget <= 0) {
|
|
204
|
+
output.fatal("Budget must be a positive number.");
|
|
205
|
+
}
|
|
206
|
+
const payload: BountyCreateInput = {
|
|
207
|
+
title,
|
|
208
|
+
description,
|
|
209
|
+
budget,
|
|
210
|
+
category: categoryInput,
|
|
211
|
+
tags,
|
|
212
|
+
};
|
|
213
|
+
|
|
214
|
+
const created = await createBounty(payload);
|
|
215
|
+
|
|
216
|
+
const active: ActiveBounty = {
|
|
217
|
+
bountyId: created.bountyId,
|
|
218
|
+
createdAt: new Date().toISOString(),
|
|
219
|
+
status: "open",
|
|
220
|
+
title,
|
|
221
|
+
description,
|
|
222
|
+
budget,
|
|
223
|
+
category: categoryInput,
|
|
224
|
+
tags,
|
|
225
|
+
posterName: agent.name,
|
|
226
|
+
posterSecret: created.posterSecret,
|
|
227
|
+
sourceChannel: sourceChannel || "cli",
|
|
228
|
+
};
|
|
229
|
+
saveActiveBounty(active);
|
|
230
|
+
|
|
231
|
+
let cronRegistered = false;
|
|
232
|
+
try {
|
|
233
|
+
const cron = ensureBountyPollCron();
|
|
234
|
+
cronRegistered = cron.enabled && cron.created;
|
|
235
|
+
if (cronRegistered) {
|
|
236
|
+
output.log(" OpenClaw cron job registered for `acp bounty poll`.\n");
|
|
237
|
+
}
|
|
238
|
+
} catch {
|
|
239
|
+
// Cron registration failed — include instruction in output
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
output.output(
|
|
243
|
+
{
|
|
244
|
+
bountyId: created.bountyId,
|
|
245
|
+
status: "open",
|
|
246
|
+
cronRegistered,
|
|
247
|
+
...(!cronRegistered
|
|
248
|
+
? {
|
|
249
|
+
cronAction: "required",
|
|
250
|
+
cronCommand: `openclaw cron add --id "openclaw-acp-bounty-poll" --schedule "*/10 * * * *" --command "cd \\"${ROOT}\\" && npx acp bounty poll --json"`,
|
|
251
|
+
}
|
|
252
|
+
: {}),
|
|
253
|
+
},
|
|
254
|
+
(data) => {
|
|
255
|
+
output.heading("Bounty Created");
|
|
256
|
+
output.field("Bounty ID", data.bountyId);
|
|
257
|
+
output.field("Status", data.status);
|
|
258
|
+
if (!cronRegistered) {
|
|
259
|
+
output.log(
|
|
260
|
+
"\n IMPORTANT: Register the bounty poll cron job by running:",
|
|
261
|
+
);
|
|
262
|
+
output.log(
|
|
263
|
+
` openclaw cron add --id "openclaw-acp-bounty-poll" --schedule "*/10 * * * *" --command "cd \\"${ROOT}\\" && npx acp bounty poll --json"`,
|
|
264
|
+
);
|
|
265
|
+
}
|
|
266
|
+
output.log("");
|
|
267
|
+
},
|
|
268
|
+
);
|
|
269
|
+
} finally {
|
|
270
|
+
rl.close();
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
export interface BountyCreateFlags {
|
|
275
|
+
title?: string;
|
|
276
|
+
description?: string;
|
|
277
|
+
budget?: number;
|
|
278
|
+
category?: string;
|
|
279
|
+
tags?: string;
|
|
280
|
+
sourceChannel?: string;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
export async function createFromFlags(flags: BountyCreateFlags): Promise<void> {
|
|
284
|
+
const agent = await requireActiveAgent();
|
|
285
|
+
|
|
286
|
+
const title = flags.title?.trim();
|
|
287
|
+
const description = flags.description?.trim() || title;
|
|
288
|
+
const budget = flags.budget;
|
|
289
|
+
const category = (flags.category?.trim() || "digital").toLowerCase();
|
|
290
|
+
const tags = flags.tags?.trim() || "";
|
|
291
|
+
const sourceChannel = flags.sourceChannel?.trim() || "cli";
|
|
292
|
+
|
|
293
|
+
if (!title) output.fatal("--title is required.");
|
|
294
|
+
if (budget == null || !Number.isFinite(budget) || budget <= 0) {
|
|
295
|
+
output.fatal("--budget must be a positive number.");
|
|
296
|
+
}
|
|
297
|
+
if (category !== "digital" && category !== "physical") {
|
|
298
|
+
output.fatal('--category must be "digital" or "physical".');
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
const payload: BountyCreateInput = {
|
|
302
|
+
title: title!,
|
|
303
|
+
description: description || title!,
|
|
304
|
+
budget: budget!,
|
|
305
|
+
category,
|
|
306
|
+
tags,
|
|
307
|
+
};
|
|
308
|
+
|
|
309
|
+
const created = await createBounty(payload);
|
|
310
|
+
|
|
311
|
+
const active: ActiveBounty = {
|
|
312
|
+
bountyId: created.bountyId,
|
|
313
|
+
createdAt: new Date().toISOString(),
|
|
314
|
+
status: "open",
|
|
315
|
+
title: title!,
|
|
316
|
+
description: description || title!,
|
|
317
|
+
budget: budget!,
|
|
318
|
+
category,
|
|
319
|
+
tags,
|
|
320
|
+
posterName: agent.name,
|
|
321
|
+
posterSecret: created.posterSecret,
|
|
322
|
+
...(sourceChannel ? { sourceChannel } : {}),
|
|
323
|
+
};
|
|
324
|
+
saveActiveBounty(active);
|
|
325
|
+
|
|
326
|
+
let cronRegistered = false;
|
|
327
|
+
try {
|
|
328
|
+
const cron = ensureBountyPollCron();
|
|
329
|
+
cronRegistered = cron.enabled && cron.created;
|
|
330
|
+
if (cronRegistered) {
|
|
331
|
+
output.log(" OpenClaw cron job registered for `acp bounty poll`.\n");
|
|
332
|
+
}
|
|
333
|
+
} catch {
|
|
334
|
+
// Cron registration failed — include instruction in output
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
output.output(
|
|
338
|
+
{
|
|
339
|
+
bountyId: created.bountyId,
|
|
340
|
+
status: "open",
|
|
341
|
+
sourceChannel: sourceChannel || null,
|
|
342
|
+
cronRegistered,
|
|
343
|
+
...(!cronRegistered
|
|
344
|
+
? {
|
|
345
|
+
cronAction: "required",
|
|
346
|
+
cronCommand: `openclaw cron add --id "openclaw-acp-bounty-poll" --schedule "*/10 * * * *" --command "cd \\"${ROOT}\\" && npx acp bounty poll --json"`,
|
|
347
|
+
}
|
|
348
|
+
: {}),
|
|
349
|
+
},
|
|
350
|
+
(data) => {
|
|
351
|
+
output.heading("Bounty Created");
|
|
352
|
+
output.field("Bounty ID", data.bountyId);
|
|
353
|
+
output.field("Status", data.status);
|
|
354
|
+
if (data.sourceChannel)
|
|
355
|
+
output.field("Source Channel", data.sourceChannel);
|
|
356
|
+
if (!cronRegistered) {
|
|
357
|
+
output.log(
|
|
358
|
+
"\n IMPORTANT: Register the bounty poll cron job by running:",
|
|
359
|
+
);
|
|
360
|
+
output.log(
|
|
361
|
+
` openclaw cron add --id "openclaw-acp-bounty-poll" --schedule "*/10 * * * *" --command "cd \\"${ROOT}\\" && npx acp bounty poll --json"`,
|
|
362
|
+
);
|
|
363
|
+
}
|
|
364
|
+
output.log("");
|
|
365
|
+
},
|
|
366
|
+
);
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
export async function create(
|
|
370
|
+
query?: string,
|
|
371
|
+
flags?: BountyCreateFlags,
|
|
372
|
+
): Promise<void> {
|
|
373
|
+
// If any structured flag is provided, use the non-interactive path
|
|
374
|
+
if (flags && (flags.title || flags.budget != null)) {
|
|
375
|
+
return createFromFlags(flags);
|
|
376
|
+
}
|
|
377
|
+
return createInteractive(query, flags?.sourceChannel);
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
export async function list(): Promise<void> {
|
|
381
|
+
const bounties = listActiveBounties();
|
|
382
|
+
output.output({ bounties }, (data) => {
|
|
383
|
+
output.heading("Active Bounties");
|
|
384
|
+
if (data.bounties.length === 0) {
|
|
385
|
+
output.log(" No active bounties.\n");
|
|
386
|
+
return;
|
|
387
|
+
}
|
|
388
|
+
for (const b of data.bounties) {
|
|
389
|
+
output.field("Bounty ID", b.bountyId);
|
|
390
|
+
output.field("Status", b.status);
|
|
391
|
+
output.field("Title", b.title);
|
|
392
|
+
if (b.acpJobId) output.field("ACP Job ID", b.acpJobId);
|
|
393
|
+
output.log("");
|
|
394
|
+
}
|
|
395
|
+
});
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
function normalizeCandidateForWatch(
|
|
399
|
+
candidate: Record<string, unknown>,
|
|
400
|
+
): Record<string, unknown> {
|
|
401
|
+
return {
|
|
402
|
+
id: candidate.id,
|
|
403
|
+
agentName:
|
|
404
|
+
candidateField(candidate, ["agent_name", "agentName", "name"]) ||
|
|
405
|
+
"(unknown)",
|
|
406
|
+
agentWallet:
|
|
407
|
+
candidateField(candidate, [
|
|
408
|
+
"agent_wallet",
|
|
409
|
+
"agentWallet",
|
|
410
|
+
"agent_wallet_address",
|
|
411
|
+
"agentWalletAddress",
|
|
412
|
+
"walletAddress",
|
|
413
|
+
"providerWalletAddress",
|
|
414
|
+
"provider_address",
|
|
415
|
+
]) || "",
|
|
416
|
+
offeringName:
|
|
417
|
+
candidateField(candidate, [
|
|
418
|
+
"job_offering",
|
|
419
|
+
"jobOffering",
|
|
420
|
+
"offeringName",
|
|
421
|
+
"jobOfferingName",
|
|
422
|
+
"offering_name",
|
|
423
|
+
"name",
|
|
424
|
+
]) || "",
|
|
425
|
+
price:
|
|
426
|
+
candidate.price ??
|
|
427
|
+
candidate.job_offering_price ??
|
|
428
|
+
candidate.jobOfferingPrice ??
|
|
429
|
+
candidate.job_fee ??
|
|
430
|
+
candidate.jobFee ??
|
|
431
|
+
candidate.fee ??
|
|
432
|
+
null,
|
|
433
|
+
priceType:
|
|
434
|
+
candidate.priceType ??
|
|
435
|
+
candidate.price_type ??
|
|
436
|
+
candidate.jobFeeType ??
|
|
437
|
+
candidate.job_fee_type ??
|
|
438
|
+
null,
|
|
439
|
+
requirementSchema:
|
|
440
|
+
candidate.requirementSchema ??
|
|
441
|
+
candidate.requirement_schema ??
|
|
442
|
+
candidate.requirement ??
|
|
443
|
+
null,
|
|
444
|
+
};
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
export async function poll(): Promise<void> {
|
|
448
|
+
const bounties = listActiveBounties();
|
|
449
|
+
const result: {
|
|
450
|
+
checked: number;
|
|
451
|
+
pendingMatch: Array<{
|
|
452
|
+
bountyId: string;
|
|
453
|
+
title: string;
|
|
454
|
+
description: string;
|
|
455
|
+
budget: number;
|
|
456
|
+
sourceChannel?: string;
|
|
457
|
+
candidates: Record<string, unknown>[];
|
|
458
|
+
}>;
|
|
459
|
+
claimedJobs: Array<{
|
|
460
|
+
bountyId: string;
|
|
461
|
+
acpJobId: string;
|
|
462
|
+
title: string;
|
|
463
|
+
jobPhase: string;
|
|
464
|
+
deliverable?: string;
|
|
465
|
+
sourceChannel?: string;
|
|
466
|
+
}>;
|
|
467
|
+
rejectedByProvider: Array<{
|
|
468
|
+
bountyId: string;
|
|
469
|
+
title: string;
|
|
470
|
+
description: string;
|
|
471
|
+
budget: number;
|
|
472
|
+
sourceChannel?: string;
|
|
473
|
+
candidates: Record<string, unknown>[];
|
|
474
|
+
}>;
|
|
475
|
+
cleaned: Array<{
|
|
476
|
+
bountyId: string;
|
|
477
|
+
title: string;
|
|
478
|
+
status: string;
|
|
479
|
+
sourceChannel?: string;
|
|
480
|
+
}>;
|
|
481
|
+
errors: Array<{
|
|
482
|
+
bountyId: string;
|
|
483
|
+
error: string;
|
|
484
|
+
}>;
|
|
485
|
+
} = {
|
|
486
|
+
checked: 0,
|
|
487
|
+
pendingMatch: [],
|
|
488
|
+
claimedJobs: [],
|
|
489
|
+
rejectedByProvider: [],
|
|
490
|
+
cleaned: [],
|
|
491
|
+
errors: [],
|
|
492
|
+
};
|
|
493
|
+
|
|
494
|
+
for (const b of bounties) {
|
|
495
|
+
result.checked += 1;
|
|
496
|
+
try {
|
|
497
|
+
// --- Claimed bounties: track ACP job status ---
|
|
498
|
+
if (b.status === "claimed" && !b.acpJobId) {
|
|
499
|
+
const remote = await getMatchStatus(b.bountyId);
|
|
500
|
+
const remoteJobId = String(remote.acp_job_id ?? "");
|
|
501
|
+
if (remoteJobId) {
|
|
502
|
+
saveActiveBounty({ ...b, acpJobId: remoteJobId });
|
|
503
|
+
b.acpJobId = remoteJobId;
|
|
504
|
+
} else {
|
|
505
|
+
result.errors.push({
|
|
506
|
+
bountyId: b.bountyId,
|
|
507
|
+
error:
|
|
508
|
+
"Bounty is claimed but missing acpJobId — resetting local claim state",
|
|
509
|
+
});
|
|
510
|
+
|
|
511
|
+
const reset: ActiveBounty = {
|
|
512
|
+
...b,
|
|
513
|
+
status: "open",
|
|
514
|
+
selectedCandidateId: undefined,
|
|
515
|
+
acpJobId: undefined,
|
|
516
|
+
notifiedPendingMatch: false,
|
|
517
|
+
};
|
|
518
|
+
saveActiveBounty(reset);
|
|
519
|
+
|
|
520
|
+
b.status = "open";
|
|
521
|
+
b.selectedCandidateId = undefined;
|
|
522
|
+
b.acpJobId = undefined;
|
|
523
|
+
b.notifiedPendingMatch = false;
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
if (b.status === "claimed" && b.acpJobId) {
|
|
527
|
+
let jobPhase = "";
|
|
528
|
+
let deliverable: string | undefined;
|
|
529
|
+
try {
|
|
530
|
+
const jobRes = await client.get(`/acp/jobs/${b.acpJobId}`);
|
|
531
|
+
const jobData = jobRes.data?.data ?? jobRes.data;
|
|
532
|
+
jobPhase = String(jobData?.phase ?? "").toUpperCase();
|
|
533
|
+
deliverable = jobData?.deliverable ?? undefined;
|
|
534
|
+
} catch {
|
|
535
|
+
// If job fetch fails, skip this bounty for now
|
|
536
|
+
result.errors.push({
|
|
537
|
+
bountyId: b.bountyId,
|
|
538
|
+
error: `Failed to fetch ACP job ${b.acpJobId} status`,
|
|
539
|
+
});
|
|
540
|
+
continue;
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
const isTerminalJob =
|
|
544
|
+
jobPhase === "COMPLETED" ||
|
|
545
|
+
jobPhase === "REJECTED" ||
|
|
546
|
+
jobPhase === "EXPIRED";
|
|
547
|
+
|
|
548
|
+
if (jobPhase === "REJECTED") {
|
|
549
|
+
// Provider rejected — sync with backend (switches bounty back to open)
|
|
550
|
+
if (b.posterSecret) {
|
|
551
|
+
try {
|
|
552
|
+
await syncBountyJobStatus({
|
|
553
|
+
bountyId: b.bountyId,
|
|
554
|
+
posterSecret: b.posterSecret,
|
|
555
|
+
});
|
|
556
|
+
} catch {
|
|
557
|
+
// non-fatal
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
// Reset local state: back to open, clear job fields, allow re-notification
|
|
562
|
+
const reset: ActiveBounty = {
|
|
563
|
+
...b,
|
|
564
|
+
status: "open",
|
|
565
|
+
selectedCandidateId: undefined,
|
|
566
|
+
acpJobId: undefined,
|
|
567
|
+
notifiedPendingMatch: false,
|
|
568
|
+
};
|
|
569
|
+
saveActiveBounty(reset);
|
|
570
|
+
|
|
571
|
+
// Re-fetch to check if backend already has new candidates
|
|
572
|
+
let candidates: Record<string, unknown>[] = [];
|
|
573
|
+
try {
|
|
574
|
+
const fresh = await getMatchStatus(b.bountyId);
|
|
575
|
+
const freshStatus = String(fresh.status).toLowerCase();
|
|
576
|
+
if (
|
|
577
|
+
freshStatus === "pending_match" &&
|
|
578
|
+
Array.isArray(fresh.candidates) &&
|
|
579
|
+
fresh.candidates.length > 0
|
|
580
|
+
) {
|
|
581
|
+
candidates = fresh.candidates.map((c) =>
|
|
582
|
+
normalizeCandidateForWatch(c as Record<string, unknown>),
|
|
583
|
+
);
|
|
584
|
+
saveActiveBounty({
|
|
585
|
+
...reset,
|
|
586
|
+
status: "pending_match",
|
|
587
|
+
notifiedPendingMatch: true,
|
|
588
|
+
});
|
|
589
|
+
}
|
|
590
|
+
} catch {
|
|
591
|
+
// non-fatal — candidates will be picked up on next poll
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
result.rejectedByProvider.push({
|
|
595
|
+
bountyId: b.bountyId,
|
|
596
|
+
title: b.title,
|
|
597
|
+
description: b.description,
|
|
598
|
+
budget: b.budget,
|
|
599
|
+
...(b.sourceChannel ? { sourceChannel: b.sourceChannel } : {}),
|
|
600
|
+
candidates,
|
|
601
|
+
});
|
|
602
|
+
} else if (isTerminalJob) {
|
|
603
|
+
// COMPLETED or EXPIRED — clean up
|
|
604
|
+
if (b.posterSecret) {
|
|
605
|
+
try {
|
|
606
|
+
await syncBountyJobStatus({
|
|
607
|
+
bountyId: b.bountyId,
|
|
608
|
+
posterSecret: b.posterSecret,
|
|
609
|
+
});
|
|
610
|
+
} catch {
|
|
611
|
+
// non-fatal — continue with cleanup
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
const terminalStatus =
|
|
616
|
+
jobPhase === "COMPLETED" ? "fulfilled" : jobPhase.toLowerCase();
|
|
617
|
+
removeActiveBounty(b.bountyId);
|
|
618
|
+
result.cleaned.push({
|
|
619
|
+
bountyId: b.bountyId,
|
|
620
|
+
status: terminalStatus,
|
|
621
|
+
title: b.title,
|
|
622
|
+
...(b.sourceChannel ? { sourceChannel: b.sourceChannel } : {}),
|
|
623
|
+
});
|
|
624
|
+
} else {
|
|
625
|
+
// Job still in progress — save current phase
|
|
626
|
+
saveActiveBounty({ ...b });
|
|
627
|
+
result.claimedJobs.push({
|
|
628
|
+
bountyId: b.bountyId,
|
|
629
|
+
acpJobId: b.acpJobId,
|
|
630
|
+
title: b.title,
|
|
631
|
+
jobPhase,
|
|
632
|
+
deliverable,
|
|
633
|
+
...(b.sourceChannel ? { sourceChannel: b.sourceChannel } : {}),
|
|
634
|
+
});
|
|
635
|
+
}
|
|
636
|
+
continue;
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
// --- Non-claimed bounties: check match status ---
|
|
640
|
+
const remote = await getMatchStatus(b.bountyId);
|
|
641
|
+
const status = String(remote.status).toLowerCase();
|
|
642
|
+
|
|
643
|
+
if (
|
|
644
|
+
status === "fulfilled" ||
|
|
645
|
+
status === "expired" ||
|
|
646
|
+
status === "rejected"
|
|
647
|
+
) {
|
|
648
|
+
removeActiveBounty(b.bountyId);
|
|
649
|
+
result.cleaned.push({
|
|
650
|
+
bountyId: b.bountyId,
|
|
651
|
+
title: b.title,
|
|
652
|
+
status,
|
|
653
|
+
...(b.sourceChannel ? { sourceChannel: b.sourceChannel } : {}),
|
|
654
|
+
});
|
|
655
|
+
continue;
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
if (status === "claimed") {
|
|
659
|
+
const remoteJobId = String(remote.acp_job_id ?? "");
|
|
660
|
+
if (remoteJobId) {
|
|
661
|
+
saveActiveBounty({ ...b, status: "claimed", acpJobId: remoteJobId });
|
|
662
|
+
result.claimedJobs.push({
|
|
663
|
+
bountyId: b.bountyId,
|
|
664
|
+
acpJobId: remoteJobId,
|
|
665
|
+
title: b.title,
|
|
666
|
+
jobPhase: "UNKNOWN",
|
|
667
|
+
...(b.sourceChannel ? { sourceChannel: b.sourceChannel } : {}),
|
|
668
|
+
});
|
|
669
|
+
} else {
|
|
670
|
+
saveActiveBounty({ ...b, status: "claimed" });
|
|
671
|
+
result.errors.push({
|
|
672
|
+
bountyId: b.bountyId,
|
|
673
|
+
error: "Bounty is claimed but remote response missing acp_job_id",
|
|
674
|
+
});
|
|
675
|
+
}
|
|
676
|
+
continue;
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
const isNewPendingMatch =
|
|
680
|
+
status === "pending_match" &&
|
|
681
|
+
Array.isArray(remote.candidates) &&
|
|
682
|
+
remote.candidates.length > 0 &&
|
|
683
|
+
!b.notifiedPendingMatch;
|
|
684
|
+
|
|
685
|
+
saveActiveBounty({
|
|
686
|
+
...b,
|
|
687
|
+
status: remote.status,
|
|
688
|
+
// Mark as notified once we include it in pendingMatch output
|
|
689
|
+
...(isNewPendingMatch ? { notifiedPendingMatch: true } : {}),
|
|
690
|
+
});
|
|
691
|
+
|
|
692
|
+
if (isNewPendingMatch) {
|
|
693
|
+
const candidates = remote.candidates.map((c) =>
|
|
694
|
+
normalizeCandidateForWatch(c as Record<string, unknown>),
|
|
695
|
+
);
|
|
696
|
+
|
|
697
|
+
result.pendingMatch.push({
|
|
698
|
+
bountyId: b.bountyId,
|
|
699
|
+
title: b.title,
|
|
700
|
+
description: b.description,
|
|
701
|
+
budget: b.budget,
|
|
702
|
+
...(b.sourceChannel ? { sourceChannel: b.sourceChannel } : {}),
|
|
703
|
+
candidates,
|
|
704
|
+
});
|
|
705
|
+
}
|
|
706
|
+
} catch (e) {
|
|
707
|
+
result.errors.push({
|
|
708
|
+
bountyId: b.bountyId,
|
|
709
|
+
error: e instanceof Error ? e.message : String(e),
|
|
710
|
+
});
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
try {
|
|
715
|
+
removeBountyPollCronIfUnused();
|
|
716
|
+
} catch {
|
|
717
|
+
// non-fatal
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
output.output(result, (r) => {
|
|
721
|
+
output.heading("Bounty Poll");
|
|
722
|
+
output.field("Checked", r.checked);
|
|
723
|
+
output.field("Pending Match", r.pendingMatch.length);
|
|
724
|
+
output.field("Rejected by Provider", r.rejectedByProvider.length);
|
|
725
|
+
output.field("Claimed Jobs", r.claimedJobs.length);
|
|
726
|
+
output.field("Cleaned", r.cleaned.length);
|
|
727
|
+
output.field("Errors", r.errors.length);
|
|
728
|
+
if (r.pendingMatch.length > 0) {
|
|
729
|
+
output.log("\n Pending Match (candidates ready):");
|
|
730
|
+
for (const p of r.pendingMatch) {
|
|
731
|
+
output.log(
|
|
732
|
+
` - Bounty ${p.bountyId}: "${p.title}" — ${p.candidates.length} candidate(s)`,
|
|
733
|
+
);
|
|
734
|
+
for (const c of p.candidates) {
|
|
735
|
+
const price =
|
|
736
|
+
c.priceType === "fixed"
|
|
737
|
+
? `${c.price} USDC`
|
|
738
|
+
: c.price != null
|
|
739
|
+
? String(c.price)
|
|
740
|
+
: "N/A";
|
|
741
|
+
output.log(` ${c.agentName} — ${c.offeringName} (${price})`);
|
|
742
|
+
}
|
|
743
|
+
output.log(` -> run: acp bounty select ${p.bountyId}`);
|
|
744
|
+
}
|
|
745
|
+
}
|
|
746
|
+
if (r.rejectedByProvider.length > 0) {
|
|
747
|
+
output.log(
|
|
748
|
+
"\n Rejected by Provider (bounty reopened for new candidates):",
|
|
749
|
+
);
|
|
750
|
+
for (const rj of r.rejectedByProvider) {
|
|
751
|
+
const candidateCount = rj.candidates.length;
|
|
752
|
+
output.log(
|
|
753
|
+
` - Bounty ${rj.bountyId}: "${rj.title}" — provider rejected the job`,
|
|
754
|
+
);
|
|
755
|
+
if (candidateCount > 0) {
|
|
756
|
+
output.log(` ${candidateCount} new candidate(s) available:`);
|
|
757
|
+
for (const c of rj.candidates) {
|
|
758
|
+
const price =
|
|
759
|
+
(c as any).priceType === "fixed"
|
|
760
|
+
? `${(c as any).price} USDC`
|
|
761
|
+
: (c as any).price != null
|
|
762
|
+
? String((c as any).price)
|
|
763
|
+
: "N/A";
|
|
764
|
+
output.log(
|
|
765
|
+
` ${(c as any).agentName} — ${(c as any).offeringName} (${price})`,
|
|
766
|
+
);
|
|
767
|
+
}
|
|
768
|
+
output.log(` -> run: acp bounty select ${rj.bountyId}`);
|
|
769
|
+
} else {
|
|
770
|
+
output.log(
|
|
771
|
+
` Bounty is back to open — waiting for new candidates.`,
|
|
772
|
+
);
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
}
|
|
776
|
+
if (r.claimedJobs.length > 0) {
|
|
777
|
+
output.log("\n Claimed Jobs (in progress):");
|
|
778
|
+
for (const j of r.claimedJobs) {
|
|
779
|
+
output.log(
|
|
780
|
+
` - Bounty ${j.bountyId}: "${j.title}" — Job ${j.acpJobId} phase: ${j.jobPhase}`,
|
|
781
|
+
);
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
if (r.cleaned.length > 0) {
|
|
785
|
+
output.log("\n Cleaned (terminal):");
|
|
786
|
+
for (const c of r.cleaned) {
|
|
787
|
+
output.log(` - Bounty ${c.bountyId}: "${c.title}" — ${c.status}`);
|
|
788
|
+
}
|
|
789
|
+
}
|
|
790
|
+
if (r.errors.length > 0) {
|
|
791
|
+
output.log("\n Errors:");
|
|
792
|
+
for (const err of r.errors) {
|
|
793
|
+
output.log(` - ${err.bountyId}: ${err.error}`);
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
output.log("");
|
|
797
|
+
});
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
export async function status(
|
|
801
|
+
bountyId: string,
|
|
802
|
+
flags?: { sync?: boolean },
|
|
803
|
+
): Promise<void> {
|
|
804
|
+
if (!bountyId) output.fatal("Usage: acp bounty status <bountyId> [--sync]");
|
|
805
|
+
|
|
806
|
+
let bounty = getActiveBounty(bountyId);
|
|
807
|
+
|
|
808
|
+
if (flags?.sync && bounty) {
|
|
809
|
+
if (!bounty.posterSecret)
|
|
810
|
+
output.fatal("Cannot sync: missing poster secret for this bounty.");
|
|
811
|
+
try {
|
|
812
|
+
await syncBountyJobStatus({
|
|
813
|
+
bountyId,
|
|
814
|
+
posterSecret: bounty.posterSecret,
|
|
815
|
+
});
|
|
816
|
+
} catch (e: any) {
|
|
817
|
+
const msg =
|
|
818
|
+
e?.response?.data?.detail?.detail ??
|
|
819
|
+
e?.response?.data?.detail ??
|
|
820
|
+
(e instanceof Error ? e.message : String(e));
|
|
821
|
+
output.warn(`Failed to sync job status: ${msg}`);
|
|
822
|
+
}
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
let remote: Record<string, unknown> | null = null;
|
|
826
|
+
try {
|
|
827
|
+
remote = await getBountyDetails(bountyId);
|
|
828
|
+
} catch (e: any) {
|
|
829
|
+
const msg =
|
|
830
|
+
e?.response?.data?.detail?.detail ??
|
|
831
|
+
e?.response?.data?.detail ??
|
|
832
|
+
(e instanceof Error ? e.message : String(e));
|
|
833
|
+
if (!bounty) output.fatal(`Bounty not found: ${msg}`);
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
if (!bounty && remote) {
|
|
837
|
+
bounty = {
|
|
838
|
+
bountyId: String(remote.id ?? bountyId),
|
|
839
|
+
createdAt: String(remote.created_at ?? ""),
|
|
840
|
+
status: String(remote.status ?? ""),
|
|
841
|
+
title: String(remote.title ?? ""),
|
|
842
|
+
description: String(remote.description ?? ""),
|
|
843
|
+
budget: Number(remote.budget ?? 0),
|
|
844
|
+
category: String(remote.category ?? "digital"),
|
|
845
|
+
tags: String(remote.tags ?? ""),
|
|
846
|
+
posterName: String(remote.poster_name ?? ""),
|
|
847
|
+
posterSecret: "",
|
|
848
|
+
};
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
if (!bounty) output.fatal(`Bounty ${bountyId} not found.`);
|
|
852
|
+
|
|
853
|
+
const status = String(remote?.status ?? bounty.status).toLowerCase();
|
|
854
|
+
const claimedBy = String(remote?.claimed_by ?? "");
|
|
855
|
+
const acpJobId = String(remote?.acp_job_id ?? bounty.acpJobId ?? "");
|
|
856
|
+
const matchedAgent = String(remote?.matched_acp_agent ?? "");
|
|
857
|
+
const candidates =
|
|
858
|
+
status === "pending_match" && remote?.matched_acp_agent_profile
|
|
859
|
+
? [remote.matched_acp_agent_profile]
|
|
860
|
+
: [];
|
|
861
|
+
|
|
862
|
+
output.output(
|
|
863
|
+
{
|
|
864
|
+
bountyId: bounty.bountyId,
|
|
865
|
+
status,
|
|
866
|
+
title: bounty.title,
|
|
867
|
+
description: bounty.description,
|
|
868
|
+
budget: bounty.budget,
|
|
869
|
+
category: bounty.category,
|
|
870
|
+
tags: bounty.tags,
|
|
871
|
+
...(acpJobId ? { acpJobId } : {}),
|
|
872
|
+
...(claimedBy ? { claimedBy } : {}),
|
|
873
|
+
...(matchedAgent ? { matchedAgent } : {}),
|
|
874
|
+
...(status === "pending_match" && candidates.length > 0
|
|
875
|
+
? { candidates }
|
|
876
|
+
: {}),
|
|
877
|
+
...(bounty.sourceChannel ? { sourceChannel: bounty.sourceChannel } : {}),
|
|
878
|
+
createdAt: bounty.createdAt,
|
|
879
|
+
},
|
|
880
|
+
(data) => {
|
|
881
|
+
output.heading(`Bounty ${data.bountyId}`);
|
|
882
|
+
output.field("Status", data.status);
|
|
883
|
+
output.field("Title", data.title);
|
|
884
|
+
output.field("Description", data.description);
|
|
885
|
+
output.field("Budget", data.budget);
|
|
886
|
+
output.field("Category", data.category);
|
|
887
|
+
output.field("Tags", data.tags);
|
|
888
|
+
if (data.acpJobId) output.field("ACP Job ID", data.acpJobId);
|
|
889
|
+
if (data.claimedBy) output.field("Claimed By", data.claimedBy);
|
|
890
|
+
if (data.matchedAgent)
|
|
891
|
+
output.field("Claimed By Wallet Address", data.matchedAgent);
|
|
892
|
+
if (data.candidates) output.field("Candidates", data.candidates.length);
|
|
893
|
+
if (data.sourceChannel)
|
|
894
|
+
output.field("Source Channel", data.sourceChannel);
|
|
895
|
+
output.field("Created", data.createdAt);
|
|
896
|
+
output.log("");
|
|
897
|
+
},
|
|
898
|
+
);
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
export async function select(bountyId: string): Promise<void> {
|
|
902
|
+
if (!bountyId) output.fatal("Usage: acp bounty select <bountyId>");
|
|
903
|
+
const active = getActiveBounty(bountyId);
|
|
904
|
+
if (!active) output.fatal(`Bounty not found in local state: ${bountyId}`);
|
|
905
|
+
const posterSecret = active.posterSecret;
|
|
906
|
+
if (!posterSecret) {
|
|
907
|
+
output.fatal("Missing poster secret for this bounty.");
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
const match = await getMatchStatus(bountyId);
|
|
911
|
+
if (String(match.status).toLowerCase() !== "pending_match") {
|
|
912
|
+
output.fatal(
|
|
913
|
+
`Bounty is not pending_match. Current status: ${match.status}`,
|
|
914
|
+
);
|
|
915
|
+
}
|
|
916
|
+
if (!Array.isArray(match.candidates) || match.candidates.length === 0) {
|
|
917
|
+
output.fatal("No candidates available for this bounty.");
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
if (output.isJsonMode()) {
|
|
921
|
+
output.output(
|
|
922
|
+
{ bountyId, status: match.status, candidates: match.candidates },
|
|
923
|
+
() => {},
|
|
924
|
+
);
|
|
925
|
+
return;
|
|
926
|
+
}
|
|
927
|
+
|
|
928
|
+
const rl = readline.createInterface({
|
|
929
|
+
input: process.stdin,
|
|
930
|
+
output: process.stdout,
|
|
931
|
+
});
|
|
932
|
+
|
|
933
|
+
try {
|
|
934
|
+
output.heading(`Select Candidate for Bounty ${bountyId}`);
|
|
935
|
+
for (let i = 0; i < match.candidates.length; i++) {
|
|
936
|
+
const c = match.candidates[i] as Record<string, unknown>;
|
|
937
|
+
const candidateId = parseCandidateId(c.id) ?? -1;
|
|
938
|
+
output.log(
|
|
939
|
+
` [${i + 1}] candidateId=${candidateId} ${JSON.stringify(c)}`,
|
|
940
|
+
);
|
|
941
|
+
}
|
|
942
|
+
output.log(" [0] None of these candidates");
|
|
943
|
+
|
|
944
|
+
const choiceRaw = (
|
|
945
|
+
await question(rl, " Choose candidate number: ")
|
|
946
|
+
).trim();
|
|
947
|
+
if (choiceRaw === "0") {
|
|
948
|
+
await rejectCandidates({
|
|
949
|
+
bountyId,
|
|
950
|
+
posterSecret,
|
|
951
|
+
});
|
|
952
|
+
saveActiveBounty({
|
|
953
|
+
...active,
|
|
954
|
+
status: "open",
|
|
955
|
+
selectedCandidateId: undefined,
|
|
956
|
+
acpJobId: undefined,
|
|
957
|
+
notifiedPendingMatch: false,
|
|
958
|
+
});
|
|
959
|
+
output.log(
|
|
960
|
+
" Rejected current candidates. Bounty moved back to open for new matching.\n",
|
|
961
|
+
);
|
|
962
|
+
return;
|
|
963
|
+
}
|
|
964
|
+
const idx = parseInt(choiceRaw, 10) - 1;
|
|
965
|
+
if (!Number.isInteger(idx) || idx < 0 || idx >= match.candidates.length) {
|
|
966
|
+
output.fatal("Invalid candidate selection.");
|
|
967
|
+
}
|
|
968
|
+
const selected = match.candidates[idx] as Record<string, unknown>;
|
|
969
|
+
const candidateId = parseCandidateId(selected.id);
|
|
970
|
+
if (candidateId == null) output.fatal("Selected candidate has invalid id.");
|
|
971
|
+
|
|
972
|
+
const walletDefault = candidateField(selected, [
|
|
973
|
+
"agent_wallet",
|
|
974
|
+
"agentWallet",
|
|
975
|
+
"agent_wallet_address",
|
|
976
|
+
"agentWalletAddress",
|
|
977
|
+
"walletAddress",
|
|
978
|
+
"providerWalletAddress",
|
|
979
|
+
"provider_address",
|
|
980
|
+
]);
|
|
981
|
+
const offeringDefault = candidateField(selected, [
|
|
982
|
+
"job_offering",
|
|
983
|
+
"jobOffering",
|
|
984
|
+
"offeringName",
|
|
985
|
+
"jobOfferingName",
|
|
986
|
+
"offering_name",
|
|
987
|
+
"name",
|
|
988
|
+
]);
|
|
989
|
+
|
|
990
|
+
const wallet = walletDefault || "";
|
|
991
|
+
const offering = offeringDefault || "";
|
|
992
|
+
|
|
993
|
+
if (!wallet) {
|
|
994
|
+
output.fatal(
|
|
995
|
+
"Selected candidate is missing provider wallet (expected agent_wallet or walletAddress fields).",
|
|
996
|
+
);
|
|
997
|
+
}
|
|
998
|
+
if (!offering) {
|
|
999
|
+
output.fatal(
|
|
1000
|
+
"Selected candidate is missing job offering (expected job_offering/offeringName fields).",
|
|
1001
|
+
);
|
|
1002
|
+
}
|
|
1003
|
+
|
|
1004
|
+
const providerName =
|
|
1005
|
+
candidateField(selected, ["agent_name", "agentName", "name"]) ||
|
|
1006
|
+
"(unknown)";
|
|
1007
|
+
const offeringPrice = candidatePriceDisplay(selected);
|
|
1008
|
+
|
|
1009
|
+
output.log("\n Selected Candidate");
|
|
1010
|
+
output.log(" ------------------");
|
|
1011
|
+
output.log(` Provider: ${providerName}`);
|
|
1012
|
+
output.log(` Wallet: ${wallet}`);
|
|
1013
|
+
output.log(` Offering: ${offering}`);
|
|
1014
|
+
output.log(` Price: ${offeringPrice}`);
|
|
1015
|
+
const confirm = (
|
|
1016
|
+
await question(
|
|
1017
|
+
rl,
|
|
1018
|
+
"\n Continue and create ACP job for this candidate? (Y/n): ",
|
|
1019
|
+
)
|
|
1020
|
+
)
|
|
1021
|
+
.trim()
|
|
1022
|
+
.toLowerCase();
|
|
1023
|
+
if (!(confirm === "y" || confirm === "yes" || confirm === "")) {
|
|
1024
|
+
output.log(" Candidate selection cancelled.\n");
|
|
1025
|
+
return;
|
|
1026
|
+
}
|
|
1027
|
+
|
|
1028
|
+
const schema = getCandidateRequirementSchema(selected);
|
|
1029
|
+
const serviceRequirements =
|
|
1030
|
+
schema != null ? await collectRequirementsFromSchema(rl, schema) : {};
|
|
1031
|
+
|
|
1032
|
+
const job = await client.post<{
|
|
1033
|
+
data?: { jobId?: number };
|
|
1034
|
+
jobId?: number;
|
|
1035
|
+
}>("/acp/jobs", {
|
|
1036
|
+
providerWalletAddress: wallet,
|
|
1037
|
+
jobOfferingName: offering,
|
|
1038
|
+
serviceRequirements,
|
|
1039
|
+
});
|
|
1040
|
+
const acpJobId = String(job.data?.data?.jobId ?? job.data?.jobId ?? "");
|
|
1041
|
+
if (!acpJobId)
|
|
1042
|
+
output.fatal("Failed to create ACP job for selected candidate.");
|
|
1043
|
+
|
|
1044
|
+
await confirmMatch({
|
|
1045
|
+
bountyId,
|
|
1046
|
+
posterSecret,
|
|
1047
|
+
candidateId,
|
|
1048
|
+
acpJobId,
|
|
1049
|
+
});
|
|
1050
|
+
|
|
1051
|
+
const next: ActiveBounty = {
|
|
1052
|
+
...active,
|
|
1053
|
+
status: "claimed",
|
|
1054
|
+
selectedCandidateId: candidateId,
|
|
1055
|
+
acpJobId,
|
|
1056
|
+
};
|
|
1057
|
+
saveActiveBounty(next);
|
|
1058
|
+
|
|
1059
|
+
output.output(
|
|
1060
|
+
{
|
|
1061
|
+
bountyId,
|
|
1062
|
+
candidateId,
|
|
1063
|
+
acpJobId,
|
|
1064
|
+
status: "claimed",
|
|
1065
|
+
},
|
|
1066
|
+
(data) => {
|
|
1067
|
+
output.heading("Bounty Claimed");
|
|
1068
|
+
output.field("Bounty ID", data.bountyId);
|
|
1069
|
+
output.field("Candidate ID", data.candidateId);
|
|
1070
|
+
output.field("ACP Job ID", data.acpJobId);
|
|
1071
|
+
output.field("Status", data.status);
|
|
1072
|
+
output.log(
|
|
1073
|
+
`\n Use \`acp job status <jobId>\` to monitor the ACP job.`,
|
|
1074
|
+
);
|
|
1075
|
+
output.log(
|
|
1076
|
+
` Then run \`acp bounty status ${data.bountyId} --sync\` to sync/update bounty status.\n`,
|
|
1077
|
+
);
|
|
1078
|
+
},
|
|
1079
|
+
);
|
|
1080
|
+
} finally {
|
|
1081
|
+
rl.close();
|
|
1082
|
+
}
|
|
1083
|
+
}
|
|
1084
|
+
|
|
1085
|
+
export interface BountyUpdateFlags {
|
|
1086
|
+
title?: string;
|
|
1087
|
+
description?: string;
|
|
1088
|
+
budget?: number;
|
|
1089
|
+
tags?: string;
|
|
1090
|
+
}
|
|
1091
|
+
|
|
1092
|
+
export async function update(
|
|
1093
|
+
bountyId: string,
|
|
1094
|
+
flags: BountyUpdateFlags,
|
|
1095
|
+
): Promise<void> {
|
|
1096
|
+
if (!bountyId) output.fatal("Usage: acp bounty update <bountyId> [flags]");
|
|
1097
|
+
|
|
1098
|
+
const active = getActiveBounty(bountyId);
|
|
1099
|
+
if (!active) output.fatal(`Bounty not found in local state: ${bountyId}`);
|
|
1100
|
+
|
|
1101
|
+
if (active.status !== "open") {
|
|
1102
|
+
output.fatal(
|
|
1103
|
+
`Bounty ${bountyId} cannot be updated — status is "${active.status}". Only "open" bounties can be updated.`,
|
|
1104
|
+
);
|
|
1105
|
+
}
|
|
1106
|
+
|
|
1107
|
+
if (!active.posterSecret) {
|
|
1108
|
+
output.fatal("Missing poster secret for this bounty.");
|
|
1109
|
+
}
|
|
1110
|
+
|
|
1111
|
+
const title = flags.title?.trim() || undefined;
|
|
1112
|
+
const description = flags.description?.trim() || undefined;
|
|
1113
|
+
const budget =
|
|
1114
|
+
flags.budget != null && Number.isFinite(flags.budget) && flags.budget > 0
|
|
1115
|
+
? flags.budget
|
|
1116
|
+
: undefined;
|
|
1117
|
+
const tags = flags.tags?.trim() || undefined;
|
|
1118
|
+
|
|
1119
|
+
if (!title && !description && budget == null && !tags) {
|
|
1120
|
+
output.fatal(
|
|
1121
|
+
"Nothing to update. Provide at least one of: --title, --description, --budget, --tags",
|
|
1122
|
+
);
|
|
1123
|
+
}
|
|
1124
|
+
|
|
1125
|
+
try {
|
|
1126
|
+
await updateBounty(bountyId, {
|
|
1127
|
+
poster_secret: active.posterSecret,
|
|
1128
|
+
...(title ? { title } : {}),
|
|
1129
|
+
...(description ? { description } : {}),
|
|
1130
|
+
...(budget != null ? { budget } : {}),
|
|
1131
|
+
...(tags ? { tags } : {}),
|
|
1132
|
+
});
|
|
1133
|
+
} catch (e: any) {
|
|
1134
|
+
const msg =
|
|
1135
|
+
e?.response?.data?.detail?.detail ??
|
|
1136
|
+
e?.response?.data?.detail ??
|
|
1137
|
+
(e instanceof Error ? e.message : String(e));
|
|
1138
|
+
output.fatal(`Failed to update bounty ${bountyId}: ${msg}`);
|
|
1139
|
+
}
|
|
1140
|
+
|
|
1141
|
+
// Update local state
|
|
1142
|
+
const updated: ActiveBounty = {
|
|
1143
|
+
...active,
|
|
1144
|
+
...(title ? { title } : {}),
|
|
1145
|
+
...(description ? { description } : {}),
|
|
1146
|
+
...(budget != null ? { budget } : {}),
|
|
1147
|
+
...(tags ? { tags } : {}),
|
|
1148
|
+
};
|
|
1149
|
+
saveActiveBounty(updated);
|
|
1150
|
+
|
|
1151
|
+
output.output(
|
|
1152
|
+
{
|
|
1153
|
+
bountyId,
|
|
1154
|
+
updated: {
|
|
1155
|
+
...(title ? { title } : {}),
|
|
1156
|
+
...(description ? { description } : {}),
|
|
1157
|
+
...(budget != null ? { budget } : {}),
|
|
1158
|
+
...(tags ? { tags } : {}),
|
|
1159
|
+
},
|
|
1160
|
+
},
|
|
1161
|
+
(data) => {
|
|
1162
|
+
output.heading("Bounty Updated");
|
|
1163
|
+
output.field("Bounty ID", data.bountyId);
|
|
1164
|
+
if (data.updated.title) output.field("Title", data.updated.title);
|
|
1165
|
+
if (data.updated.description)
|
|
1166
|
+
output.field("Description", data.updated.description);
|
|
1167
|
+
if (data.updated.budget != null)
|
|
1168
|
+
output.field("Budget", data.updated.budget);
|
|
1169
|
+
if (data.updated.tags) output.field("Tags", data.updated.tags);
|
|
1170
|
+
output.log("");
|
|
1171
|
+
},
|
|
1172
|
+
);
|
|
1173
|
+
}
|
|
1174
|
+
|
|
1175
|
+
export async function cleanup(bountyId: string): Promise<void> {
|
|
1176
|
+
if (!bountyId) output.fatal("Usage: acp bounty cleanup <bountyId>");
|
|
1177
|
+
const active = getActiveBounty(bountyId);
|
|
1178
|
+
if (!active) {
|
|
1179
|
+
output.log(` Bounty not found locally: ${bountyId}`);
|
|
1180
|
+
return;
|
|
1181
|
+
}
|
|
1182
|
+
removeActiveBounty(bountyId);
|
|
1183
|
+
try {
|
|
1184
|
+
removeBountyPollCronIfUnused();
|
|
1185
|
+
} catch {
|
|
1186
|
+
// non-fatal
|
|
1187
|
+
}
|
|
1188
|
+
output.log(` Cleaned up bounty ${bountyId}\n`);
|
|
1189
|
+
}
|