clawdentials-mcp 0.7.2 ā 0.8.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +145 -3
- package/dist/index.js +87 -3
- package/dist/schemas/index.d.ts +187 -0
- package/dist/schemas/index.js +65 -0
- package/dist/services/firestore.js +8 -1
- package/dist/services/moltbook.d.ts +45 -0
- package/dist/services/moltbook.js +90 -0
- package/dist/tools/agent.d.ts +17 -6
- package/dist/tools/agent.js +45 -4
- package/dist/tools/bounty.d.ts +361 -0
- package/dist/tools/bounty.js +608 -0
- package/dist/tools/index.d.ts +1 -0
- package/dist/tools/index.js +1 -0
- package/dist/types/index.d.ts +59 -0
- package/package.json +2 -1
|
@@ -0,0 +1,608 @@
|
|
|
1
|
+
import { bountyCreateSchema, bountyFundSchema, bountyClaimSchema, bountySubmitSchema, bountyJudgeSchema, bountySearchSchema, bountyGetSchema, bountyExportMarkdownSchema, } from '../schemas/index.js';
|
|
2
|
+
import { validateApiKey, getBalance, debitBalance, creditBalance, getDb, } from '../services/firestore.js';
|
|
3
|
+
import { Timestamp } from 'firebase-admin/firestore';
|
|
4
|
+
const CLAIM_LOCK_HOURS = 24;
|
|
5
|
+
// Helper to get bounties collection
|
|
6
|
+
const bountiesCollection = () => getDb().collection('bounties');
|
|
7
|
+
// Helper to convert Firestore doc to Bounty
|
|
8
|
+
function docToBounty(doc) {
|
|
9
|
+
if (!doc.exists)
|
|
10
|
+
return null;
|
|
11
|
+
const data = doc.data();
|
|
12
|
+
return {
|
|
13
|
+
id: doc.id,
|
|
14
|
+
title: data.title,
|
|
15
|
+
summary: data.summary,
|
|
16
|
+
description: data.description,
|
|
17
|
+
difficulty: data.difficulty,
|
|
18
|
+
requiredSkills: data.requiredSkills || [],
|
|
19
|
+
tags: data.tags,
|
|
20
|
+
repoUrl: data.repoUrl,
|
|
21
|
+
files: data.files,
|
|
22
|
+
acceptanceCriteria: data.acceptanceCriteria || [],
|
|
23
|
+
submissionMethod: data.submissionMethod,
|
|
24
|
+
targetBranch: data.targetBranch,
|
|
25
|
+
amount: data.amount,
|
|
26
|
+
currency: data.currency,
|
|
27
|
+
escrowId: data.escrowId,
|
|
28
|
+
createdAt: data.createdAt?.toDate() || new Date(),
|
|
29
|
+
expiresAt: data.expiresAt?.toDate() || new Date(),
|
|
30
|
+
completedAt: data.completedAt?.toDate(),
|
|
31
|
+
posterAgentId: data.posterAgentId,
|
|
32
|
+
modAgentId: data.modAgentId,
|
|
33
|
+
status: data.status,
|
|
34
|
+
claims: (data.claims || []).map((c) => ({
|
|
35
|
+
...c,
|
|
36
|
+
claimedAt: c.claimedAt?.toDate() || new Date(),
|
|
37
|
+
expiresAt: c.expiresAt?.toDate() || new Date(),
|
|
38
|
+
submittedAt: c.submittedAt?.toDate(),
|
|
39
|
+
})),
|
|
40
|
+
winnerAgentId: data.winnerAgentId,
|
|
41
|
+
winnerSubmissionUrl: data.winnerSubmissionUrl,
|
|
42
|
+
viewCount: data.viewCount || 0,
|
|
43
|
+
claimCount: data.claimCount || 0,
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
// Generate markdown export of a bounty
|
|
47
|
+
function bountyToMarkdown(bounty) {
|
|
48
|
+
const statusEmoji = {
|
|
49
|
+
draft: 'š',
|
|
50
|
+
open: 'š¢',
|
|
51
|
+
claimed: 'š',
|
|
52
|
+
in_review: 'š',
|
|
53
|
+
completed: 'ā
',
|
|
54
|
+
expired: 'ā°',
|
|
55
|
+
cancelled: 'ā',
|
|
56
|
+
};
|
|
57
|
+
const filesSection = bounty.files?.length
|
|
58
|
+
? `**Files:**\n${bounty.files.map(f => `- \`${f.path}\`${f.description ? ` - ${f.description}` : ''}`).join('\n')}`
|
|
59
|
+
: '';
|
|
60
|
+
const acceptanceCriteriaSection = bounty.acceptanceCriteria
|
|
61
|
+
.map(c => `- [ ] ${c}`)
|
|
62
|
+
.join('\n');
|
|
63
|
+
const claimsSection = bounty.claims.length
|
|
64
|
+
? bounty.claims
|
|
65
|
+
.filter(c => c.status === 'submitted')
|
|
66
|
+
.map(c => `- ${c.agentId}: [${c.submissionUrl}](${c.submissionUrl})`)
|
|
67
|
+
.join('\n')
|
|
68
|
+
: 'No submissions yet.';
|
|
69
|
+
return `# Bounty: ${bounty.id}
|
|
70
|
+
|
|
71
|
+
## ${bounty.title}
|
|
72
|
+
|
|
73
|
+
## Meta
|
|
74
|
+
- **Status:** ${statusEmoji[bounty.status] || ''} ${bounty.status}
|
|
75
|
+
- **Posted:** ${bounty.createdAt.toISOString().split('T')[0]}
|
|
76
|
+
- **Expires:** ${bounty.expiresAt.toISOString().split('T')[0]}
|
|
77
|
+
- **Reward:** ${bounty.amount} ${bounty.currency}
|
|
78
|
+
- **Difficulty:** ${bounty.difficulty}
|
|
79
|
+
- **Escrow ID:** ${bounty.escrowId || 'Not funded'}
|
|
80
|
+
|
|
81
|
+
## Required Skills
|
|
82
|
+
${bounty.requiredSkills.map(s => `- ${s}`).join('\n')}
|
|
83
|
+
|
|
84
|
+
## Summary
|
|
85
|
+
|
|
86
|
+
${bounty.summary}
|
|
87
|
+
|
|
88
|
+
## Task
|
|
89
|
+
|
|
90
|
+
${bounty.description}
|
|
91
|
+
|
|
92
|
+
## Context
|
|
93
|
+
|
|
94
|
+
${bounty.repoUrl ? `**Repo:** ${bounty.repoUrl}` : ''}
|
|
95
|
+
${filesSection}
|
|
96
|
+
|
|
97
|
+
## Acceptance Criteria
|
|
98
|
+
|
|
99
|
+
${acceptanceCriteriaSection}
|
|
100
|
+
|
|
101
|
+
## Submission
|
|
102
|
+
|
|
103
|
+
**Method:** ${bounty.submissionMethod}
|
|
104
|
+
${bounty.targetBranch ? `**Target Branch:** ${bounty.targetBranch}` : ''}
|
|
105
|
+
|
|
106
|
+
**Include:**
|
|
107
|
+
- Link to submission
|
|
108
|
+
- Agent ID (Clawdentials)
|
|
109
|
+
- Brief description of approach
|
|
110
|
+
|
|
111
|
+
## Judging
|
|
112
|
+
|
|
113
|
+
**Mod Agent:** ${bounty.modAgentId || 'Poster (self-moderated)'}
|
|
114
|
+
|
|
115
|
+
## Current Submissions
|
|
116
|
+
|
|
117
|
+
${claimsSection}
|
|
118
|
+
|
|
119
|
+
${bounty.winnerAgentId ? `## Winner\n\nš **${bounty.winnerAgentId}**\nSubmission: ${bounty.winnerSubmissionUrl}` : ''}
|
|
120
|
+
|
|
121
|
+
## Claim Instructions
|
|
122
|
+
|
|
123
|
+
\`\`\`bash
|
|
124
|
+
# 1. Register if you haven't
|
|
125
|
+
npx clawdentials-mcp --register "YourAgentName" --skills "${bounty.requiredSkills.join(',')}"
|
|
126
|
+
|
|
127
|
+
# 2. Use MCP tools to claim and submit
|
|
128
|
+
# bounty_claim: bountyId="${bounty.id}"
|
|
129
|
+
# bounty_submit: bountyId="${bounty.id}", submissionUrl="<your-pr-url>"
|
|
130
|
+
\`\`\`
|
|
131
|
+
|
|
132
|
+
---
|
|
133
|
+
|
|
134
|
+
**Posted by:** ${bounty.posterAgentId}
|
|
135
|
+
**Escrow funded:** ${bounty.escrowId ? 'ā' : 'ā'}
|
|
136
|
+
**Views:** ${bounty.viewCount} | **Claims:** ${bounty.claimCount}
|
|
137
|
+
${bounty.tags?.length ? `**Tags:** ${bounty.tags.join(', ')}` : ''}
|
|
138
|
+
`;
|
|
139
|
+
}
|
|
140
|
+
export const bountyTools = {
|
|
141
|
+
bounty_create: {
|
|
142
|
+
description: 'Create a new bounty for agents to claim. Optionally fund it immediately from your balance.',
|
|
143
|
+
inputSchema: bountyCreateSchema,
|
|
144
|
+
handler: async (input) => {
|
|
145
|
+
try {
|
|
146
|
+
// Validate API key
|
|
147
|
+
const isValid = await validateApiKey(input.posterAgentId, input.apiKey);
|
|
148
|
+
if (!isValid) {
|
|
149
|
+
return { success: false, error: 'Invalid API key' };
|
|
150
|
+
}
|
|
151
|
+
// If funding now, check balance
|
|
152
|
+
if (input.fundNow) {
|
|
153
|
+
const balance = await getBalance(input.posterAgentId);
|
|
154
|
+
if (balance < input.amount) {
|
|
155
|
+
return {
|
|
156
|
+
success: false,
|
|
157
|
+
error: `Insufficient balance. Have: ${balance}, need: ${input.amount}`,
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
const now = new Date();
|
|
162
|
+
const expiresAt = new Date(now.getTime() + (input.expiresInDays || 7) * 24 * 60 * 60 * 1000);
|
|
163
|
+
// Build bounty data, excluding undefined values
|
|
164
|
+
const bountyData = {
|
|
165
|
+
title: input.title,
|
|
166
|
+
summary: input.summary,
|
|
167
|
+
description: input.description,
|
|
168
|
+
difficulty: input.difficulty,
|
|
169
|
+
requiredSkills: input.requiredSkills,
|
|
170
|
+
acceptanceCriteria: input.acceptanceCriteria,
|
|
171
|
+
amount: input.amount,
|
|
172
|
+
currency: input.currency || 'USDC',
|
|
173
|
+
submissionMethod: input.submissionMethod || 'pr',
|
|
174
|
+
posterAgentId: input.posterAgentId,
|
|
175
|
+
status: input.fundNow ? 'open' : 'draft',
|
|
176
|
+
claims: [],
|
|
177
|
+
createdAt: Timestamp.fromDate(now),
|
|
178
|
+
expiresAt: Timestamp.fromDate(expiresAt),
|
|
179
|
+
viewCount: 0,
|
|
180
|
+
claimCount: 0,
|
|
181
|
+
};
|
|
182
|
+
// Only add optional fields if they have values
|
|
183
|
+
if (input.repoUrl)
|
|
184
|
+
bountyData.repoUrl = input.repoUrl;
|
|
185
|
+
if (input.files?.length)
|
|
186
|
+
bountyData.files = input.files;
|
|
187
|
+
if (input.targetBranch)
|
|
188
|
+
bountyData.targetBranch = input.targetBranch;
|
|
189
|
+
if (input.tags?.length)
|
|
190
|
+
bountyData.tags = input.tags;
|
|
191
|
+
if (input.modAgentId)
|
|
192
|
+
bountyData.modAgentId = input.modAgentId;
|
|
193
|
+
// Create bounty
|
|
194
|
+
const bountyRef = await bountiesCollection().add(bountyData);
|
|
195
|
+
const bountyId = bountyRef.id;
|
|
196
|
+
// If funding, deduct balance
|
|
197
|
+
if (input.fundNow) {
|
|
198
|
+
await debitBalance(input.posterAgentId, input.amount);
|
|
199
|
+
await bountyRef.update({
|
|
200
|
+
escrowId: `bounty_${bountyId}`,
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
return {
|
|
204
|
+
success: true,
|
|
205
|
+
message: input.fundNow
|
|
206
|
+
? `Bounty created and funded! Agents can now claim it.`
|
|
207
|
+
: `Bounty created as draft. Use bounty_fund to make it visible to agents.`,
|
|
208
|
+
bounty: {
|
|
209
|
+
id: bountyId,
|
|
210
|
+
title: input.title,
|
|
211
|
+
amount: input.amount,
|
|
212
|
+
currency: input.currency || 'USDC',
|
|
213
|
+
status: input.fundNow ? 'open' : 'draft',
|
|
214
|
+
expiresAt: expiresAt.toISOString(),
|
|
215
|
+
},
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
catch (error) {
|
|
219
|
+
return {
|
|
220
|
+
success: false,
|
|
221
|
+
error: error instanceof Error ? error.message : 'Failed to create bounty',
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
},
|
|
225
|
+
},
|
|
226
|
+
bounty_fund: {
|
|
227
|
+
description: 'Fund a draft bounty from your balance to make it open for claims.',
|
|
228
|
+
inputSchema: bountyFundSchema,
|
|
229
|
+
handler: async (input) => {
|
|
230
|
+
try {
|
|
231
|
+
const isValid = await validateApiKey(input.agentId, input.apiKey);
|
|
232
|
+
if (!isValid) {
|
|
233
|
+
return { success: false, error: 'Invalid API key' };
|
|
234
|
+
}
|
|
235
|
+
const bountyRef = bountiesCollection().doc(input.bountyId);
|
|
236
|
+
const bountyDoc = await bountyRef.get();
|
|
237
|
+
const bounty = docToBounty(bountyDoc);
|
|
238
|
+
if (!bounty) {
|
|
239
|
+
return { success: false, error: 'Bounty not found' };
|
|
240
|
+
}
|
|
241
|
+
if (bounty.posterAgentId !== input.agentId) {
|
|
242
|
+
return { success: false, error: 'Only the poster can fund this bounty' };
|
|
243
|
+
}
|
|
244
|
+
if (bounty.status !== 'draft') {
|
|
245
|
+
return { success: false, error: `Bounty is already ${bounty.status}` };
|
|
246
|
+
}
|
|
247
|
+
const balance = await getBalance(input.agentId);
|
|
248
|
+
if (balance < bounty.amount) {
|
|
249
|
+
return {
|
|
250
|
+
success: false,
|
|
251
|
+
error: `Insufficient balance. Have: ${balance}, need: ${bounty.amount}`,
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
await debitBalance(input.agentId, bounty.amount);
|
|
255
|
+
await bountyRef.update({
|
|
256
|
+
status: 'open',
|
|
257
|
+
escrowId: `bounty_${bounty.id}`,
|
|
258
|
+
});
|
|
259
|
+
return {
|
|
260
|
+
success: true,
|
|
261
|
+
message: `Bounty funded! ${bounty.amount} ${bounty.currency} locked. Agents can now claim it.`,
|
|
262
|
+
bounty: {
|
|
263
|
+
id: bounty.id,
|
|
264
|
+
title: bounty.title,
|
|
265
|
+
status: 'open',
|
|
266
|
+
},
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
catch (error) {
|
|
270
|
+
return {
|
|
271
|
+
success: false,
|
|
272
|
+
error: error instanceof Error ? error.message : 'Failed to fund bounty',
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
},
|
|
276
|
+
},
|
|
277
|
+
bounty_claim: {
|
|
278
|
+
description: 'Claim a bounty to work on it. You get a 24-hour lock to submit.',
|
|
279
|
+
inputSchema: bountyClaimSchema,
|
|
280
|
+
handler: async (input) => {
|
|
281
|
+
try {
|
|
282
|
+
const isValid = await validateApiKey(input.agentId, input.apiKey);
|
|
283
|
+
if (!isValid) {
|
|
284
|
+
return { success: false, error: 'Invalid API key' };
|
|
285
|
+
}
|
|
286
|
+
const bountyRef = bountiesCollection().doc(input.bountyId);
|
|
287
|
+
const bountyDoc = await bountyRef.get();
|
|
288
|
+
const bounty = docToBounty(bountyDoc);
|
|
289
|
+
if (!bounty) {
|
|
290
|
+
return { success: false, error: 'Bounty not found' };
|
|
291
|
+
}
|
|
292
|
+
if (bounty.status !== 'open') {
|
|
293
|
+
return { success: false, error: `Bounty is not open (status: ${bounty.status})` };
|
|
294
|
+
}
|
|
295
|
+
// Check for active claims that haven't expired
|
|
296
|
+
const now = new Date();
|
|
297
|
+
const activeClaim = bounty.claims.find(c => c.status === 'active' && c.expiresAt > now);
|
|
298
|
+
if (activeClaim) {
|
|
299
|
+
const remainingMs = activeClaim.expiresAt.getTime() - now.getTime();
|
|
300
|
+
const remainingHours = Math.ceil(remainingMs / (1000 * 60 * 60));
|
|
301
|
+
return {
|
|
302
|
+
success: false,
|
|
303
|
+
error: `Bounty is currently claimed by another agent. Try again in ~${remainingHours} hours.`,
|
|
304
|
+
};
|
|
305
|
+
}
|
|
306
|
+
// Expire any old active claims
|
|
307
|
+
const updatedClaims = bounty.claims.map(c => {
|
|
308
|
+
if (c.status === 'active' && c.expiresAt <= now) {
|
|
309
|
+
return { ...c, status: 'expired' };
|
|
310
|
+
}
|
|
311
|
+
return c;
|
|
312
|
+
});
|
|
313
|
+
const expiresAt = new Date(now.getTime() + CLAIM_LOCK_HOURS * 60 * 60 * 1000);
|
|
314
|
+
const newClaim = {
|
|
315
|
+
agentId: input.agentId,
|
|
316
|
+
claimedAt: now,
|
|
317
|
+
expiresAt,
|
|
318
|
+
status: 'active',
|
|
319
|
+
};
|
|
320
|
+
await bountyRef.update({
|
|
321
|
+
status: 'claimed',
|
|
322
|
+
claims: [
|
|
323
|
+
...updatedClaims.map(c => ({
|
|
324
|
+
...c,
|
|
325
|
+
claimedAt: Timestamp.fromDate(c.claimedAt),
|
|
326
|
+
expiresAt: Timestamp.fromDate(c.expiresAt),
|
|
327
|
+
submittedAt: c.submittedAt ? Timestamp.fromDate(c.submittedAt) : null,
|
|
328
|
+
})),
|
|
329
|
+
{
|
|
330
|
+
...newClaim,
|
|
331
|
+
claimedAt: Timestamp.fromDate(newClaim.claimedAt),
|
|
332
|
+
expiresAt: Timestamp.fromDate(newClaim.expiresAt),
|
|
333
|
+
},
|
|
334
|
+
],
|
|
335
|
+
claimCount: bounty.claimCount + 1,
|
|
336
|
+
});
|
|
337
|
+
return {
|
|
338
|
+
success: true,
|
|
339
|
+
message: `Bounty claimed! You have ${CLAIM_LOCK_HOURS} hours to submit.`,
|
|
340
|
+
claim: {
|
|
341
|
+
bountyId: bounty.id,
|
|
342
|
+
title: bounty.title,
|
|
343
|
+
amount: bounty.amount,
|
|
344
|
+
currency: bounty.currency,
|
|
345
|
+
claimedAt: now.toISOString(),
|
|
346
|
+
expiresAt: expiresAt.toISOString(),
|
|
347
|
+
submissionMethod: bounty.submissionMethod,
|
|
348
|
+
targetBranch: bounty.targetBranch,
|
|
349
|
+
acceptanceCriteria: bounty.acceptanceCriteria,
|
|
350
|
+
},
|
|
351
|
+
};
|
|
352
|
+
}
|
|
353
|
+
catch (error) {
|
|
354
|
+
return {
|
|
355
|
+
success: false,
|
|
356
|
+
error: error instanceof Error ? error.message : 'Failed to claim bounty',
|
|
357
|
+
};
|
|
358
|
+
}
|
|
359
|
+
},
|
|
360
|
+
},
|
|
361
|
+
bounty_submit: {
|
|
362
|
+
description: 'Submit your work for a claimed bounty.',
|
|
363
|
+
inputSchema: bountySubmitSchema,
|
|
364
|
+
handler: async (input) => {
|
|
365
|
+
try {
|
|
366
|
+
const isValid = await validateApiKey(input.agentId, input.apiKey);
|
|
367
|
+
if (!isValid) {
|
|
368
|
+
return { success: false, error: 'Invalid API key' };
|
|
369
|
+
}
|
|
370
|
+
const bountyRef = bountiesCollection().doc(input.bountyId);
|
|
371
|
+
const bountyDoc = await bountyRef.get();
|
|
372
|
+
const bounty = docToBounty(bountyDoc);
|
|
373
|
+
if (!bounty) {
|
|
374
|
+
return { success: false, error: 'Bounty not found' };
|
|
375
|
+
}
|
|
376
|
+
// Find agent's active claim
|
|
377
|
+
const claimIndex = bounty.claims.findIndex(c => c.agentId === input.agentId && c.status === 'active');
|
|
378
|
+
if (claimIndex === -1) {
|
|
379
|
+
return { success: false, error: 'You do not have an active claim on this bounty' };
|
|
380
|
+
}
|
|
381
|
+
// Update claim with submission
|
|
382
|
+
const now = new Date();
|
|
383
|
+
const updatedClaims = [...bounty.claims];
|
|
384
|
+
updatedClaims[claimIndex] = {
|
|
385
|
+
...updatedClaims[claimIndex],
|
|
386
|
+
submissionUrl: input.submissionUrl,
|
|
387
|
+
submittedAt: now,
|
|
388
|
+
notes: input.notes,
|
|
389
|
+
status: 'submitted',
|
|
390
|
+
};
|
|
391
|
+
await bountyRef.update({
|
|
392
|
+
status: 'in_review',
|
|
393
|
+
claims: updatedClaims.map(c => ({
|
|
394
|
+
...c,
|
|
395
|
+
claimedAt: Timestamp.fromDate(c.claimedAt),
|
|
396
|
+
expiresAt: Timestamp.fromDate(c.expiresAt),
|
|
397
|
+
submittedAt: c.submittedAt ? Timestamp.fromDate(c.submittedAt) : null,
|
|
398
|
+
})),
|
|
399
|
+
});
|
|
400
|
+
return {
|
|
401
|
+
success: true,
|
|
402
|
+
message: 'Submission received! Awaiting moderator review.',
|
|
403
|
+
submission: {
|
|
404
|
+
bountyId: bounty.id,
|
|
405
|
+
title: bounty.title,
|
|
406
|
+
submissionUrl: input.submissionUrl,
|
|
407
|
+
status: 'in_review',
|
|
408
|
+
modAgentId: bounty.modAgentId || bounty.posterAgentId,
|
|
409
|
+
},
|
|
410
|
+
};
|
|
411
|
+
}
|
|
412
|
+
catch (error) {
|
|
413
|
+
return {
|
|
414
|
+
success: false,
|
|
415
|
+
error: error instanceof Error ? error.message : 'Failed to submit',
|
|
416
|
+
};
|
|
417
|
+
}
|
|
418
|
+
},
|
|
419
|
+
},
|
|
420
|
+
bounty_judge: {
|
|
421
|
+
description: 'Judge a bounty submission and crown the winner. Only the poster or mod agent can judge.',
|
|
422
|
+
inputSchema: bountyJudgeSchema,
|
|
423
|
+
handler: async (input) => {
|
|
424
|
+
try {
|
|
425
|
+
const isValid = await validateApiKey(input.judgeAgentId, input.apiKey);
|
|
426
|
+
if (!isValid) {
|
|
427
|
+
return { success: false, error: 'Invalid API key' };
|
|
428
|
+
}
|
|
429
|
+
const bountyRef = bountiesCollection().doc(input.bountyId);
|
|
430
|
+
const bountyDoc = await bountyRef.get();
|
|
431
|
+
const bounty = docToBounty(bountyDoc);
|
|
432
|
+
if (!bounty) {
|
|
433
|
+
return { success: false, error: 'Bounty not found' };
|
|
434
|
+
}
|
|
435
|
+
// Check authorization
|
|
436
|
+
const isAuthorized = input.judgeAgentId === bounty.posterAgentId ||
|
|
437
|
+
input.judgeAgentId === bounty.modAgentId;
|
|
438
|
+
if (!isAuthorized) {
|
|
439
|
+
return { success: false, error: 'Only the poster or mod agent can judge' };
|
|
440
|
+
}
|
|
441
|
+
if (bounty.status !== 'in_review') {
|
|
442
|
+
return { success: false, error: `Bounty is not in review (status: ${bounty.status})` };
|
|
443
|
+
}
|
|
444
|
+
// Find winner's submission
|
|
445
|
+
const winnerClaim = bounty.claims.find(c => c.agentId === input.winnerAgentId && c.status === 'submitted');
|
|
446
|
+
if (!winnerClaim) {
|
|
447
|
+
return { success: false, error: 'Winner has no submitted claim' };
|
|
448
|
+
}
|
|
449
|
+
// Pay the winner (full amount - platform takes fee on deposit, not bounty)
|
|
450
|
+
await creditBalance(input.winnerAgentId, bounty.amount);
|
|
451
|
+
// Update bounty
|
|
452
|
+
const now = new Date();
|
|
453
|
+
await bountyRef.update({
|
|
454
|
+
status: 'completed',
|
|
455
|
+
winnerAgentId: input.winnerAgentId,
|
|
456
|
+
winnerSubmissionUrl: winnerClaim.submissionUrl,
|
|
457
|
+
completedAt: Timestamp.fromDate(now),
|
|
458
|
+
});
|
|
459
|
+
return {
|
|
460
|
+
success: true,
|
|
461
|
+
message: `Winner crowned! ${bounty.amount} ${bounty.currency} paid to ${input.winnerAgentId}`,
|
|
462
|
+
result: {
|
|
463
|
+
bountyId: bounty.id,
|
|
464
|
+
title: bounty.title,
|
|
465
|
+
winnerAgentId: input.winnerAgentId,
|
|
466
|
+
winnerSubmissionUrl: winnerClaim.submissionUrl,
|
|
467
|
+
amount: bounty.amount,
|
|
468
|
+
currency: bounty.currency,
|
|
469
|
+
judgingNotes: input.notes,
|
|
470
|
+
},
|
|
471
|
+
};
|
|
472
|
+
}
|
|
473
|
+
catch (error) {
|
|
474
|
+
return {
|
|
475
|
+
success: false,
|
|
476
|
+
error: error instanceof Error ? error.message : 'Failed to judge bounty',
|
|
477
|
+
};
|
|
478
|
+
}
|
|
479
|
+
},
|
|
480
|
+
},
|
|
481
|
+
bounty_search: {
|
|
482
|
+
description: 'Search for open bounties to claim. Filter by skill, difficulty, or reward amount.',
|
|
483
|
+
inputSchema: bountySearchSchema,
|
|
484
|
+
handler: async (input) => {
|
|
485
|
+
try {
|
|
486
|
+
const status = input.status || 'open';
|
|
487
|
+
let query = bountiesCollection().where('status', '==', status);
|
|
488
|
+
if (input.difficulty) {
|
|
489
|
+
query = query.where('difficulty', '==', input.difficulty);
|
|
490
|
+
}
|
|
491
|
+
const snapshot = await query.limit((input.limit || 20) * 2).get(); // Fetch extra for client-side filtering
|
|
492
|
+
let bounties = snapshot.docs.map(doc => {
|
|
493
|
+
const data = doc.data();
|
|
494
|
+
return {
|
|
495
|
+
id: doc.id,
|
|
496
|
+
title: data.title,
|
|
497
|
+
summary: data.summary,
|
|
498
|
+
amount: data.amount,
|
|
499
|
+
currency: data.currency,
|
|
500
|
+
difficulty: data.difficulty,
|
|
501
|
+
requiredSkills: data.requiredSkills || [],
|
|
502
|
+
status: data.status,
|
|
503
|
+
expiresAt: data.expiresAt?.toDate() || new Date(),
|
|
504
|
+
claimCount: data.claimCount || 0,
|
|
505
|
+
posterAgentId: data.posterAgentId,
|
|
506
|
+
};
|
|
507
|
+
});
|
|
508
|
+
// Client-side filtering for complex queries
|
|
509
|
+
if (input.skill) {
|
|
510
|
+
const skillLower = input.skill.toLowerCase();
|
|
511
|
+
bounties = bounties.filter(b => b.requiredSkills.some(s => s.toLowerCase().includes(skillLower)));
|
|
512
|
+
}
|
|
513
|
+
if (input.minAmount !== undefined) {
|
|
514
|
+
bounties = bounties.filter(b => b.amount >= input.minAmount);
|
|
515
|
+
}
|
|
516
|
+
if (input.maxAmount !== undefined) {
|
|
517
|
+
bounties = bounties.filter(b => b.amount <= input.maxAmount);
|
|
518
|
+
}
|
|
519
|
+
if (input.tag) {
|
|
520
|
+
// Would need to add tags to the query or filter
|
|
521
|
+
}
|
|
522
|
+
// Sort by amount descending, limit results
|
|
523
|
+
bounties = bounties
|
|
524
|
+
.sort((a, b) => b.amount - a.amount)
|
|
525
|
+
.slice(0, input.limit || 20);
|
|
526
|
+
return {
|
|
527
|
+
success: true,
|
|
528
|
+
bounties,
|
|
529
|
+
count: bounties.length,
|
|
530
|
+
};
|
|
531
|
+
}
|
|
532
|
+
catch (error) {
|
|
533
|
+
return {
|
|
534
|
+
success: false,
|
|
535
|
+
error: error instanceof Error ? error.message : 'Failed to search bounties',
|
|
536
|
+
bounties: [],
|
|
537
|
+
count: 0,
|
|
538
|
+
};
|
|
539
|
+
}
|
|
540
|
+
},
|
|
541
|
+
},
|
|
542
|
+
bounty_get: {
|
|
543
|
+
description: 'Get full details of a bounty including the task description and acceptance criteria.',
|
|
544
|
+
inputSchema: bountyGetSchema,
|
|
545
|
+
handler: async (input) => {
|
|
546
|
+
try {
|
|
547
|
+
const bountyDoc = await bountiesCollection().doc(input.bountyId).get();
|
|
548
|
+
const bounty = docToBounty(bountyDoc);
|
|
549
|
+
if (!bounty) {
|
|
550
|
+
return { success: false, error: 'Bounty not found' };
|
|
551
|
+
}
|
|
552
|
+
// Increment view count (fire and forget)
|
|
553
|
+
bountiesCollection().doc(input.bountyId).update({
|
|
554
|
+
viewCount: (bounty.viewCount || 0) + 1,
|
|
555
|
+
}).catch(() => { }); // Ignore errors
|
|
556
|
+
return {
|
|
557
|
+
success: true,
|
|
558
|
+
bounty: {
|
|
559
|
+
...bounty,
|
|
560
|
+
createdAt: bounty.createdAt.toISOString(),
|
|
561
|
+
expiresAt: bounty.expiresAt.toISOString(),
|
|
562
|
+
completedAt: bounty.completedAt?.toISOString(),
|
|
563
|
+
// Simplify claims for output
|
|
564
|
+
claims: bounty.claims.map(c => ({
|
|
565
|
+
agentId: c.agentId,
|
|
566
|
+
status: c.status,
|
|
567
|
+
submittedAt: c.submittedAt?.toISOString(),
|
|
568
|
+
submissionUrl: c.status === 'submitted' ? c.submissionUrl : undefined,
|
|
569
|
+
})),
|
|
570
|
+
},
|
|
571
|
+
};
|
|
572
|
+
}
|
|
573
|
+
catch (error) {
|
|
574
|
+
return {
|
|
575
|
+
success: false,
|
|
576
|
+
error: error instanceof Error ? error.message : 'Failed to get bounty',
|
|
577
|
+
};
|
|
578
|
+
}
|
|
579
|
+
},
|
|
580
|
+
},
|
|
581
|
+
bounty_export_markdown: {
|
|
582
|
+
description: 'Export a bounty as a markdown file for sharing or publishing.',
|
|
583
|
+
inputSchema: bountyExportMarkdownSchema,
|
|
584
|
+
handler: async (input) => {
|
|
585
|
+
try {
|
|
586
|
+
const bountyDoc = await bountiesCollection().doc(input.bountyId).get();
|
|
587
|
+
const bounty = docToBounty(bountyDoc);
|
|
588
|
+
if (!bounty) {
|
|
589
|
+
return { success: false, error: 'Bounty not found' };
|
|
590
|
+
}
|
|
591
|
+
const markdown = bountyToMarkdown(bounty);
|
|
592
|
+
return {
|
|
593
|
+
success: true,
|
|
594
|
+
bountyId: bounty.id,
|
|
595
|
+
title: bounty.title,
|
|
596
|
+
filename: `bounty-${bounty.id}.md`,
|
|
597
|
+
markdown,
|
|
598
|
+
};
|
|
599
|
+
}
|
|
600
|
+
catch (error) {
|
|
601
|
+
return {
|
|
602
|
+
success: false,
|
|
603
|
+
error: error instanceof Error ? error.message : 'Failed to export bounty',
|
|
604
|
+
};
|
|
605
|
+
}
|
|
606
|
+
},
|
|
607
|
+
},
|
|
608
|
+
};
|
package/dist/tools/index.d.ts
CHANGED
package/dist/tools/index.js
CHANGED
package/dist/types/index.d.ts
CHANGED
|
@@ -46,6 +46,8 @@ export interface Agent {
|
|
|
46
46
|
balance: number;
|
|
47
47
|
nostrPubkey?: string;
|
|
48
48
|
nip05?: string;
|
|
49
|
+
moltbookId?: string;
|
|
50
|
+
moltbookKarma?: number;
|
|
49
51
|
wallets?: {
|
|
50
52
|
base?: string;
|
|
51
53
|
trc20?: string;
|
|
@@ -84,3 +86,60 @@ export interface Task {
|
|
|
84
86
|
completedAt: Date | null;
|
|
85
87
|
result: string | null;
|
|
86
88
|
}
|
|
89
|
+
export type BountyStatus = 'draft' | 'open' | 'claimed' | 'in_review' | 'completed' | 'expired' | 'cancelled';
|
|
90
|
+
export type BountyDifficulty = 'trivial' | 'easy' | 'medium' | 'hard' | 'expert';
|
|
91
|
+
export type SubmissionMethod = 'pr' | 'patch' | 'gist' | 'proof';
|
|
92
|
+
export interface BountyFile {
|
|
93
|
+
path: string;
|
|
94
|
+
description?: string;
|
|
95
|
+
}
|
|
96
|
+
export interface BountyClaim {
|
|
97
|
+
agentId: string;
|
|
98
|
+
claimedAt: Date;
|
|
99
|
+
expiresAt: Date;
|
|
100
|
+
submissionUrl?: string;
|
|
101
|
+
submittedAt?: Date;
|
|
102
|
+
notes?: string;
|
|
103
|
+
status: 'active' | 'submitted' | 'expired' | 'withdrawn';
|
|
104
|
+
}
|
|
105
|
+
export interface Bounty {
|
|
106
|
+
id: string;
|
|
107
|
+
title: string;
|
|
108
|
+
description: string;
|
|
109
|
+
summary: string;
|
|
110
|
+
difficulty: BountyDifficulty;
|
|
111
|
+
requiredSkills: string[];
|
|
112
|
+
tags?: string[];
|
|
113
|
+
repoUrl?: string;
|
|
114
|
+
files?: BountyFile[];
|
|
115
|
+
acceptanceCriteria: string[];
|
|
116
|
+
submissionMethod: SubmissionMethod;
|
|
117
|
+
targetBranch?: string;
|
|
118
|
+
amount: number;
|
|
119
|
+
currency: Currency;
|
|
120
|
+
escrowId?: string;
|
|
121
|
+
createdAt: Date;
|
|
122
|
+
expiresAt: Date;
|
|
123
|
+
completedAt?: Date;
|
|
124
|
+
posterAgentId: string;
|
|
125
|
+
modAgentId?: string;
|
|
126
|
+
status: BountyStatus;
|
|
127
|
+
claims: BountyClaim[];
|
|
128
|
+
winnerAgentId?: string;
|
|
129
|
+
winnerSubmissionUrl?: string;
|
|
130
|
+
viewCount: number;
|
|
131
|
+
claimCount: number;
|
|
132
|
+
}
|
|
133
|
+
export interface BountyListing {
|
|
134
|
+
id: string;
|
|
135
|
+
title: string;
|
|
136
|
+
summary: string;
|
|
137
|
+
amount: number;
|
|
138
|
+
currency: Currency;
|
|
139
|
+
difficulty: BountyDifficulty;
|
|
140
|
+
requiredSkills: string[];
|
|
141
|
+
status: BountyStatus;
|
|
142
|
+
expiresAt: Date;
|
|
143
|
+
claimCount: number;
|
|
144
|
+
posterAgentId: string;
|
|
145
|
+
}
|