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,269 @@
1
+ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
2
+ import {
3
+ detectCLI,
4
+ detectAllCLIs,
5
+ clearCache,
6
+ getCapabilities,
7
+ CLI_TOOLS,
8
+ } from './cli-detector.js';
9
+
10
+ // Mock child_process
11
+ vi.mock('child_process', () => ({
12
+ execSync: vi.fn(),
13
+ exec: vi.fn(),
14
+ }));
15
+
16
+ import { execSync } from 'child_process';
17
+
18
+ describe('cli-detector', () => {
19
+ beforeEach(() => {
20
+ vi.clearAllMocks();
21
+ clearCache();
22
+ });
23
+
24
+ describe('detectCLI', () => {
25
+ it('finds claude when installed', async () => {
26
+ execSync.mockImplementation((cmd) => {
27
+ if (cmd.includes('which claude') || cmd.includes('where claude')) {
28
+ return Buffer.from('/usr/local/bin/claude\n');
29
+ }
30
+ if (cmd.includes('--version')) {
31
+ return Buffer.from('claude v4.2.1\n');
32
+ }
33
+ throw new Error('Command not found');
34
+ });
35
+
36
+ const result = await detectCLI('claude');
37
+
38
+ expect(result).not.toBeNull();
39
+ expect(result.name).toBe('claude');
40
+ expect(result.path).toContain('claude');
41
+ });
42
+
43
+ it('returns null when CLI not installed', async () => {
44
+ execSync.mockImplementation(() => {
45
+ throw new Error('Command not found');
46
+ });
47
+
48
+ const result = await detectCLI('claude');
49
+
50
+ expect(result).toBeNull();
51
+ });
52
+
53
+ it('gets version string', async () => {
54
+ execSync.mockImplementation((cmd) => {
55
+ if (cmd.includes('which') || cmd.includes('where')) {
56
+ return Buffer.from('/usr/local/bin/claude\n');
57
+ }
58
+ if (cmd.includes('--version')) {
59
+ return Buffer.from('claude v4.2.1\n');
60
+ }
61
+ return Buffer.from('');
62
+ });
63
+
64
+ const result = await detectCLI('claude');
65
+
66
+ expect(result.version).toBe('v4.2.1');
67
+ });
68
+
69
+ it('detects codex CLI', async () => {
70
+ execSync.mockImplementation((cmd) => {
71
+ if (cmd.includes('which codex') || cmd.includes('where codex')) {
72
+ return Buffer.from('/usr/local/bin/codex\n');
73
+ }
74
+ if (cmd.includes('--version')) {
75
+ return Buffer.from('codex 1.3.0\n');
76
+ }
77
+ throw new Error('not found');
78
+ });
79
+
80
+ const result = await detectCLI('codex');
81
+
82
+ expect(result).not.toBeNull();
83
+ expect(result.name).toBe('codex');
84
+ });
85
+
86
+ it('detects gemini CLI', async () => {
87
+ execSync.mockImplementation((cmd) => {
88
+ if (cmd.includes('which gemini') || cmd.includes('where gemini')) {
89
+ return Buffer.from('/usr/local/bin/gemini\n');
90
+ }
91
+ if (cmd.includes('--version')) {
92
+ return Buffer.from('gemini 0.9.2\n');
93
+ }
94
+ throw new Error('not found');
95
+ });
96
+
97
+ const result = await detectCLI('gemini');
98
+
99
+ expect(result).not.toBeNull();
100
+ expect(result.name).toBe('gemini');
101
+ });
102
+ });
103
+
104
+ describe('detectAllCLIs', () => {
105
+ it('returns map of detected CLIs', async () => {
106
+ execSync.mockImplementation((cmd) => {
107
+ if (cmd.includes('claude')) {
108
+ if (cmd.includes('which') || cmd.includes('where')) {
109
+ return Buffer.from('/usr/local/bin/claude\n');
110
+ }
111
+ return Buffer.from('v4.2.1\n');
112
+ }
113
+ throw new Error('not found');
114
+ });
115
+
116
+ const result = await detectAllCLIs();
117
+
118
+ expect(result).toBeInstanceOf(Map);
119
+ expect(result.has('claude')).toBe(true);
120
+ });
121
+
122
+ it('caches results', async () => {
123
+ let callCount = 0;
124
+ execSync.mockImplementation((cmd) => {
125
+ callCount++;
126
+ if (cmd.includes('which') || cmd.includes('where')) {
127
+ return Buffer.from('/path/to/cli\n');
128
+ }
129
+ return Buffer.from('v1.0.0\n');
130
+ });
131
+
132
+ await detectAllCLIs();
133
+ const initialCount = callCount;
134
+
135
+ await detectAllCLIs();
136
+
137
+ // Should not have called execSync again
138
+ expect(callCount).toBe(initialCount);
139
+ });
140
+ });
141
+
142
+ describe('clearCache', () => {
143
+ it('forces re-detection', async () => {
144
+ let callCount = 0;
145
+ execSync.mockImplementation((cmd) => {
146
+ callCount++;
147
+ if (cmd.includes('which') || cmd.includes('where')) {
148
+ return Buffer.from('/path/to/cli\n');
149
+ }
150
+ return Buffer.from('v1.0.0\n');
151
+ });
152
+
153
+ await detectAllCLIs();
154
+ const countAfterFirst = callCount;
155
+
156
+ clearCache();
157
+ await detectAllCLIs();
158
+
159
+ // Should have called again after cache clear
160
+ expect(callCount).toBeGreaterThan(countAfterFirst);
161
+ });
162
+ });
163
+
164
+ describe('getCapabilities', () => {
165
+ it('returns CLI capabilities for claude', () => {
166
+ const caps = getCapabilities('claude');
167
+
168
+ expect(caps).toContain('review');
169
+ expect(caps).toContain('code-gen');
170
+ expect(caps).toContain('refactor');
171
+ });
172
+
173
+ it('returns CLI capabilities for codex', () => {
174
+ const caps = getCapabilities('codex');
175
+
176
+ expect(caps).toContain('review');
177
+ expect(caps).toContain('code-gen');
178
+ });
179
+
180
+ it('returns CLI capabilities for gemini', () => {
181
+ const caps = getCapabilities('gemini');
182
+
183
+ expect(caps).toContain('design');
184
+ expect(caps).toContain('vision');
185
+ expect(caps).toContain('review');
186
+ });
187
+
188
+ it('returns empty array for unknown CLI', () => {
189
+ const caps = getCapabilities('unknown');
190
+
191
+ expect(caps).toEqual([]);
192
+ });
193
+ });
194
+
195
+ describe('CLI_TOOLS', () => {
196
+ it('exports claude tool config', () => {
197
+ expect(CLI_TOOLS.claude).toBeDefined();
198
+ expect(CLI_TOOLS.claude.command).toBe('claude');
199
+ });
200
+
201
+ it('exports codex tool config', () => {
202
+ expect(CLI_TOOLS.codex).toBeDefined();
203
+ expect(CLI_TOOLS.codex.command).toBe('codex');
204
+ });
205
+
206
+ it('exports gemini tool config', () => {
207
+ expect(CLI_TOOLS.gemini).toBeDefined();
208
+ expect(CLI_TOOLS.gemini.command).toBe('gemini');
209
+ });
210
+
211
+ it('includes headless args for each tool', () => {
212
+ expect(CLI_TOOLS.claude.headlessArgs).toBeDefined();
213
+ expect(CLI_TOOLS.codex.headlessArgs).toBeDefined();
214
+ expect(CLI_TOOLS.gemini.headlessArgs).toBeDefined();
215
+ });
216
+ });
217
+
218
+ describe('Windows compatibility', () => {
219
+ it('handles Windows command extensions', async () => {
220
+ // Simulate Windows where 'where' is used instead of 'which'
221
+ const originalPlatform = process.platform;
222
+ Object.defineProperty(process, 'platform', { value: 'win32' });
223
+
224
+ execSync.mockImplementation((cmd) => {
225
+ if (cmd.includes('where')) {
226
+ return Buffer.from('C:\\Program Files\\claude\\claude.exe\n');
227
+ }
228
+ return Buffer.from('v1.0.0\n');
229
+ });
230
+
231
+ const result = await detectCLI('claude');
232
+
233
+ // Restore
234
+ Object.defineProperty(process, 'platform', { value: originalPlatform });
235
+
236
+ expect(result).not.toBeNull();
237
+ });
238
+ });
239
+
240
+ describe('PATH variations', () => {
241
+ it('handles CLI in non-standard paths', async () => {
242
+ execSync.mockImplementation((cmd) => {
243
+ if (cmd.includes('which') || cmd.includes('where')) {
244
+ return Buffer.from('/opt/custom/bin/claude\n');
245
+ }
246
+ return Buffer.from('v4.0.0\n');
247
+ });
248
+
249
+ const result = await detectCLI('claude');
250
+
251
+ expect(result.path).toBe('/opt/custom/bin/claude');
252
+ });
253
+ });
254
+
255
+ describe('timeout handling', () => {
256
+ it('handles slow detection with timeout', async () => {
257
+ execSync.mockImplementation(() => {
258
+ // Simulate timeout by throwing
259
+ const error = new Error('Command timed out');
260
+ error.code = 'ETIMEDOUT';
261
+ throw error;
262
+ });
263
+
264
+ const result = await detectCLI('claude');
265
+
266
+ expect(result).toBeNull();
267
+ });
268
+ });
269
+ });
@@ -0,0 +1,212 @@
1
+ /**
2
+ * CLI Provider - Provider implementation for CLI tools
3
+ *
4
+ * Supports running AI CLI tools locally or via devserver:
5
+ * - claude (Claude Code)
6
+ * - codex (Codex CLI)
7
+ * - gemini (Gemini CLI)
8
+ */
9
+
10
+ import { spawn } from 'child_process';
11
+ import { createProvider, PROVIDER_TYPES } from './provider-interface.js';
12
+
13
+ /**
14
+ * Parse output, trying to extract JSON
15
+ * @param {string} output - Raw output string
16
+ * @returns {Object|null} Parsed JSON or null
17
+ */
18
+ export function parseOutput(output) {
19
+ if (!output || output.trim() === '') {
20
+ return null;
21
+ }
22
+
23
+ // Try parsing the whole thing as JSON
24
+ try {
25
+ return JSON.parse(output);
26
+ } catch (e) {
27
+ // Try to find JSON in the output
28
+ const jsonMatch = output.match(/\{[\s\S]*\}/);
29
+ if (jsonMatch) {
30
+ try {
31
+ return JSON.parse(jsonMatch[0]);
32
+ } catch (e2) {
33
+ return null;
34
+ }
35
+ }
36
+ return null;
37
+ }
38
+ }
39
+
40
+ /**
41
+ * Build command-line arguments for a CLI tool
42
+ * @param {string} command - CLI command name
43
+ * @param {string} prompt - The prompt
44
+ * @param {Object} opts - Options
45
+ * @returns {string[]} Array of arguments
46
+ */
47
+ export function buildArgs(command, prompt, opts = {}) {
48
+ const args = [...(opts.headlessArgs || [])];
49
+
50
+ // Add the prompt
51
+ args.push(prompt);
52
+
53
+ return args;
54
+ }
55
+
56
+ /**
57
+ * Run a CLI tool locally
58
+ * @param {string} command - CLI command
59
+ * @param {string} prompt - The prompt
60
+ * @param {Object} opts - Options
61
+ * @returns {Promise<Object>} ProviderResult
62
+ */
63
+ export function runLocal(command, prompt, opts = {}) {
64
+ return new Promise((resolve, reject) => {
65
+ const args = buildArgs(command, prompt, opts);
66
+ const timeout = opts.timeout || 120000;
67
+
68
+ let stdout = '';
69
+ let stderr = '';
70
+ let timedOut = false;
71
+
72
+ const proc = spawn(command, args, {
73
+ cwd: opts.cwd,
74
+ env: process.env,
75
+ shell: false,
76
+ });
77
+
78
+ const timeoutId = setTimeout(() => {
79
+ timedOut = true;
80
+ proc.kill('SIGTERM');
81
+ reject(new Error(`CLI timeout after ${timeout}ms`));
82
+ }, timeout);
83
+
84
+ proc.stdout.on('data', (data) => {
85
+ stdout += data.toString();
86
+ });
87
+
88
+ proc.stderr.on('data', (data) => {
89
+ stderr += data.toString();
90
+ });
91
+
92
+ proc.on('close', (code) => {
93
+ clearTimeout(timeoutId);
94
+
95
+ if (timedOut) return;
96
+
97
+ const parsed = parseOutput(stdout);
98
+
99
+ resolve({
100
+ raw: stdout,
101
+ parsed,
102
+ exitCode: code || 0,
103
+ stderr,
104
+ });
105
+ });
106
+
107
+ proc.on('error', (err) => {
108
+ clearTimeout(timeoutId);
109
+ if (!timedOut) {
110
+ reject(err);
111
+ }
112
+ });
113
+ });
114
+ }
115
+
116
+ /**
117
+ * Run a CLI tool via devserver
118
+ * @param {Object} params - Parameters
119
+ * @param {string} params.devserverUrl - Devserver URL
120
+ * @param {string} params.provider - Provider name
121
+ * @param {string} params.prompt - The prompt
122
+ * @param {Object} params.opts - Run options
123
+ * @param {number} [params.pollInterval=1000] - Poll interval in ms
124
+ * @param {number} [params.maxPollTime=300000] - Max poll time in ms
125
+ * @returns {Promise<Object>} ProviderResult
126
+ */
127
+ export async function runViaDevserver({
128
+ devserverUrl,
129
+ provider,
130
+ prompt,
131
+ opts = {},
132
+ pollInterval = 1000,
133
+ maxPollTime = 300000,
134
+ }) {
135
+ // Submit task
136
+ const submitResponse = await fetch(`${devserverUrl}/api/run`, {
137
+ method: 'POST',
138
+ headers: {
139
+ 'Content-Type': 'application/json',
140
+ },
141
+ body: JSON.stringify({
142
+ provider,
143
+ prompt,
144
+ opts,
145
+ }),
146
+ });
147
+
148
+ if (!submitResponse.ok) {
149
+ throw new Error(`Failed to submit task: ${submitResponse.statusText}`);
150
+ }
151
+
152
+ const { taskId } = await submitResponse.json();
153
+
154
+ // Poll for result
155
+ const startTime = Date.now();
156
+
157
+ while (Date.now() - startTime < maxPollTime) {
158
+ const statusResponse = await fetch(`${devserverUrl}/api/task/${taskId}`);
159
+
160
+ if (!statusResponse.ok) {
161
+ throw new Error(`Failed to get task status: ${statusResponse.statusText}`);
162
+ }
163
+
164
+ const status = await statusResponse.json();
165
+
166
+ if (status.status === 'completed') {
167
+ return status.result;
168
+ }
169
+
170
+ if (status.status === 'failed') {
171
+ throw new Error(status.error || 'Task failed');
172
+ }
173
+
174
+ // Wait before polling again
175
+ await new Promise(r => setTimeout(r, pollInterval));
176
+ }
177
+
178
+ throw new Error('Task timed out waiting for devserver response');
179
+ }
180
+
181
+ /**
182
+ * Create a CLI provider instance
183
+ * @param {Object} config - Provider configuration
184
+ * @returns {Object} Provider instance
185
+ */
186
+ export function createCLIProvider(config) {
187
+ const runner = async (prompt, opts) => {
188
+ if (config.detected) {
189
+ return runLocal(config.command, prompt, {
190
+ ...opts,
191
+ headlessArgs: config.headlessArgs,
192
+ });
193
+ }
194
+
195
+ if (config.devserverUrl) {
196
+ return runViaDevserver({
197
+ devserverUrl: config.devserverUrl,
198
+ provider: config.name,
199
+ prompt,
200
+ opts,
201
+ });
202
+ }
203
+
204
+ throw new Error(`CLI ${config.name} not detected and no devserver configured`);
205
+ };
206
+
207
+ return createProvider({
208
+ ...config,
209
+ type: PROVIDER_TYPES.CLI,
210
+ runner,
211
+ });
212
+ }