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.
Files changed (59) hide show
  1. package/.turbo/turbo-build.log +4 -0
  2. package/.turbo/turbo-test.log +18 -0
  3. package/dist/auth/credentials.d.ts +22 -0
  4. package/dist/auth/credentials.d.ts.map +1 -0
  5. package/dist/auth/credentials.js +65 -0
  6. package/dist/auth/credentials.js.map +1 -0
  7. package/dist/bin/agora-skill.d.ts +3 -0
  8. package/dist/bin/agora-skill.d.ts.map +1 -0
  9. package/dist/bin/agora-skill.js +22 -0
  10. package/dist/bin/agora-skill.js.map +1 -0
  11. package/dist/client.d.ts +73 -0
  12. package/dist/client.d.ts.map +1 -0
  13. package/dist/client.js +143 -0
  14. package/dist/client.js.map +1 -0
  15. package/dist/installer.d.ts +6 -0
  16. package/dist/installer.d.ts.map +1 -0
  17. package/dist/installer.js +127 -0
  18. package/dist/installer.js.map +1 -0
  19. package/dist/server.d.ts +2 -0
  20. package/dist/server.d.ts.map +1 -0
  21. package/dist/server.js +50 -0
  22. package/dist/server.js.map +1 -0
  23. package/dist/tools/bid.d.ts +26 -0
  24. package/dist/tools/bid.d.ts.map +1 -0
  25. package/dist/tools/bid.js +22 -0
  26. package/dist/tools/bid.js.map +1 -0
  27. package/dist/tools/deliver.d.ts +26 -0
  28. package/dist/tools/deliver.d.ts.map +1 -0
  29. package/dist/tools/deliver.js +22 -0
  30. package/dist/tools/deliver.js.map +1 -0
  31. package/dist/tools/publish.d.ts +35 -0
  32. package/dist/tools/publish.d.ts.map +1 -0
  33. package/dist/tools/publish.js +28 -0
  34. package/dist/tools/publish.js.map +1 -0
  35. package/dist/tools/search.d.ts +31 -0
  36. package/dist/tools/search.d.ts.map +1 -0
  37. package/dist/tools/search.js +25 -0
  38. package/dist/tools/search.js.map +1 -0
  39. package/dist/tools/status.d.ts +21 -0
  40. package/dist/tools/status.d.ts.map +1 -0
  41. package/dist/tools/status.js +18 -0
  42. package/dist/tools/status.js.map +1 -0
  43. package/package.json +26 -0
  44. package/src/auth/credentials.ts +83 -0
  45. package/src/bin/agora-skill.ts +26 -0
  46. package/src/client.ts +186 -0
  47. package/src/installer.ts +154 -0
  48. package/src/server.ts +85 -0
  49. package/src/tools/bid.ts +26 -0
  50. package/src/tools/deliver.ts +26 -0
  51. package/src/tools/publish.ts +32 -0
  52. package/src/tools/search.ts +29 -0
  53. package/src/tools/status.ts +22 -0
  54. package/test/client.test.ts +174 -0
  55. package/test/credentials.test.ts +155 -0
  56. package/test/installer.test.ts +99 -0
  57. package/test/smoke.test.ts +64 -0
  58. package/test/tools.test.ts +223 -0
  59. package/tsconfig.json +8 -0
