tlc-claude-code 1.4.1 → 1.4.4
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/dashboard/dist/App.js +229 -35
- package/dashboard/dist/components/AgentRegistryPane.d.ts +35 -0
- package/dashboard/dist/components/AgentRegistryPane.js +89 -0
- package/dashboard/dist/components/AgentRegistryPane.test.d.ts +1 -0
- package/dashboard/dist/components/AgentRegistryPane.test.js +200 -0
- package/dashboard/dist/components/RouterPane.d.ts +5 -0
- package/dashboard/dist/components/RouterPane.js +65 -0
- package/dashboard/dist/components/RouterPane.test.d.ts +1 -0
- package/dashboard/dist/components/RouterPane.test.js +176 -0
- package/dashboard/dist/components/accessibility.test.d.ts +1 -0
- package/dashboard/dist/components/accessibility.test.js +116 -0
- package/dashboard/dist/components/layout/MobileNav.d.ts +16 -0
- package/dashboard/dist/components/layout/MobileNav.js +31 -0
- package/dashboard/dist/components/layout/MobileNav.test.d.ts +1 -0
- package/dashboard/dist/components/layout/MobileNav.test.js +111 -0
- package/dashboard/dist/components/performance.test.d.ts +1 -0
- package/dashboard/dist/components/performance.test.js +114 -0
- package/dashboard/dist/components/responsive.test.d.ts +1 -0
- package/dashboard/dist/components/responsive.test.js +114 -0
- package/dashboard/dist/components/ui/Dropdown.d.ts +22 -0
- package/dashboard/dist/components/ui/Dropdown.js +109 -0
- package/dashboard/dist/components/ui/Dropdown.test.d.ts +1 -0
- package/dashboard/dist/components/ui/Dropdown.test.js +105 -0
- package/dashboard/dist/components/ui/Modal.d.ts +13 -0
- package/dashboard/dist/components/ui/Modal.js +25 -0
- package/dashboard/dist/components/ui/Modal.test.d.ts +1 -0
- package/dashboard/dist/components/ui/Modal.test.js +91 -0
- package/dashboard/dist/components/ui/Skeleton.d.ts +32 -0
- package/dashboard/dist/components/ui/Skeleton.js +48 -0
- package/dashboard/dist/components/ui/Skeleton.test.d.ts +1 -0
- package/dashboard/dist/components/ui/Skeleton.test.js +125 -0
- package/dashboard/dist/components/ui/Toast.d.ts +32 -0
- package/dashboard/dist/components/ui/Toast.js +21 -0
- package/dashboard/dist/components/ui/Toast.test.d.ts +1 -0
- package/dashboard/dist/components/ui/Toast.test.js +118 -0
- package/dashboard/dist/hooks/useTheme.d.ts +37 -0
- package/dashboard/dist/hooks/useTheme.js +96 -0
- package/dashboard/dist/hooks/useTheme.test.d.ts +1 -0
- package/dashboard/dist/hooks/useTheme.test.js +94 -0
- package/dashboard/dist/hooks/useWebSocket.d.ts +17 -0
- package/dashboard/dist/hooks/useWebSocket.js +100 -0
- package/dashboard/dist/hooks/useWebSocket.test.d.ts +1 -0
- package/dashboard/dist/hooks/useWebSocket.test.js +115 -0
- package/dashboard/dist/stores/projectStore.d.ts +44 -0
- package/dashboard/dist/stores/projectStore.js +76 -0
- package/dashboard/dist/stores/projectStore.test.d.ts +1 -0
- package/dashboard/dist/stores/projectStore.test.js +114 -0
- package/dashboard/dist/stores/uiStore.d.ts +29 -0
- package/dashboard/dist/stores/uiStore.js +72 -0
- package/dashboard/dist/stores/uiStore.test.d.ts +1 -0
- package/dashboard/dist/stores/uiStore.test.js +93 -0
- package/dashboard/package.json +3 -3
- package/docker-compose.dev.yml +6 -1
- package/package.json +5 -2
- package/server/dashboard/index.html +1336 -779
- package/server/index.js +178 -0
- package/server/lib/agent-cleanup.js +177 -0
- package/server/lib/agent-cleanup.test.js +359 -0
- package/server/lib/agent-hooks.js +126 -0
- package/server/lib/agent-hooks.test.js +303 -0
- package/server/lib/agent-metadata.js +179 -0
- package/server/lib/agent-metadata.test.js +383 -0
- package/server/lib/agent-persistence.js +191 -0
- package/server/lib/agent-persistence.test.js +475 -0
- package/server/lib/agent-registry-command.js +340 -0
- package/server/lib/agent-registry-command.test.js +334 -0
- package/server/lib/agent-registry.js +155 -0
- package/server/lib/agent-registry.test.js +239 -0
- package/server/lib/agent-state.js +236 -0
- package/server/lib/agent-state.test.js +375 -0
- package/server/lib/api-provider.js +186 -0
- package/server/lib/api-provider.test.js +336 -0
- package/server/lib/cli-detector.js +166 -0
- package/server/lib/cli-detector.test.js +269 -0
- package/server/lib/cli-provider.js +212 -0
- package/server/lib/cli-provider.test.js +349 -0
- package/server/lib/debug.test.js +62 -0
- package/server/lib/devserver-router-api.js +249 -0
- package/server/lib/devserver-router-api.test.js +426 -0
- package/server/lib/model-router.js +245 -0
- package/server/lib/model-router.test.js +313 -0
- package/server/lib/output-schemas.js +269 -0
- package/server/lib/output-schemas.test.js +307 -0
- package/server/lib/provider-interface.js +153 -0
- package/server/lib/provider-interface.test.js +394 -0
- package/server/lib/provider-queue.js +158 -0
- package/server/lib/provider-queue.test.js +315 -0
- package/server/lib/router-config.js +221 -0
- package/server/lib/router-config.test.js +237 -0
- package/server/lib/router-setup-command.js +419 -0
- package/server/lib/router-setup-command.test.js +375 -0
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
2
|
+
import fs from 'fs/promises';
|
|
3
|
+
import {
|
|
4
|
+
loadSchema,
|
|
5
|
+
validateOutput,
|
|
6
|
+
buildPromptWithSchema,
|
|
7
|
+
schemaToPromptInstructions,
|
|
8
|
+
BUILTIN_SCHEMAS,
|
|
9
|
+
} from './output-schemas.js';
|
|
10
|
+
|
|
11
|
+
// Mock fs/promises
|
|
12
|
+
vi.mock('fs/promises');
|
|
13
|
+
|
|
14
|
+
describe('output-schemas', () => {
|
|
15
|
+
beforeEach(() => {
|
|
16
|
+
vi.clearAllMocks();
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
describe('loadSchema', () => {
|
|
20
|
+
it('reads schema from file', async () => {
|
|
21
|
+
const schema = {
|
|
22
|
+
type: 'object',
|
|
23
|
+
properties: { result: { type: 'string' } },
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
fs.readFile.mockResolvedValue(JSON.stringify(schema));
|
|
27
|
+
|
|
28
|
+
const loaded = await loadSchema('/.tlc/schemas/test-schema.json');
|
|
29
|
+
|
|
30
|
+
expect(loaded).toEqual(schema);
|
|
31
|
+
expect(fs.readFile).toHaveBeenCalledWith('/.tlc/schemas/test-schema.json', 'utf8');
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it('returns parsed JSON', async () => {
|
|
35
|
+
const schema = {
|
|
36
|
+
type: 'object',
|
|
37
|
+
properties: {
|
|
38
|
+
summary: { type: 'string' },
|
|
39
|
+
score: { type: 'number' },
|
|
40
|
+
},
|
|
41
|
+
required: ['summary'],
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
fs.readFile.mockResolvedValue(JSON.stringify(schema));
|
|
45
|
+
|
|
46
|
+
const loaded = await loadSchema('/.tlc/schemas/review.json');
|
|
47
|
+
|
|
48
|
+
expect(loaded.type).toBe('object');
|
|
49
|
+
expect(loaded.properties.summary.type).toBe('string');
|
|
50
|
+
expect(loaded.required).toContain('summary');
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it('handles missing schema file', async () => {
|
|
54
|
+
fs.readFile.mockRejectedValue(new Error('ENOENT: no such file'));
|
|
55
|
+
|
|
56
|
+
await expect(loadSchema('/.tlc/schemas/missing.json'))
|
|
57
|
+
.rejects.toThrow(/ENOENT|no such file/i);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it('handles invalid JSON', async () => {
|
|
61
|
+
fs.readFile.mockResolvedValue('not valid json {');
|
|
62
|
+
|
|
63
|
+
await expect(loadSchema('/.tlc/schemas/invalid.json'))
|
|
64
|
+
.rejects.toThrow();
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
describe('validateOutput', () => {
|
|
69
|
+
const schema = {
|
|
70
|
+
type: 'object',
|
|
71
|
+
properties: {
|
|
72
|
+
summary: { type: 'string' },
|
|
73
|
+
score: { type: 'integer', minimum: 0, maximum: 100 },
|
|
74
|
+
approved: { type: 'boolean' },
|
|
75
|
+
},
|
|
76
|
+
required: ['summary', 'score', 'approved'],
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
it('passes valid data', () => {
|
|
80
|
+
const data = {
|
|
81
|
+
summary: 'Code looks good',
|
|
82
|
+
score: 85,
|
|
83
|
+
approved: true,
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
const result = validateOutput(data, schema);
|
|
87
|
+
|
|
88
|
+
expect(result.valid).toBe(true);
|
|
89
|
+
expect(result.errors).toEqual([]);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it('rejects invalid data', () => {
|
|
93
|
+
const data = {
|
|
94
|
+
summary: 'Code looks good',
|
|
95
|
+
score: 'not a number', // Should be integer
|
|
96
|
+
approved: true,
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
const result = validateOutput(data, schema);
|
|
100
|
+
|
|
101
|
+
expect(result.valid).toBe(false);
|
|
102
|
+
expect(result.errors.length).toBeGreaterThan(0);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it('rejects missing required fields', () => {
|
|
106
|
+
const data = {
|
|
107
|
+
summary: 'Code looks good',
|
|
108
|
+
// Missing score and approved
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
const result = validateOutput(data, schema);
|
|
112
|
+
|
|
113
|
+
expect(result.valid).toBe(false);
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it('validates nested objects', () => {
|
|
117
|
+
const nestedSchema = {
|
|
118
|
+
type: 'object',
|
|
119
|
+
properties: {
|
|
120
|
+
issue: {
|
|
121
|
+
type: 'object',
|
|
122
|
+
properties: {
|
|
123
|
+
severity: { type: 'string', enum: ['critical', 'moderate', 'suggestion'] },
|
|
124
|
+
line: { type: 'integer' },
|
|
125
|
+
},
|
|
126
|
+
required: ['severity'],
|
|
127
|
+
},
|
|
128
|
+
},
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
const validData = {
|
|
132
|
+
issue: { severity: 'critical', line: 42 },
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
const invalidData = {
|
|
136
|
+
issue: { severity: 'invalid-severity' },
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
expect(validateOutput(validData, nestedSchema).valid).toBe(true);
|
|
140
|
+
expect(validateOutput(invalidData, nestedSchema).valid).toBe(false);
|
|
141
|
+
});
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
describe('BUILTIN_SCHEMAS', () => {
|
|
145
|
+
it('has review-result schema', () => {
|
|
146
|
+
const schema = BUILTIN_SCHEMAS['review-result'];
|
|
147
|
+
|
|
148
|
+
expect(schema).toBeDefined();
|
|
149
|
+
expect(schema.properties.summary).toBeDefined();
|
|
150
|
+
expect(schema.properties.issues).toBeDefined();
|
|
151
|
+
expect(schema.properties.score).toBeDefined();
|
|
152
|
+
expect(schema.properties.approved).toBeDefined();
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
it('review-result has required fields', () => {
|
|
156
|
+
const schema = BUILTIN_SCHEMAS['review-result'];
|
|
157
|
+
|
|
158
|
+
expect(schema.required).toContain('summary');
|
|
159
|
+
expect(schema.required).toContain('issues');
|
|
160
|
+
expect(schema.required).toContain('score');
|
|
161
|
+
expect(schema.required).toContain('approved');
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
it('has design-result schema', () => {
|
|
165
|
+
const schema = BUILTIN_SCHEMAS['design-result'];
|
|
166
|
+
|
|
167
|
+
expect(schema).toBeDefined();
|
|
168
|
+
expect(schema.properties.mockups).toBeDefined();
|
|
169
|
+
expect(schema.properties.rationale).toBeDefined();
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
it('design-result has required fields', () => {
|
|
173
|
+
const schema = BUILTIN_SCHEMAS['design-result'];
|
|
174
|
+
|
|
175
|
+
expect(schema.required).toContain('mockups');
|
|
176
|
+
expect(schema.required).toContain('rationale');
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
it('has code-result schema', () => {
|
|
180
|
+
const schema = BUILTIN_SCHEMAS['code-result'];
|
|
181
|
+
|
|
182
|
+
expect(schema).toBeDefined();
|
|
183
|
+
expect(schema.properties.files).toBeDefined();
|
|
184
|
+
expect(schema.properties.explanation).toBeDefined();
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
it('code-result has required fields', () => {
|
|
188
|
+
const schema = BUILTIN_SCHEMAS['code-result'];
|
|
189
|
+
|
|
190
|
+
expect(schema.required).toContain('files');
|
|
191
|
+
expect(schema.required).toContain('explanation');
|
|
192
|
+
});
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
describe('buildPromptWithSchema', () => {
|
|
196
|
+
it('injects schema into prompt', () => {
|
|
197
|
+
const prompt = 'Review this code';
|
|
198
|
+
const schema = {
|
|
199
|
+
type: 'object',
|
|
200
|
+
properties: { result: { type: 'string' } },
|
|
201
|
+
};
|
|
202
|
+
|
|
203
|
+
const result = buildPromptWithSchema(prompt, schema);
|
|
204
|
+
|
|
205
|
+
expect(result).toContain('Review this code');
|
|
206
|
+
expect(result).toContain('JSON');
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
it('includes schema structure', () => {
|
|
210
|
+
const prompt = 'Analyze';
|
|
211
|
+
const schema = {
|
|
212
|
+
type: 'object',
|
|
213
|
+
properties: {
|
|
214
|
+
summary: { type: 'string' },
|
|
215
|
+
score: { type: 'integer' },
|
|
216
|
+
},
|
|
217
|
+
required: ['summary'],
|
|
218
|
+
};
|
|
219
|
+
|
|
220
|
+
const result = buildPromptWithSchema(prompt, schema);
|
|
221
|
+
|
|
222
|
+
expect(result).toContain('summary');
|
|
223
|
+
expect(result).toContain('score');
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
it('returns original prompt when no schema', () => {
|
|
227
|
+
const prompt = 'Just a prompt';
|
|
228
|
+
|
|
229
|
+
const result = buildPromptWithSchema(prompt, null);
|
|
230
|
+
|
|
231
|
+
expect(result).toBe(prompt);
|
|
232
|
+
});
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
describe('schemaToPromptInstructions', () => {
|
|
236
|
+
it('creates text instructions from schema', () => {
|
|
237
|
+
const schema = {
|
|
238
|
+
type: 'object',
|
|
239
|
+
properties: {
|
|
240
|
+
summary: { type: 'string', description: 'A brief summary' },
|
|
241
|
+
score: { type: 'integer', minimum: 0, maximum: 100 },
|
|
242
|
+
},
|
|
243
|
+
required: ['summary', 'score'],
|
|
244
|
+
};
|
|
245
|
+
|
|
246
|
+
const instructions = schemaToPromptInstructions(schema);
|
|
247
|
+
|
|
248
|
+
expect(instructions).toContain('summary');
|
|
249
|
+
expect(instructions).toContain('score');
|
|
250
|
+
expect(instructions).toContain('required');
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
it('handles nested objects', () => {
|
|
254
|
+
const schema = {
|
|
255
|
+
type: 'object',
|
|
256
|
+
properties: {
|
|
257
|
+
data: {
|
|
258
|
+
type: 'object',
|
|
259
|
+
properties: {
|
|
260
|
+
value: { type: 'string' },
|
|
261
|
+
},
|
|
262
|
+
},
|
|
263
|
+
},
|
|
264
|
+
};
|
|
265
|
+
|
|
266
|
+
const instructions = schemaToPromptInstructions(schema);
|
|
267
|
+
|
|
268
|
+
expect(instructions).toContain('data');
|
|
269
|
+
expect(instructions).toContain('value');
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
it('handles arrays', () => {
|
|
273
|
+
const schema = {
|
|
274
|
+
type: 'object',
|
|
275
|
+
properties: {
|
|
276
|
+
items: {
|
|
277
|
+
type: 'array',
|
|
278
|
+
items: { type: 'string' },
|
|
279
|
+
},
|
|
280
|
+
},
|
|
281
|
+
};
|
|
282
|
+
|
|
283
|
+
const instructions = schemaToPromptInstructions(schema);
|
|
284
|
+
|
|
285
|
+
expect(instructions).toContain('items');
|
|
286
|
+
expect(instructions).toContain('array');
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
it('includes enum values', () => {
|
|
290
|
+
const schema = {
|
|
291
|
+
type: 'object',
|
|
292
|
+
properties: {
|
|
293
|
+
status: {
|
|
294
|
+
type: 'string',
|
|
295
|
+
enum: ['pending', 'approved', 'rejected'],
|
|
296
|
+
},
|
|
297
|
+
},
|
|
298
|
+
};
|
|
299
|
+
|
|
300
|
+
const instructions = schemaToPromptInstructions(schema);
|
|
301
|
+
|
|
302
|
+
expect(instructions).toContain('pending');
|
|
303
|
+
expect(instructions).toContain('approved');
|
|
304
|
+
expect(instructions).toContain('rejected');
|
|
305
|
+
});
|
|
306
|
+
});
|
|
307
|
+
});
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Provider Interface - Unified interface for CLI and API providers
|
|
3
|
+
*
|
|
4
|
+
* Supports two provider types:
|
|
5
|
+
* - CLI: Wraps AI coding CLI tools (claude, codex, gemini)
|
|
6
|
+
* - API: Direct REST calls to OpenAI-compatible endpoints
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Provider types enum
|
|
11
|
+
*/
|
|
12
|
+
export const PROVIDER_TYPES = {
|
|
13
|
+
CLI: 'cli',
|
|
14
|
+
API: 'api',
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Validate provider configuration
|
|
19
|
+
* @param {Object} config - Provider configuration
|
|
20
|
+
* @throws {Error} If configuration is invalid
|
|
21
|
+
*/
|
|
22
|
+
export function validateConfig(config) {
|
|
23
|
+
if (!config.name) {
|
|
24
|
+
throw new Error('name is required');
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
if (!config.type) {
|
|
28
|
+
throw new Error('type is required');
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (config.type !== PROVIDER_TYPES.CLI && config.type !== PROVIDER_TYPES.API) {
|
|
32
|
+
throw new Error(`Invalid provider type: ${config.type}. Must be 'cli' or 'api'`);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (config.type === PROVIDER_TYPES.CLI && !config.command) {
|
|
36
|
+
throw new Error('command is required for CLI providers');
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (config.type === PROVIDER_TYPES.API && !config.baseUrl) {
|
|
40
|
+
throw new Error('baseUrl is required for API providers');
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Create a ProviderResult object
|
|
46
|
+
* @param {Object} data - Result data
|
|
47
|
+
* @param {string} data.raw - Raw output string
|
|
48
|
+
* @param {any} data.parsed - Parsed output (typically JSON)
|
|
49
|
+
* @param {number} data.exitCode - Exit code (0 for success)
|
|
50
|
+
* @param {Object} [data.tokenUsage] - Token usage statistics
|
|
51
|
+
* @param {number} [data.cost] - Cost in USD
|
|
52
|
+
* @returns {Object} ProviderResult
|
|
53
|
+
*/
|
|
54
|
+
export function createProviderResult({ raw, parsed, exitCode, tokenUsage = null, cost = null }) {
|
|
55
|
+
return {
|
|
56
|
+
raw,
|
|
57
|
+
parsed,
|
|
58
|
+
exitCode,
|
|
59
|
+
tokenUsage,
|
|
60
|
+
cost,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Calculate cost from token usage
|
|
66
|
+
* @param {Object} tokenUsage - Token usage { input, output }
|
|
67
|
+
* @param {Object} pricing - Pricing { input, output } per 1K tokens
|
|
68
|
+
* @returns {number} Cost in USD
|
|
69
|
+
*/
|
|
70
|
+
function calculateCost(tokenUsage, pricing) {
|
|
71
|
+
if (!tokenUsage || !pricing) return null;
|
|
72
|
+
|
|
73
|
+
const inputCost = (tokenUsage.input * pricing.input) / 1000;
|
|
74
|
+
const outputCost = (tokenUsage.output * pricing.output) / 1000;
|
|
75
|
+
|
|
76
|
+
return inputCost + outputCost;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Create a provider instance
|
|
81
|
+
* @param {Object} config - Provider configuration
|
|
82
|
+
* @param {string} config.name - Provider name
|
|
83
|
+
* @param {string} config.type - Provider type ('cli' or 'api')
|
|
84
|
+
* @param {string} [config.command] - CLI command (required for CLI type)
|
|
85
|
+
* @param {string} [config.baseUrl] - API base URL (required for API type)
|
|
86
|
+
* @param {string} [config.model] - Model identifier
|
|
87
|
+
* @param {string[]} [config.capabilities] - List of capabilities
|
|
88
|
+
* @param {string[]} [config.headlessArgs] - Headless mode arguments
|
|
89
|
+
* @param {Object} [config.pricing] - Token pricing { input, output }
|
|
90
|
+
* @param {Function} [config.runner] - Custom runner function (for testing)
|
|
91
|
+
* @returns {Object} Provider instance
|
|
92
|
+
*/
|
|
93
|
+
export function createProvider(config) {
|
|
94
|
+
// Validate
|
|
95
|
+
if (!config.name) {
|
|
96
|
+
throw new Error('name is required');
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (config.type !== PROVIDER_TYPES.CLI && config.type !== PROVIDER_TYPES.API) {
|
|
100
|
+
throw new Error(`Invalid provider type: ${config.type}. Must be 'cli' or 'api'`);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Freeze capabilities to make them immutable
|
|
104
|
+
const capabilities = Object.freeze([...(config.capabilities || [])]);
|
|
105
|
+
|
|
106
|
+
// Default runner that throws (should be overridden)
|
|
107
|
+
const defaultRunner = async () => {
|
|
108
|
+
throw new Error('Provider runner not implemented');
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
const runner = config.runner || defaultRunner;
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Run a prompt through the provider
|
|
115
|
+
* @param {string} prompt - The prompt to send
|
|
116
|
+
* @param {Object} opts - Run options
|
|
117
|
+
* @param {string} [opts.outputFormat] - Output format ('json' or 'text')
|
|
118
|
+
* @param {string} [opts.sandbox] - Sandbox mode
|
|
119
|
+
* @param {Object} [opts.outputSchema] - JSON schema for output
|
|
120
|
+
* @param {string} [opts.cwd] - Working directory
|
|
121
|
+
* @returns {Promise<Object>} ProviderResult
|
|
122
|
+
*/
|
|
123
|
+
async function run(prompt, opts = {}) {
|
|
124
|
+
const result = await runner(prompt, opts);
|
|
125
|
+
|
|
126
|
+
// Calculate cost if we have token usage and pricing
|
|
127
|
+
if (result.tokenUsage && config.pricing) {
|
|
128
|
+
result.cost = calculateCost(result.tokenUsage, config.pricing);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
return createProviderResult({
|
|
132
|
+
raw: result.raw,
|
|
133
|
+
parsed: result.parsed,
|
|
134
|
+
exitCode: result.exitCode,
|
|
135
|
+
tokenUsage: result.tokenUsage || null,
|
|
136
|
+
cost: result.cost || null,
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return {
|
|
141
|
+
name: config.name,
|
|
142
|
+
type: config.type,
|
|
143
|
+
command: config.command,
|
|
144
|
+
baseUrl: config.baseUrl,
|
|
145
|
+
model: config.model,
|
|
146
|
+
headlessArgs: config.headlessArgs,
|
|
147
|
+
pricing: config.pricing,
|
|
148
|
+
detected: config.detected || false,
|
|
149
|
+
devserverOnly: config.devserverOnly,
|
|
150
|
+
capabilities,
|
|
151
|
+
run,
|
|
152
|
+
};
|
|
153
|
+
}
|