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.
Files changed (91) hide show
  1. package/dashboard/dist/App.js +229 -35
  2. package/dashboard/dist/components/AgentRegistryPane.d.ts +35 -0
  3. package/dashboard/dist/components/AgentRegistryPane.js +89 -0
  4. package/dashboard/dist/components/AgentRegistryPane.test.d.ts +1 -0
  5. package/dashboard/dist/components/AgentRegistryPane.test.js +200 -0
  6. package/dashboard/dist/components/RouterPane.d.ts +5 -0
  7. package/dashboard/dist/components/RouterPane.js +65 -0
  8. package/dashboard/dist/components/RouterPane.test.d.ts +1 -0
  9. package/dashboard/dist/components/RouterPane.test.js +176 -0
  10. package/dashboard/dist/components/accessibility.test.d.ts +1 -0
  11. package/dashboard/dist/components/accessibility.test.js +116 -0
  12. package/dashboard/dist/components/layout/MobileNav.d.ts +16 -0
  13. package/dashboard/dist/components/layout/MobileNav.js +31 -0
  14. package/dashboard/dist/components/layout/MobileNav.test.d.ts +1 -0
  15. package/dashboard/dist/components/layout/MobileNav.test.js +111 -0
  16. package/dashboard/dist/components/performance.test.d.ts +1 -0
  17. package/dashboard/dist/components/performance.test.js +114 -0
  18. package/dashboard/dist/components/responsive.test.d.ts +1 -0
  19. package/dashboard/dist/components/responsive.test.js +114 -0
  20. package/dashboard/dist/components/ui/Dropdown.d.ts +22 -0
  21. package/dashboard/dist/components/ui/Dropdown.js +109 -0
  22. package/dashboard/dist/components/ui/Dropdown.test.d.ts +1 -0
  23. package/dashboard/dist/components/ui/Dropdown.test.js +105 -0
  24. package/dashboard/dist/components/ui/Modal.d.ts +13 -0
  25. package/dashboard/dist/components/ui/Modal.js +25 -0
  26. package/dashboard/dist/components/ui/Modal.test.d.ts +1 -0
  27. package/dashboard/dist/components/ui/Modal.test.js +91 -0
  28. package/dashboard/dist/components/ui/Skeleton.d.ts +32 -0
  29. package/dashboard/dist/components/ui/Skeleton.js +48 -0
  30. package/dashboard/dist/components/ui/Skeleton.test.d.ts +1 -0
  31. package/dashboard/dist/components/ui/Skeleton.test.js +125 -0
  32. package/dashboard/dist/components/ui/Toast.d.ts +32 -0
  33. package/dashboard/dist/components/ui/Toast.js +21 -0
  34. package/dashboard/dist/components/ui/Toast.test.d.ts +1 -0
  35. package/dashboard/dist/components/ui/Toast.test.js +118 -0
  36. package/dashboard/dist/hooks/useTheme.d.ts +37 -0
  37. package/dashboard/dist/hooks/useTheme.js +96 -0
  38. package/dashboard/dist/hooks/useTheme.test.d.ts +1 -0
  39. package/dashboard/dist/hooks/useTheme.test.js +94 -0
  40. package/dashboard/dist/hooks/useWebSocket.d.ts +17 -0
  41. package/dashboard/dist/hooks/useWebSocket.js +100 -0
  42. package/dashboard/dist/hooks/useWebSocket.test.d.ts +1 -0
  43. package/dashboard/dist/hooks/useWebSocket.test.js +115 -0
  44. package/dashboard/dist/stores/projectStore.d.ts +44 -0
  45. package/dashboard/dist/stores/projectStore.js +76 -0
  46. package/dashboard/dist/stores/projectStore.test.d.ts +1 -0
  47. package/dashboard/dist/stores/projectStore.test.js +114 -0
  48. package/dashboard/dist/stores/uiStore.d.ts +29 -0
  49. package/dashboard/dist/stores/uiStore.js +72 -0
  50. package/dashboard/dist/stores/uiStore.test.d.ts +1 -0
  51. package/dashboard/dist/stores/uiStore.test.js +93 -0
  52. package/dashboard/package.json +3 -3
  53. package/docker-compose.dev.yml +6 -1
  54. package/package.json +5 -2
  55. package/server/dashboard/index.html +1336 -779
  56. package/server/index.js +178 -0
  57. package/server/lib/agent-cleanup.js +177 -0
  58. package/server/lib/agent-cleanup.test.js +359 -0
  59. package/server/lib/agent-hooks.js +126 -0
  60. package/server/lib/agent-hooks.test.js +303 -0
  61. package/server/lib/agent-metadata.js +179 -0
  62. package/server/lib/agent-metadata.test.js +383 -0
  63. package/server/lib/agent-persistence.js +191 -0
  64. package/server/lib/agent-persistence.test.js +475 -0
  65. package/server/lib/agent-registry-command.js +340 -0
  66. package/server/lib/agent-registry-command.test.js +334 -0
  67. package/server/lib/agent-registry.js +155 -0
  68. package/server/lib/agent-registry.test.js +239 -0
  69. package/server/lib/agent-state.js +236 -0
  70. package/server/lib/agent-state.test.js +375 -0
  71. package/server/lib/api-provider.js +186 -0
  72. package/server/lib/api-provider.test.js +336 -0
  73. package/server/lib/cli-detector.js +166 -0
  74. package/server/lib/cli-detector.test.js +269 -0
  75. package/server/lib/cli-provider.js +212 -0
  76. package/server/lib/cli-provider.test.js +349 -0
  77. package/server/lib/debug.test.js +62 -0
  78. package/server/lib/devserver-router-api.js +249 -0
  79. package/server/lib/devserver-router-api.test.js +426 -0
  80. package/server/lib/model-router.js +245 -0
  81. package/server/lib/model-router.test.js +313 -0
  82. package/server/lib/output-schemas.js +269 -0
  83. package/server/lib/output-schemas.test.js +307 -0
  84. package/server/lib/provider-interface.js +153 -0
  85. package/server/lib/provider-interface.test.js +394 -0
  86. package/server/lib/provider-queue.js +158 -0
  87. package/server/lib/provider-queue.test.js +315 -0
  88. package/server/lib/router-config.js +221 -0
  89. package/server/lib/router-config.test.js +237 -0
  90. package/server/lib/router-setup-command.js +419 -0
  91. 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
+ }