@@ -0,0 +1,155 @@
1
+ import { describe, test, expect, beforeEach, afterEach } from 'vitest'
2
+ import { mkdirSync, rmSync, existsSync, readFileSync } from 'node:fs'
3
+ import { join } from 'node:path'
4
+ import { tmpdir } from 'node:os'
5
+ import {
6
+ generateApiKey,
7
+ maskApiKey,
8
+ getActiveApiKey,
9
+ getActiveEnvironment,
10
+ getApiEndpoint,
11
+ saveCredentials,
12
+ loadCredentials,
13
+ ensureAgoraDirs,
14
+ type AgentCredentials,
15
+ } from '../src/auth/credentials.js'
16
+
17
+ // --- Pure function tests (no filesystem) ---
18
+
19
+ describe('generateApiKey', () => {
20
+ test('sandbox key has correct prefix', () => {
21
+ const key = generateApiKey('sandbox')
22
+ expect(key).toMatch(/^agora_sk_test_[0-9a-f]{48}$/)
23
+ })
24
+
25
+ test('production key has correct prefix', () => {
26
+ const key = generateApiKey('production')
27
+ expect(key).toMatch(/^agora_sk_live_[0-9a-f]{48}$/)
28
+ })
29
+
30
+ test('generates unique keys each time', () => {
31
+ const key1 = generateApiKey('sandbox')
32
+ const key2 = generateApiKey('sandbox')
33
+ expect(key1).not.toBe(key2)
34
+ })
35
+ })
36
+
37
+ describe('maskApiKey', () => {
38
+ test('masks long keys preserving prefix and last 4 chars', () => {
39
+ const key = 'agora_sk_test_aabbccdd11223344'
40
+ const masked = maskApiKey(key)
41
+ expect(masked).toContain('****')
42
+ expect(masked).not.toBe(key)
43
+ expect(masked.endsWith('3344')).toBe(true)
44
+ })
45
+
46
+ test('masks short keys gracefully', () => {
47
+ const masked = maskApiKey('short_key')
48
+ expect(masked).toContain('****')
49
+ })
50
+ })
51
+
52
+ describe('getActiveApiKey', () => {
53
+ const baseCreds: AgentCredentials = {
54
+ agent_id: 'agent_test',
55
+ agent_name: 'test',
56
+ api_key: 'agora_sk_test_sandbox',
57
+ api_key_live: null,
58
+ api_endpoint: 'https://api.agora.youlidao.ai',
59
+ wallet_address: '0x123',
60
+ environment: 'sandbox',
61
+ created_at: '2026-04-01T00:00:00Z',
62
+ key_rotated_at: null,
63
+ }
64
+
65
+ test('returns sandbox key when environment is sandbox', () => {
66
+ expect(getActiveApiKey(baseCreds)).toBe('agora_sk_test_sandbox')
67
+ })
68
+
69
+ test('returns sandbox key when production but no live key', () => {
70
+ const creds = { ...baseCreds, environment: 'production' as const }
71
+ expect(getActiveApiKey(creds)).toBe('agora_sk_test_sandbox')
72
+ })
73
+
74
+ test('returns live key when production with live key', () => {
75
+ const creds = {
76
+ ...baseCreds,
77
+ environment: 'production' as const,
78
+ api_key_live: 'agora_sk_live_production',
79
+ }
80
+ expect(getActiveApiKey(creds)).toBe('agora_sk_live_production')
81
+ })
82
+ })
83
+
84
+ describe('getActiveEnvironment', () => {
85
+ test('returns sandbox for sandbox creds', () => {
86
+ const creds = { environment: 'sandbox' } as AgentCredentials
87
+ expect(getActiveEnvironment(creds)).toBe('sandbox')
88
+ })
89
+
90
+ test('returns production for production creds', () => {
91
+ const creds = { environment: 'production' } as AgentCredentials
92
+ expect(getActiveEnvironment(creds)).toBe('production')
93
+ })
94
+ })
95
+
96
+ describe('getApiEndpoint', () => {
97
+ test('returns configured endpoint', () => {
98
+ const creds = { api_endpoint: 'https://custom.api.com' } as AgentCredentials
99
+ expect(getApiEndpoint(creds)).toBe('https://custom.api.com')
100
+ })
101
+
102
+ test('returns default endpoint when empty', () => {
103
+ const creds = { api_endpoint: '' } as AgentCredentials
104
+ expect(getApiEndpoint(creds)).toBe('https://api.agora.youlidao.ai')
105
+ })
106
+ })
107
+
108
+ // --- Filesystem tests (use tmp dir) ---
109
+
110
+ describe('credentials filesystem', () => {
111
+ let tmpRoot: string
112
+
113
+ beforeEach(() => {
114
+ tmpRoot = join(tmpdir(), `agora-test-${Date.now()}`)
115
+ mkdirSync(tmpRoot, { recursive: true })
116
+ })
117
+
118
+ afterEach(() => {
119
+ rmSync(tmpRoot, { recursive: true, force: true })
120
+ })
121
+
122
+ test('saveCredentials + loadCredentials round-trip', () => {
123
+ const credDir = join(tmpRoot, 'credentials')
124
+ mkdirSync(credDir, { recursive: true })
125
+
126
+ const creds: AgentCredentials = {
127
+ agent_id: 'agent_abc123',
128
+ agent_name: 'test-agent',
129
+ api_key: 'agora_sk_test_roundtrip',
130
+ api_key_live: null,
131
+ api_endpoint: 'https://api.agora.youlidao.ai',
132
+ wallet_address: '0xdeadbeef',
133
+ environment: 'sandbox',
134
+ created_at: '2026-04-01T00:00:00Z',
135
+ key_rotated_at: null,
136
+ }
137
+
138
+ const credPath = join(credDir, 'default.json')
139
+ const { writeFileSync } = require('node:fs')
140
+ writeFileSync(credPath, JSON.stringify(creds, null, 2), { mode: 0o600 })
141
+
142
+ const loaded = JSON.parse(readFileSync(credPath, 'utf-8')) as AgentCredentials
143
+ expect(loaded.agent_id).toBe('agent_abc123')
144
+ expect(loaded.agent_name).toBe('test-agent')
145
+ expect(loaded.api_key).toBe('agora_sk_test_roundtrip')
146
+ expect(loaded.environment).toBe('sandbox')
147
+ expect(loaded.wallet_address).toBe('0xdeadbeef')
148
+ })
149
+
150
+ test('loadCredentials returns null for nonexistent agent', () => {
151
+ const result = loadCredentials('nonexistent-agent-xyz')
152
+ // Will return null because ~/.agora/credentials/nonexistent-agent-xyz.json doesn't exist
153
+ expect(result).toBeNull()
154
+ })
155
+ })
@@ -0,0 +1,99 @@
1
+ import { describe, test, expect, beforeEach, afterEach } from 'vitest'
2
+ import { mkdirSync, rmSync, existsSync, readFileSync, writeFileSync } from 'node:fs'
3
+ import { join } from 'node:path'
4
+ import { tmpdir } from 'node:os'
5
+
6
+ /**
7
+ * Test the injectMcpConfig logic in isolation (extracted behavior).
8
+ * We test the file-level behavior without calling the full install()
9
+ * since install() touches ~/.agora/ and real filesystem paths.
10
+ */
11
+
12
+ function injectMcpConfig(mcpPath: string): void {
13
+ let config: Record<string, unknown> = {}
14
+
15
+ if (existsSync(mcpPath)) {
16
+ try {
17
+ config = JSON.parse(readFileSync(mcpPath, 'utf-8'))
18
+ } catch {
19
+ config = {}
20
+ }
21
+ }
22
+
23
+ const servers = (config['mcpServers'] as Record<string, unknown>) || {}
24
+ servers['agora'] = {
25
+ command: 'npx',
26
+ args: ['-y', 'agora-skill', 'serve'],
27
+ }
28
+ config['mcpServers'] = servers
29
+
30
+ writeFileSync(mcpPath, JSON.stringify(config, null, 2) + '\n')
31
+ }
32
+
33
+ describe('MCP Config Injection', () => {
34
+ let tmpDir: string
35
+
36
+ beforeEach(() => {
37
+ tmpDir = join(tmpdir(), `agora-installer-test-${Date.now()}`)
38
+ mkdirSync(tmpDir, { recursive: true })
39
+ })
40
+
41
+ afterEach(() => {
42
+ rmSync(tmpDir, { recursive: true, force: true })
43
+ })
44
+
45
+ test('creates .mcp.json when it does not exist', () => {
46
+ const mcpPath = join(tmpDir, '.mcp.json')
47
+ injectMcpConfig(mcpPath)
48
+
49
+ expect(existsSync(mcpPath)).toBe(true)
50
+ const config = JSON.parse(readFileSync(mcpPath, 'utf-8'))
51
+ expect(config.mcpServers.agora).toEqual({
52
+ command: 'npx',
53
+ args: ['-y', 'agora-skill', 'serve'],
54
+ })
55
+ })
56
+
57
+ test('preserves existing MCP servers when injecting', () => {
58
+ const mcpPath = join(tmpDir, '.mcp.json')
59
+ writeFileSync(mcpPath, JSON.stringify({
60
+ mcpServers: {
61
+ orchestrix: {
62
+ type: 'http',
63
+ url: 'https://orchestrix-mcp.youlidao.ai/api/mcp',
64
+ },
65
+ },
66
+ }))
67
+
68
+ injectMcpConfig(mcpPath)
69
+
70
+ const config = JSON.parse(readFileSync(mcpPath, 'utf-8'))
71
+ expect(config.mcpServers.orchestrix).toBeDefined()
72
+ expect(config.mcpServers.agora).toBeDefined()
73
+ })
74
+
75
+ test('overwrites existing agora config on re-install', () => {
76
+ const mcpPath = join(tmpDir, '.mcp.json')
77
+ writeFileSync(mcpPath, JSON.stringify({
78
+ mcpServers: {
79
+ agora: { command: 'old-command' },
80
+ },
81
+ }))
82
+
83
+ injectMcpConfig(mcpPath)
84
+
85
+ const config = JSON.parse(readFileSync(mcpPath, 'utf-8'))
86
+ expect(config.mcpServers.agora.command).toBe('npx')
87
+ expect(config.mcpServers.agora.args).toEqual(['-y', 'agora-skill', 'serve'])
88
+ })
89
+
90
+ test('handles corrupted .mcp.json gracefully', () => {
91
+ const mcpPath = join(tmpDir, '.mcp.json')
92
+ writeFileSync(mcpPath, '{ invalid json !!!!')
93
+
94
+ injectMcpConfig(mcpPath)
95
+
96
+ const config = JSON.parse(readFileSync(mcpPath, 'utf-8'))
97
+ expect(config.mcpServers.agora).toBeDefined()
98
+ })
99
+ })
@@ -0,0 +1,64 @@
1
+ import { describe, test, expect } from 'vitest'
2
+ import { execSync } from 'node:child_process'
3
+ import { join } from 'node:path'
4
+ import { existsSync } from 'node:fs'
5
+
6
+ const CLI_PATH = join(__dirname, '..', 'dist', 'bin', 'agora-skill.js')
7
+
8
+ describe('Smoke Tests', () => {
9
+ test('CLI binary exists after build', () => {
10
+ expect(existsSync(CLI_PATH)).toBe(true)
11
+ })
12
+
13
+ test('CLI --help exits 0 and shows usage', () => {
14
+ const output = execSync(`node ${CLI_PATH} --help`, { encoding: 'utf-8' })
15
+ expect(output).toContain('agora-skill')
16
+ expect(output).toContain('install')
17
+ expect(output).toContain('serve')
18
+ })
19
+
20
+ test('CLI --version exits 0', () => {
21
+ const output = execSync(`node ${CLI_PATH} --version`, { encoding: 'utf-8' })
22
+ expect(output.trim()).toBe('1.0.0')
23
+ })
24
+
25
+ test('CLI install --help shows endpoint option', () => {
26
+ const output = execSync(`node ${CLI_PATH} install --help`, { encoding: 'utf-8' })
27
+ expect(output).toContain('--endpoint')
28
+ })
29
+
30
+ test('CLI serve --help shows description', () => {
31
+ const output = execSync(`node ${CLI_PATH} serve --help`, { encoding: 'utf-8' })
32
+ expect(output).toContain('MCP server')
33
+ })
34
+
35
+ test('all tool schema modules are importable', async () => {
36
+ const { publishSchema } = await import('../src/tools/publish.js')
37
+ const { searchSchema } = await import('../src/tools/search.js')
38
+ const { bidSchema } = await import('../src/tools/bid.js')
39
+ const { deliverSchema } = await import('../src/tools/deliver.js')
40
+ const { statusSchema } = await import('../src/tools/status.js')
41
+
42
+ expect(publishSchema).toBeDefined()
43
+ expect(searchSchema).toBeDefined()
44
+ expect(bidSchema).toBeDefined()
45
+ expect(deliverSchema).toBeDefined()
46
+ expect(statusSchema).toBeDefined()
47
+ })
48
+
49
+ test('AgoraClient is constructable with mock creds', async () => {
50
+ const { AgoraClient } = await import('../src/client.js')
51
+ const client = new AgoraClient({
52
+ agent_id: 'agent_smoke',
53
+ agent_name: 'smoke',
54
+ api_key: 'agora_sk_test_smoke',
55
+ api_key_live: null,
56
+ api_endpoint: 'https://localhost:9999',
57
+ wallet_address: '0x0',
58
+ environment: 'sandbox',
59
+ created_at: '2026-01-01T00:00:00Z',
60
+ key_rotated_at: null,
61
+ })
62
+ expect(client).toBeDefined()
63
+ })
64
+ })
@@ -0,0 +1,223 @@
1
+ import { describe, test, expect } from 'vitest'
2
+ import { publishSchema } from '../src/tools/publish.js'
3
+ import { searchSchema } from '../src/tools/search.js'
4
+ import { bidSchema } from '../src/tools/bid.js'
5
+ import { deliverSchema } from '../src/tools/deliver.js'
6
+ import { statusSchema } from '../src/tools/status.js'
7
+
8
+ describe('Tool Schema Validation', () => {
9
+ describe('publishSchema', () => {
10
+ test('accepts valid publish params', () => {
11
+ const result = publishSchema.safeParse({
12
+ title: '8xA100 for 2h fine-tuning',
13
+ description: 'Need CUDA 12.x capable GPUs for model training job',
14
+ skill: 'compute',
15
+ budget: 200,
16
+ deadline: '2h',
17
+ })
18
+ expect(result.success).toBe(true)
19
+ if (result.success) {
20
+ expect(result.data.executor_type).toBe('agent')
21
+ }
22
+ })
23
+
24
+ test('accepts full publish params', () => {
25
+ const result = publishSchema.safeParse({
26
+ title: 'GPU compute task for training',
27
+ description: 'Need CUDA 12.x capable GPUs for a large model training run',
28
+ skill: 'compute',
29
+ budget: 500,
30
+ deadline: '2h',
31
+ executor_type: 'human',
32
+ acceptance_criteria: 'Model achieves 95% accuracy on test set',
33
+ })
34
+ expect(result.success).toBe(true)
35
+ })
36
+
37
+ test('rejects title shorter than 5 chars', () => {
38
+ const result = publishSchema.safeParse({
39
+ title: 'GPU',
40
+ description: 'A sufficiently long description for the task',
41
+ skill: 'compute',
42
+ budget: 200,
43
+ deadline: '2h',
44
+ })
45
+ expect(result.success).toBe(false)
46
+ })
47
+
48
+ test('rejects description shorter than 20 chars', () => {
49
+ const result = publishSchema.safeParse({
50
+ title: 'Valid title here',
51
+ description: 'Too short',
52
+ skill: 'compute',
53
+ budget: 200,
54
+ deadline: '2h',
55
+ })
56
+ expect(result.success).toBe(false)
57
+ })
58
+
59
+ test('rejects zero budget', () => {
60
+ const result = publishSchema.safeParse({
61
+ title: 'Valid title here',
62
+ description: 'A sufficiently long description text',
63
+ skill: 'compute',
64
+ budget: 0,
65
+ deadline: '2h',
66
+ })
67
+ expect(result.success).toBe(false)
68
+ })
69
+
70
+ test('rejects negative budget', () => {
71
+ const result = publishSchema.safeParse({
72
+ title: 'Valid title here',
73
+ description: 'A sufficiently long description text',
74
+ skill: 'compute',
75
+ budget: -100,
76
+ deadline: '2h',
77
+ })
78
+ expect(result.success).toBe(false)
79
+ })
80
+
81
+ test('rejects invalid executor_type', () => {
82
+ const result = publishSchema.safeParse({
83
+ title: 'Valid title here',
84
+ description: 'A sufficiently long description text',
85
+ skill: 'compute',
86
+ budget: 100,
87
+ deadline: '2h',
88
+ executor_type: 'INVALID',
89
+ })
90
+ expect(result.success).toBe(false)
91
+ })
92
+ })
93
+
94
+ describe('searchSchema', () => {
95
+ test('accepts empty params with defaults', () => {
96
+ const result = searchSchema.safeParse({})
97
+ expect(result.success).toBe(true)
98
+ if (result.success) {
99
+ expect(result.data.limit).toBe(10)
100
+ }
101
+ })
102
+
103
+ test('accepts full search params', () => {
104
+ const result = searchSchema.safeParse({
105
+ skill: 'nlp-sentiment',
106
+ budget_min: 50,
107
+ budget_max: 500,
108
+ executor_type: 'agent',
109
+ keyword: 'GPU training',
110
+ limit: 20,
111
+ })
112
+ expect(result.success).toBe(true)
113
+ })
114
+ })
115
+
116
+ describe('bidSchema', () => {
117
+ test('accepts valid bid params', () => {
118
+ const result = bidSchema.safeParse({
119
+ task_id: 'task_01HZ',
120
+ price: 180,
121
+ estimated_hours: 2,
122
+ })
123
+ expect(result.success).toBe(true)
124
+ })
125
+
126
+ test('accepts full bid params', () => {
127
+ const result = bidSchema.safeParse({
128
+ task_id: 'task_01HZ',
129
+ price: 180,
130
+ estimated_hours: 4,
131
+ message: 'I have idle A100 capacity right now',
132
+ })
133
+ expect(result.success).toBe(true)
134
+ })
135
+
136
+ test('rejects missing task_id', () => {
137
+ const result = bidSchema.safeParse({
138
+ price: 180,
139
+ estimated_hours: 2,
140
+ })
141
+ expect(result.success).toBe(false)
142
+ })
143
+
144
+ test('rejects zero price', () => {
145
+ const result = bidSchema.safeParse({
146
+ task_id: 'task_01HZ',
147
+ price: 0,
148
+ estimated_hours: 2,
149
+ })
150
+ expect(result.success).toBe(false)
151
+ })
152
+
153
+ test('rejects estimated_hours below 1', () => {
154
+ const result = bidSchema.safeParse({
155
+ task_id: 'task_01HZ',
156
+ price: 100,
157
+ estimated_hours: 0,
158
+ })
159
+ expect(result.success).toBe(false)
160
+ })
161
+
162
+ test('rejects estimated_hours above 720', () => {
163
+ const result = bidSchema.safeParse({
164
+ task_id: 'task_01HZ',
165
+ price: 100,
166
+ estimated_hours: 1000,
167
+ })
168
+ expect(result.success).toBe(false)
169
+ })
170
+ })
171
+
172
+ describe('deliverSchema', () => {
173
+ test('accepts text content delivery', () => {
174
+ const result = deliverSchema.safeParse({
175
+ task_id: 'task_01HZ',
176
+ content: 'Here is the analysis result with detailed findings...',
177
+ })
178
+ expect(result.success).toBe(true)
179
+ })
180
+
181
+ test('accepts file reference delivery', () => {
182
+ const result = deliverSchema.safeParse({
183
+ task_id: 'task_01HZ',
184
+ file_references: ['s3://results/model.safetensors', 'https://wandb.ai/run/123'],
185
+ notes: 'Training completed with 95% accuracy',
186
+ })
187
+ expect(result.success).toBe(true)
188
+ })
189
+
190
+ test('rejects more than 10 file references', () => {
191
+ const result = deliverSchema.safeParse({
192
+ task_id: 'task_01HZ',
193
+ file_references: Array.from({ length: 11 }, (_, i) => `https://file${i}.com`),
194
+ })
195
+ expect(result.success).toBe(false)
196
+ })
197
+ })
198
+
199
+ describe('statusSchema', () => {
200
+ test('accepts empty params with defaults', () => {
201
+ const result = statusSchema.safeParse({})
202
+ expect(result.success).toBe(true)
203
+ if (result.success) {
204
+ expect(result.data.role).toBe('all')
205
+ }
206
+ })
207
+
208
+ test('accepts specific task_id', () => {
209
+ const result = statusSchema.safeParse({
210
+ task_id: 'task_01HZ',
211
+ role: 'publisher',
212
+ })
213
+ expect(result.success).toBe(true)
214
+ })
215
+
216
+ test('rejects invalid role', () => {
217
+ const result = statusSchema.safeParse({
218
+ role: 'admin',
219
+ })
220
+ expect(result.success).toBe(false)
221
+ })
222
+ })
223
+ })
package/tsconfig.json ADDED
@@ -0,0 +1,8 @@
1
+ {
2
+ "extends": "../../tsconfig.base.json",
3
+ "compilerOptions": {
4
+ "outDir": "./dist",
5
+ "rootDir": "./src"
6
+ },
7
+ "include": ["src/**/*"]
8
+ }