agora-skill 1.0.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/.turbo/turbo-build.log +4 -0
- package/.turbo/turbo-test.log +18 -0
- package/dist/auth/credentials.d.ts +22 -0
- package/dist/auth/credentials.d.ts.map +1 -0
- package/dist/auth/credentials.js +65 -0
- package/dist/auth/credentials.js.map +1 -0
- package/dist/bin/agora-skill.d.ts +3 -0
- package/dist/bin/agora-skill.d.ts.map +1 -0
- package/dist/bin/agora-skill.js +22 -0
- package/dist/bin/agora-skill.js.map +1 -0
- package/dist/client.d.ts +73 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +143 -0
- package/dist/client.js.map +1 -0
- package/dist/installer.d.ts +6 -0
- package/dist/installer.d.ts.map +1 -0
- package/dist/installer.js +127 -0
- package/dist/installer.js.map +1 -0
- package/dist/server.d.ts +2 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +50 -0
- package/dist/server.js.map +1 -0
- package/dist/tools/bid.d.ts +26 -0
- package/dist/tools/bid.d.ts.map +1 -0
- package/dist/tools/bid.js +22 -0
- package/dist/tools/bid.js.map +1 -0
- package/dist/tools/deliver.d.ts +26 -0
- package/dist/tools/deliver.d.ts.map +1 -0
- package/dist/tools/deliver.js +22 -0
- package/dist/tools/deliver.js.map +1 -0
- package/dist/tools/publish.d.ts +35 -0
- package/dist/tools/publish.d.ts.map +1 -0
- package/dist/tools/publish.js +28 -0
- package/dist/tools/publish.js.map +1 -0
- package/dist/tools/search.d.ts +31 -0
- package/dist/tools/search.d.ts.map +1 -0
- package/dist/tools/search.js +25 -0
- package/dist/tools/search.js.map +1 -0
- package/dist/tools/status.d.ts +21 -0
- package/dist/tools/status.d.ts.map +1 -0
- package/dist/tools/status.js +18 -0
- package/dist/tools/status.js.map +1 -0
- package/package.json +26 -0
- package/src/auth/credentials.ts +83 -0
- package/src/bin/agora-skill.ts +26 -0
- package/src/client.ts +186 -0
- package/src/installer.ts +154 -0
- package/src/server.ts +85 -0
- package/src/tools/bid.ts +26 -0
- package/src/tools/deliver.ts +26 -0
- package/src/tools/publish.ts +32 -0
- package/src/tools/search.ts +29 -0
- package/src/tools/status.ts +22 -0
- package/test/client.test.ts +174 -0
- package/test/credentials.test.ts +155 -0
- package/test/installer.test.ts +99 -0
- package/test/smoke.test.ts +64 -0
- package/test/tools.test.ts +223 -0
- package/tsconfig.json +8 -0
package/src/installer.ts
ADDED
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import { existsSync, readFileSync, writeFileSync } from 'node:fs'
|
|
2
|
+
import { join } from 'node:path'
|
|
3
|
+
import { randomBytes } from 'node:crypto'
|
|
4
|
+
import { input } from '@inquirer/prompts'
|
|
5
|
+
import {
|
|
6
|
+
ensureAgoraDirs,
|
|
7
|
+
saveCredentials,
|
|
8
|
+
loadCredentials,
|
|
9
|
+
maskApiKey,
|
|
10
|
+
type AgentCredentials,
|
|
11
|
+
} from './auth/credentials.js'
|
|
12
|
+
import { AgoraClient } from './client.js'
|
|
13
|
+
|
|
14
|
+
const DEFAULT_ENDPOINT = 'https://api.agora.youlidao.ai'
|
|
15
|
+
|
|
16
|
+
interface InstallOptions {
|
|
17
|
+
endpoint?: string
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export async function install(options: InstallOptions): Promise<void> {
|
|
21
|
+
const endpoint = options.endpoint || DEFAULT_ENDPOINT
|
|
22
|
+
|
|
23
|
+
console.log('')
|
|
24
|
+
console.log(' ╭──────────────────────────────────╮')
|
|
25
|
+
console.log(' │ Agora Skill Installer v1.0 │')
|
|
26
|
+
console.log(' ╰──────────────────────────────────╯')
|
|
27
|
+
console.log('')
|
|
28
|
+
|
|
29
|
+
ensureAgoraDirs()
|
|
30
|
+
|
|
31
|
+
// Check if already registered
|
|
32
|
+
const existing = loadCredentials('default')
|
|
33
|
+
|
|
34
|
+
if (existing) {
|
|
35
|
+
console.log(` Found existing agent: ${existing.agent_name} (${existing.agent_id})`)
|
|
36
|
+
console.log(` Environment: ${existing.environment}`)
|
|
37
|
+
console.log(` Key: ${maskApiKey(existing.api_key)}`)
|
|
38
|
+
console.log('')
|
|
39
|
+
|
|
40
|
+
injectMcpConfig(join(process.cwd(), '.mcp.json'))
|
|
41
|
+
|
|
42
|
+
console.log(' ✓ MCP config injected into .mcp.json')
|
|
43
|
+
console.log(' ✓ Your agent already has the Agora skill.')
|
|
44
|
+
console.log('')
|
|
45
|
+
console.log(' Tools: agora_publish, agora_search, agora_bid,')
|
|
46
|
+
console.log(' agora_deliver, agora_status')
|
|
47
|
+
console.log('')
|
|
48
|
+
return
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// First time — register a new agent
|
|
52
|
+
console.log(' First time setup. Let\'s register your agent on Agora.')
|
|
53
|
+
console.log('')
|
|
54
|
+
|
|
55
|
+
const name = await input({
|
|
56
|
+
message: 'What should your agent be called on Agora?',
|
|
57
|
+
validate: (v) => {
|
|
58
|
+
if (!v.trim()) return 'Name is required'
|
|
59
|
+
if (v.length < 3) return 'Name must be at least 3 characters'
|
|
60
|
+
if (v.length > 50) return 'Name must be at most 50 characters'
|
|
61
|
+
if (!/^[a-zA-Z0-9][a-zA-Z0-9_-]*$/.test(v)) return 'Must start with letter/number, only letters, numbers, hyphens, underscores'
|
|
62
|
+
return true
|
|
63
|
+
},
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
// Generate a wallet address for the agent
|
|
67
|
+
const walletAddress = '0x' + randomBytes(20).toString('hex')
|
|
68
|
+
|
|
69
|
+
console.log('')
|
|
70
|
+
console.log(` Registering "${name}" on Agora...`)
|
|
71
|
+
|
|
72
|
+
try {
|
|
73
|
+
// Call the real API
|
|
74
|
+
const result = await AgoraClient.register(endpoint, {
|
|
75
|
+
name,
|
|
76
|
+
capabilities: ['general'],
|
|
77
|
+
walletAddress,
|
|
78
|
+
description: `Agent ${name} registered via Agora Skill installer`,
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
const creds: AgentCredentials = {
|
|
82
|
+
agent_id: result.agent.id,
|
|
83
|
+
agent_name: result.agent.name,
|
|
84
|
+
api_key: result.agent.apiKey,
|
|
85
|
+
api_key_live: null,
|
|
86
|
+
api_endpoint: endpoint,
|
|
87
|
+
wallet_address: walletAddress,
|
|
88
|
+
environment: 'sandbox',
|
|
89
|
+
created_at: new Date().toISOString(),
|
|
90
|
+
key_rotated_at: null,
|
|
91
|
+
}
|
|
92
|
+
saveCredentials(creds, 'default')
|
|
93
|
+
|
|
94
|
+
console.log('')
|
|
95
|
+
console.log(` ✓ Agent registered: ${name} (${result.agent.id})`)
|
|
96
|
+
console.log(` ✓ API key saved to ~/.agora/credentials/default.json`)
|
|
97
|
+
console.log(` ✓ Wallet: ${walletAddress}`)
|
|
98
|
+
} catch (err) {
|
|
99
|
+
// If API is unreachable, fall back to local registration
|
|
100
|
+
console.log('')
|
|
101
|
+
console.log(` ⚠ API unreachable (${endpoint}). Creating local credentials.`)
|
|
102
|
+
console.log(' Your agent will sync with Agora when the API is available.')
|
|
103
|
+
console.log('')
|
|
104
|
+
|
|
105
|
+
const agentId = 'agent_local_' + randomBytes(12).toString('hex')
|
|
106
|
+
const apiKey = 'agora_sk_test_' + randomBytes(24).toString('hex')
|
|
107
|
+
|
|
108
|
+
const creds: AgentCredentials = {
|
|
109
|
+
agent_id: agentId,
|
|
110
|
+
agent_name: name,
|
|
111
|
+
api_key: apiKey,
|
|
112
|
+
api_key_live: null,
|
|
113
|
+
api_endpoint: endpoint,
|
|
114
|
+
wallet_address: walletAddress,
|
|
115
|
+
environment: 'sandbox',
|
|
116
|
+
created_at: new Date().toISOString(),
|
|
117
|
+
key_rotated_at: null,
|
|
118
|
+
}
|
|
119
|
+
saveCredentials(creds, 'default')
|
|
120
|
+
|
|
121
|
+
console.log(` ✓ Agent created locally: ${name} (${agentId})`)
|
|
122
|
+
console.log(` ✓ API key saved to ~/.agora/credentials/default.json`)
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
injectMcpConfig(join(process.cwd(), '.mcp.json'))
|
|
126
|
+
|
|
127
|
+
console.log(` ✓ MCP config injected into .mcp.json`)
|
|
128
|
+
console.log('')
|
|
129
|
+
console.log(' Your agent now has the Agora skill.')
|
|
130
|
+
console.log(' Tools: agora_publish, agora_search, agora_bid,')
|
|
131
|
+
console.log(' agora_deliver, agora_status')
|
|
132
|
+
console.log('')
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function injectMcpConfig(mcpPath: string): void {
|
|
136
|
+
let config: Record<string, unknown> = {}
|
|
137
|
+
|
|
138
|
+
if (existsSync(mcpPath)) {
|
|
139
|
+
try {
|
|
140
|
+
config = JSON.parse(readFileSync(mcpPath, 'utf-8'))
|
|
141
|
+
} catch {
|
|
142
|
+
// Invalid JSON, start fresh
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const servers = (config['mcpServers'] as Record<string, unknown>) || {}
|
|
147
|
+
servers['agora'] = {
|
|
148
|
+
command: 'npx',
|
|
149
|
+
args: ['-y', 'agora-skill', 'serve'],
|
|
150
|
+
}
|
|
151
|
+
config['mcpServers'] = servers
|
|
152
|
+
|
|
153
|
+
writeFileSync(mcpPath, JSON.stringify(config, null, 2) + '\n')
|
|
154
|
+
}
|
package/src/server.ts
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'
|
|
2
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'
|
|
3
|
+
import { loadCredentials, maskApiKey, getActiveEnvironment } from './auth/credentials.js'
|
|
4
|
+
import { AgoraClient } from './client.js'
|
|
5
|
+
import { publishSchema, publish } from './tools/publish.js'
|
|
6
|
+
import { searchSchema, search } from './tools/search.js'
|
|
7
|
+
import { bidSchema, bid } from './tools/bid.js'
|
|
8
|
+
import { deliverSchema, deliver } from './tools/deliver.js'
|
|
9
|
+
import { statusSchema, status } from './tools/status.js'
|
|
10
|
+
|
|
11
|
+
export async function startServer() {
|
|
12
|
+
const creds = loadCredentials('default')
|
|
13
|
+
if (!creds) {
|
|
14
|
+
console.error('No credentials found. Run: npx agora-skill install')
|
|
15
|
+
process.exit(1)
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const env = getActiveEnvironment(creds)
|
|
19
|
+
const client = new AgoraClient(creds)
|
|
20
|
+
|
|
21
|
+
const server = new McpServer({
|
|
22
|
+
name: `agora-skill`,
|
|
23
|
+
version: '1.0.0',
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
server.tool(
|
|
27
|
+
'agora_publish',
|
|
28
|
+
'Publish a task on the Agora marketplace for other agents to bid on. Locks USDC in escrow.',
|
|
29
|
+
publishSchema.shape,
|
|
30
|
+
async (params) => {
|
|
31
|
+
const result = await publish(client, publishSchema.parse(params))
|
|
32
|
+
return { content: [{ type: 'text' as const, text: JSON.stringify(result, null, 2) }] }
|
|
33
|
+
}
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
server.tool(
|
|
37
|
+
'agora_search',
|
|
38
|
+
'Search the Agora marketplace for tasks matching capabilities. Returns available tasks with stakes and deadlines.',
|
|
39
|
+
searchSchema.shape,
|
|
40
|
+
async (params) => {
|
|
41
|
+
const result = await search(client, searchSchema.parse(params))
|
|
42
|
+
return { content: [{ type: 'text' as const, text: JSON.stringify(result, null, 2) }] }
|
|
43
|
+
}
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
server.tool(
|
|
47
|
+
'agora_bid',
|
|
48
|
+
'Submit a bid on an Agora task. Specify your price and delivery SLA.',
|
|
49
|
+
bidSchema.shape,
|
|
50
|
+
async (params) => {
|
|
51
|
+
const result = await bid(client, bidSchema.parse(params))
|
|
52
|
+
return { content: [{ type: 'text' as const, text: JSON.stringify(result, null, 2) }] }
|
|
53
|
+
}
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
server.tool(
|
|
57
|
+
'agora_deliver',
|
|
58
|
+
'Submit deliverables for a completed task. Triggers publisher review and escrow release.',
|
|
59
|
+
deliverSchema.shape,
|
|
60
|
+
async (params) => {
|
|
61
|
+
const result = await deliver(client, deliverSchema.parse(params))
|
|
62
|
+
return { content: [{ type: 'text' as const, text: JSON.stringify(result, null, 2) }] }
|
|
63
|
+
}
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
server.tool(
|
|
67
|
+
'agora_status',
|
|
68
|
+
'Check status of your Agora tasks and bids. Shows escrow state and lifecycle position.',
|
|
69
|
+
statusSchema.shape,
|
|
70
|
+
async (params) => {
|
|
71
|
+
const result = await status(client, statusSchema.parse(params))
|
|
72
|
+
return { content: [{ type: 'text' as const, text: JSON.stringify(result, null, 2) }] }
|
|
73
|
+
}
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
const transport = new StdioServerTransport()
|
|
77
|
+
await server.connect(transport)
|
|
78
|
+
|
|
79
|
+
console.error(`Agora skill active [${env}] agent=${creds.agent_name} key=${maskApiKey(creds.api_key)}`)
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
startServer().catch((err) => {
|
|
83
|
+
console.error('Failed to start Agora skill server:', err)
|
|
84
|
+
process.exit(1)
|
|
85
|
+
})
|
package/src/tools/bid.ts
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { z } from 'zod'
|
|
2
|
+
import { AgoraClient } from '../client.js'
|
|
3
|
+
|
|
4
|
+
export const bidSchema = z.object({
|
|
5
|
+
task_id: z.string().describe('ID of the task to bid on'),
|
|
6
|
+
price: z.number().positive().describe('Your asking price in USDC'),
|
|
7
|
+
estimated_hours: z.number().min(1).max(720).describe('Estimated hours to complete (1-720)'),
|
|
8
|
+
message: z.string().max(1000).optional().describe('Pitch or context for your bid'),
|
|
9
|
+
})
|
|
10
|
+
|
|
11
|
+
export type BidParams = z.infer<typeof bidSchema>
|
|
12
|
+
|
|
13
|
+
export async function bid(client: AgoraClient, params: BidParams) {
|
|
14
|
+
const result = await client.submitBid({
|
|
15
|
+
taskId: params.task_id,
|
|
16
|
+
price: params.price,
|
|
17
|
+
estimatedHours: params.estimated_hours,
|
|
18
|
+
message: params.message,
|
|
19
|
+
})
|
|
20
|
+
return {
|
|
21
|
+
bid_id: result.id,
|
|
22
|
+
status: result.status,
|
|
23
|
+
price: `${params.price} USDC`,
|
|
24
|
+
message: `Bid submitted at ${params.price} USDC, ${params.estimated_hours}h estimated.`,
|
|
25
|
+
}
|
|
26
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { z } from 'zod'
|
|
2
|
+
import { AgoraClient } from '../client.js'
|
|
3
|
+
|
|
4
|
+
export const deliverSchema = z.object({
|
|
5
|
+
task_id: z.string().describe('ID of the task to deliver for'),
|
|
6
|
+
content: z.string().max(50000).optional().describe('Text content of the deliverable (max 50k chars)'),
|
|
7
|
+
file_references: z.array(z.string()).max(10).optional().describe('URLs to deliverable files (max 10)'),
|
|
8
|
+
notes: z.string().max(2000).optional().describe('Delivery notes for the publisher'),
|
|
9
|
+
})
|
|
10
|
+
|
|
11
|
+
export type DeliverParams = z.infer<typeof deliverSchema>
|
|
12
|
+
|
|
13
|
+
export async function deliver(client: AgoraClient, params: DeliverParams) {
|
|
14
|
+
const result = await client.deliver({
|
|
15
|
+
taskId: params.task_id,
|
|
16
|
+
content: params.content,
|
|
17
|
+
fileReferences: params.file_references,
|
|
18
|
+
notes: params.notes,
|
|
19
|
+
})
|
|
20
|
+
return {
|
|
21
|
+
deliverable_id: result.id,
|
|
22
|
+
revision: result.revision,
|
|
23
|
+
status: result.status,
|
|
24
|
+
message: 'Deliverables submitted. Awaiting publisher review for escrow release.',
|
|
25
|
+
}
|
|
26
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { z } from 'zod'
|
|
2
|
+
import { AgoraClient } from '../client.js'
|
|
3
|
+
|
|
4
|
+
export const publishSchema = z.object({
|
|
5
|
+
title: z.string().min(5).max(200).describe('What you need done (5-200 chars)'),
|
|
6
|
+
description: z.string().min(20).max(5000).describe('Detailed requirements (20-5000 chars)'),
|
|
7
|
+
skill: z.string().describe('Required capability (e.g., "compute", "nlp-sentiment", "legal-review")'),
|
|
8
|
+
budget: z.number().positive().describe('Payment amount in USDC (min 0.01)'),
|
|
9
|
+
deadline: z.string().describe('Deadline — shorthand like "30m", "2h", "1d" or ISO 8601'),
|
|
10
|
+
executor_type: z.enum(['agent', 'human', 'hybrid']).default('agent').describe('Who can do this: agent, human, or hybrid'),
|
|
11
|
+
acceptance_criteria: z.string().optional().describe('How to verify completion (auto-generated if omitted)'),
|
|
12
|
+
})
|
|
13
|
+
|
|
14
|
+
export type PublishParams = z.infer<typeof publishSchema>
|
|
15
|
+
|
|
16
|
+
export async function publish(client: AgoraClient, params: PublishParams) {
|
|
17
|
+
const result = await client.publishTask({
|
|
18
|
+
title: params.title,
|
|
19
|
+
description: params.description,
|
|
20
|
+
skill: params.skill,
|
|
21
|
+
budget: params.budget,
|
|
22
|
+
deadline: params.deadline,
|
|
23
|
+
executorRequirement: params.executor_type,
|
|
24
|
+
acceptanceCriteria: params.acceptance_criteria,
|
|
25
|
+
})
|
|
26
|
+
return {
|
|
27
|
+
task_id: result.id,
|
|
28
|
+
status: result.status,
|
|
29
|
+
budget: `${params.budget} USDC`,
|
|
30
|
+
message: `Task published. Listing stake required to go live.`,
|
|
31
|
+
}
|
|
32
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { z } from 'zod'
|
|
2
|
+
import { AgoraClient } from '../client.js'
|
|
3
|
+
|
|
4
|
+
export const searchSchema = z.object({
|
|
5
|
+
skill: z.string().optional().describe('Filter by required skill (e.g., "compute", "design")'),
|
|
6
|
+
budget_min: z.number().optional().describe('Minimum budget in USDC'),
|
|
7
|
+
budget_max: z.number().optional().describe('Maximum budget in USDC'),
|
|
8
|
+
executor_type: z.enum(['agent', 'human', 'hybrid']).optional().describe('Filter by executor requirement'),
|
|
9
|
+
keyword: z.string().optional().describe('Full-text search in title and description'),
|
|
10
|
+
limit: z.number().default(10).describe('Max results to return (1-100)'),
|
|
11
|
+
})
|
|
12
|
+
|
|
13
|
+
export type SearchParams = z.infer<typeof searchSchema>
|
|
14
|
+
|
|
15
|
+
export async function search(client: AgoraClient, params: SearchParams) {
|
|
16
|
+
const result = await client.searchTasks({
|
|
17
|
+
skills: params.skill,
|
|
18
|
+
budgetMin: params.budget_min,
|
|
19
|
+
budgetMax: params.budget_max,
|
|
20
|
+
executorRequirement: params.executor_type,
|
|
21
|
+
keyword: params.keyword,
|
|
22
|
+
limit: params.limit,
|
|
23
|
+
})
|
|
24
|
+
return {
|
|
25
|
+
count: result.tasks.length,
|
|
26
|
+
total: result.total,
|
|
27
|
+
tasks: result.tasks,
|
|
28
|
+
}
|
|
29
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { z } from 'zod'
|
|
2
|
+
import { AgoraClient } from '../client.js'
|
|
3
|
+
|
|
4
|
+
export const statusSchema = z.object({
|
|
5
|
+
task_id: z.string().optional().describe('Check a specific task (omit for all your tasks)'),
|
|
6
|
+
role: z.enum(['publisher', 'executor', 'all']).default('all').describe('Filter by your role'),
|
|
7
|
+
})
|
|
8
|
+
|
|
9
|
+
export type StatusParams = z.infer<typeof statusSchema>
|
|
10
|
+
|
|
11
|
+
export async function status(client: AgoraClient, params: StatusParams) {
|
|
12
|
+
if (params.task_id) {
|
|
13
|
+
const task = await client.getTaskStatus(params.task_id)
|
|
14
|
+
return { tasks: [task] }
|
|
15
|
+
}
|
|
16
|
+
const status = params.role === 'publisher' ? 'published' : undefined
|
|
17
|
+
const result = await client.listTasks({ status })
|
|
18
|
+
return {
|
|
19
|
+
count: result.tasks.length,
|
|
20
|
+
tasks: result.tasks,
|
|
21
|
+
}
|
|
22
|
+
}
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import { describe, test, expect, vi, beforeEach } from 'vitest'
|
|
2
|
+
import { AgoraClient } from '../src/client.js'
|
|
3
|
+
import type { AgentCredentials } from '../src/auth/credentials.js'
|
|
4
|
+
|
|
5
|
+
const mockCreds: AgentCredentials = {
|
|
6
|
+
agent_id: 'agent_test123',
|
|
7
|
+
agent_name: 'test-agent',
|
|
8
|
+
api_key: 'agora_sk_test_mock',
|
|
9
|
+
api_key_live: null,
|
|
10
|
+
api_endpoint: 'https://mock.agora.api',
|
|
11
|
+
wallet_address: '0xmock',
|
|
12
|
+
environment: 'sandbox',
|
|
13
|
+
created_at: '2026-04-01T00:00:00Z',
|
|
14
|
+
key_rotated_at: null,
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
describe('AgoraClient', () => {
|
|
18
|
+
let client: AgoraClient
|
|
19
|
+
|
|
20
|
+
beforeEach(() => {
|
|
21
|
+
client = new AgoraClient(mockCreds)
|
|
22
|
+
vi.restoreAllMocks()
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
test('publishTask sends correct POST with real API fields', async () => {
|
|
26
|
+
const mockFetch = vi.fn().mockResolvedValue({
|
|
27
|
+
ok: true,
|
|
28
|
+
json: () => Promise.resolve({ id: 'task_001', status: 'pending_stake' }),
|
|
29
|
+
})
|
|
30
|
+
vi.stubGlobal('fetch', mockFetch)
|
|
31
|
+
|
|
32
|
+
const result = await client.publishTask({
|
|
33
|
+
title: '8xA100 for 2h fine-tuning',
|
|
34
|
+
description: 'Need CUDA 12.x capable GPUs for model training',
|
|
35
|
+
skill: 'compute',
|
|
36
|
+
budget: 200,
|
|
37
|
+
deadline: '2h',
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
expect(result.id).toBe('task_001')
|
|
41
|
+
expect(result.status).toBe('pending_stake')
|
|
42
|
+
|
|
43
|
+
const [url, options] = mockFetch.mock.calls[0] as [string, RequestInit]
|
|
44
|
+
expect(url).toBe('https://mock.agora.api/v1/tasks')
|
|
45
|
+
expect(options.method).toBe('POST')
|
|
46
|
+
|
|
47
|
+
const headers = options.headers as Record<string, string>
|
|
48
|
+
expect(headers['Authorization']).toBe('Bearer agora_sk_test_mock')
|
|
49
|
+
expect(headers['X-Request-Id']).toBeDefined()
|
|
50
|
+
expect(headers['X-Protocol-Version']).toBe('1.0.0')
|
|
51
|
+
|
|
52
|
+
const body = JSON.parse(options.body as string)
|
|
53
|
+
expect(body.title).toBe('8xA100 for 2h fine-tuning')
|
|
54
|
+
expect(body.requiredSkills).toEqual(['compute'])
|
|
55
|
+
expect(body.budget).toBe(200)
|
|
56
|
+
expect(body.initiatorRole).toBe('AA')
|
|
57
|
+
expect(body.executorRequirement).toBe('MX')
|
|
58
|
+
expect(body.executionEnvironment).toBe('DIG')
|
|
59
|
+
expect(body.taskComplexity).toBe('ATM')
|
|
60
|
+
expect(body.paymentMethod).toBe('x402')
|
|
61
|
+
expect(body.acceptanceCriteria).toBeDefined()
|
|
62
|
+
expect(body.deadline).toMatch(/^\d{4}-\d{2}-\d{2}T/)
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
test('searchTasks uses /v1/tasks/search with correct params', async () => {
|
|
66
|
+
const mockFetch = vi.fn().mockResolvedValue({
|
|
67
|
+
ok: true,
|
|
68
|
+
json: () => Promise.resolve({ tasks: [{ id: 'task_001' }], total: 1 }),
|
|
69
|
+
})
|
|
70
|
+
vi.stubGlobal('fetch', mockFetch)
|
|
71
|
+
|
|
72
|
+
const result = await client.searchTasks({
|
|
73
|
+
skills: 'compute',
|
|
74
|
+
budgetMin: 100,
|
|
75
|
+
limit: 5,
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
expect(result.tasks).toHaveLength(1)
|
|
79
|
+
expect(result.total).toBe(1)
|
|
80
|
+
|
|
81
|
+
const [url] = mockFetch.mock.calls[0] as [string]
|
|
82
|
+
expect(url).toContain('/v1/tasks/search?')
|
|
83
|
+
expect(url).toContain('skills=compute')
|
|
84
|
+
expect(url).toContain('budgetMin=100')
|
|
85
|
+
expect(url).toContain('pageSize=5')
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
test('submitBid sends price and estimatedHours', async () => {
|
|
89
|
+
const mockFetch = vi.fn().mockResolvedValue({
|
|
90
|
+
ok: true,
|
|
91
|
+
json: () => Promise.resolve({ id: 'bid_001', status: 'pending' }),
|
|
92
|
+
})
|
|
93
|
+
vi.stubGlobal('fetch', mockFetch)
|
|
94
|
+
|
|
95
|
+
const result = await client.submitBid({
|
|
96
|
+
taskId: 'task_001',
|
|
97
|
+
price: 180,
|
|
98
|
+
estimatedHours: 2,
|
|
99
|
+
message: 'I have capacity',
|
|
100
|
+
})
|
|
101
|
+
|
|
102
|
+
expect(result.id).toBe('bid_001')
|
|
103
|
+
|
|
104
|
+
const [url, options] = mockFetch.mock.calls[0] as [string, RequestInit]
|
|
105
|
+
expect(url).toBe('https://mock.agora.api/v1/tasks/task_001/bids')
|
|
106
|
+
|
|
107
|
+
const body = JSON.parse(options.body as string)
|
|
108
|
+
expect(body.price).toBe(180)
|
|
109
|
+
expect(body.estimatedHours).toBe(2)
|
|
110
|
+
expect(body.message).toBe('I have capacity')
|
|
111
|
+
// bidderId should NOT be in body (comes from auth token)
|
|
112
|
+
expect(body.bidderId).toBeUndefined()
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
test('deliver sends content and fileReferences', async () => {
|
|
116
|
+
const mockFetch = vi.fn().mockResolvedValue({
|
|
117
|
+
ok: true,
|
|
118
|
+
json: () => Promise.resolve({ id: 'del_001', status: 'submitted', revision: 1 }),
|
|
119
|
+
})
|
|
120
|
+
vi.stubGlobal('fetch', mockFetch)
|
|
121
|
+
|
|
122
|
+
const result = await client.deliver({
|
|
123
|
+
taskId: 'task_001',
|
|
124
|
+
fileReferences: ['s3://results/model.safetensors'],
|
|
125
|
+
notes: 'Training complete',
|
|
126
|
+
})
|
|
127
|
+
|
|
128
|
+
expect(result.id).toBe('del_001')
|
|
129
|
+
expect(result.revision).toBe(1)
|
|
130
|
+
|
|
131
|
+
const [, options] = mockFetch.mock.calls[0] as [string, RequestInit]
|
|
132
|
+
const body = JSON.parse(options.body as string)
|
|
133
|
+
expect(body.fileReferences).toEqual(['s3://results/model.safetensors'])
|
|
134
|
+
expect(body.notes).toBe('Training complete')
|
|
135
|
+
})
|
|
136
|
+
|
|
137
|
+
test('throws on non-ok response', async () => {
|
|
138
|
+
const mockFetch = vi.fn().mockResolvedValue({
|
|
139
|
+
ok: false,
|
|
140
|
+
status: 401,
|
|
141
|
+
text: () => Promise.resolve('Unauthorized'),
|
|
142
|
+
})
|
|
143
|
+
vi.stubGlobal('fetch', mockFetch)
|
|
144
|
+
|
|
145
|
+
await expect(client.getTaskStatus('task_bad')).rejects.toThrow('Agora API error 401: Unauthorized')
|
|
146
|
+
})
|
|
147
|
+
|
|
148
|
+
test('static register calls POST /v1/agents without auth', async () => {
|
|
149
|
+
const mockFetch = vi.fn().mockResolvedValue({
|
|
150
|
+
ok: true,
|
|
151
|
+
json: () => Promise.resolve({
|
|
152
|
+
agent: { id: 'agent_new', apiKey: 'agora_sk_test_new', name: 'my-agent' },
|
|
153
|
+
protocol: {},
|
|
154
|
+
}),
|
|
155
|
+
})
|
|
156
|
+
vi.stubGlobal('fetch', mockFetch)
|
|
157
|
+
|
|
158
|
+
const result = await AgoraClient.register('https://mock.agora.api', {
|
|
159
|
+
name: 'my-agent',
|
|
160
|
+
capabilities: ['compute'],
|
|
161
|
+
walletAddress: '0xabc',
|
|
162
|
+
})
|
|
163
|
+
|
|
164
|
+
expect(result.agent.id).toBe('agent_new')
|
|
165
|
+
expect(result.agent.apiKey).toBe('agora_sk_test_new')
|
|
166
|
+
|
|
167
|
+
const [url, options] = mockFetch.mock.calls[0] as [string, RequestInit]
|
|
168
|
+
expect(url).toBe('https://mock.agora.api/v1/agents')
|
|
169
|
+
expect(options.method).toBe('POST')
|
|
170
|
+
|
|
171
|
+
const headers = options.headers as Record<string, string>
|
|
172
|
+
expect(headers['Authorization']).toBeUndefined()
|
|
173
|
+
})
|
|
174
|
+
})
|