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,336 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
2
|
+
import {
|
|
3
|
+
createAPIProvider,
|
|
4
|
+
callAPI,
|
|
5
|
+
parseResponse,
|
|
6
|
+
calculateCost,
|
|
7
|
+
API_PRICING,
|
|
8
|
+
} from './api-provider.js';
|
|
9
|
+
|
|
10
|
+
// Mock fetch
|
|
11
|
+
global.fetch = vi.fn();
|
|
12
|
+
|
|
13
|
+
describe('api-provider', () => {
|
|
14
|
+
beforeEach(() => {
|
|
15
|
+
vi.clearAllMocks();
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
describe('createAPIProvider', () => {
|
|
19
|
+
it('creates provider with API type', () => {
|
|
20
|
+
const provider = createAPIProvider({
|
|
21
|
+
name: 'deepseek',
|
|
22
|
+
baseUrl: 'https://api.deepseek.com',
|
|
23
|
+
model: 'deepseek-coder',
|
|
24
|
+
capabilities: ['review'],
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
expect(provider.type).toBe('api');
|
|
28
|
+
expect(provider.name).toBe('deepseek');
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it('sets devserverOnly to true by default', () => {
|
|
32
|
+
const provider = createAPIProvider({
|
|
33
|
+
name: 'deepseek',
|
|
34
|
+
baseUrl: 'https://api.deepseek.com',
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
expect(provider.devserverOnly).toBe(true);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('stores baseUrl and model', () => {
|
|
41
|
+
const provider = createAPIProvider({
|
|
42
|
+
name: 'deepseek',
|
|
43
|
+
baseUrl: 'https://api.deepseek.com',
|
|
44
|
+
model: 'deepseek-coder',
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
expect(provider.baseUrl).toBe('https://api.deepseek.com');
|
|
48
|
+
expect(provider.model).toBe('deepseek-coder');
|
|
49
|
+
});
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
describe('callAPI', () => {
|
|
53
|
+
it('calls baseUrl/v1/chat/completions', async () => {
|
|
54
|
+
global.fetch.mockResolvedValue({
|
|
55
|
+
ok: true,
|
|
56
|
+
json: () => Promise.resolve({
|
|
57
|
+
choices: [{ message: { content: '{"result": "ok"}' } }],
|
|
58
|
+
usage: { prompt_tokens: 100, completion_tokens: 50 },
|
|
59
|
+
}),
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
await callAPI({
|
|
63
|
+
baseUrl: 'https://api.deepseek.com',
|
|
64
|
+
model: 'deepseek-coder',
|
|
65
|
+
prompt: 'test',
|
|
66
|
+
apiKey: 'sk-test',
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
expect(global.fetch).toHaveBeenCalledWith(
|
|
70
|
+
'https://api.deepseek.com/v1/chat/completions',
|
|
71
|
+
expect.any(Object)
|
|
72
|
+
);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it('sets Authorization header', async () => {
|
|
76
|
+
global.fetch.mockResolvedValue({
|
|
77
|
+
ok: true,
|
|
78
|
+
json: () => Promise.resolve({
|
|
79
|
+
choices: [{ message: { content: '{}' } }],
|
|
80
|
+
usage: { prompt_tokens: 100, completion_tokens: 50 },
|
|
81
|
+
}),
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
await callAPI({
|
|
85
|
+
baseUrl: 'https://api.deepseek.com',
|
|
86
|
+
model: 'deepseek-coder',
|
|
87
|
+
prompt: 'test',
|
|
88
|
+
apiKey: 'sk-test-key',
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
expect(global.fetch).toHaveBeenCalledWith(
|
|
92
|
+
expect.any(String),
|
|
93
|
+
expect.objectContaining({
|
|
94
|
+
headers: expect.objectContaining({
|
|
95
|
+
'Authorization': 'Bearer sk-test-key',
|
|
96
|
+
}),
|
|
97
|
+
})
|
|
98
|
+
);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it('sends model in body', async () => {
|
|
102
|
+
global.fetch.mockResolvedValue({
|
|
103
|
+
ok: true,
|
|
104
|
+
json: () => Promise.resolve({
|
|
105
|
+
choices: [{ message: { content: '{}' } }],
|
|
106
|
+
usage: { prompt_tokens: 100, completion_tokens: 50 },
|
|
107
|
+
}),
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
await callAPI({
|
|
111
|
+
baseUrl: 'https://api.deepseek.com',
|
|
112
|
+
model: 'deepseek-coder',
|
|
113
|
+
prompt: 'test',
|
|
114
|
+
apiKey: 'sk-test',
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
const callArgs = global.fetch.mock.calls[0];
|
|
118
|
+
const body = JSON.parse(callArgs[1].body);
|
|
119
|
+
|
|
120
|
+
expect(body.model).toBe('deepseek-coder');
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it('includes response_format when schema provided', async () => {
|
|
124
|
+
const schema = {
|
|
125
|
+
type: 'object',
|
|
126
|
+
properties: {
|
|
127
|
+
result: { type: 'string' },
|
|
128
|
+
},
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
global.fetch.mockResolvedValue({
|
|
132
|
+
ok: true,
|
|
133
|
+
json: () => Promise.resolve({
|
|
134
|
+
choices: [{ message: { content: '{"result": "ok"}' } }],
|
|
135
|
+
usage: { prompt_tokens: 100, completion_tokens: 50 },
|
|
136
|
+
}),
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
await callAPI({
|
|
140
|
+
baseUrl: 'https://api.deepseek.com',
|
|
141
|
+
model: 'deepseek-coder',
|
|
142
|
+
prompt: 'test',
|
|
143
|
+
apiKey: 'sk-test',
|
|
144
|
+
outputSchema: schema,
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
const callArgs = global.fetch.mock.calls[0];
|
|
148
|
+
const body = JSON.parse(callArgs[1].body);
|
|
149
|
+
|
|
150
|
+
expect(body.response_format).toBeDefined();
|
|
151
|
+
expect(body.response_format.type).toBe('json_schema');
|
|
152
|
+
});
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
describe('parseResponse', () => {
|
|
156
|
+
it('extracts content from response', () => {
|
|
157
|
+
const response = {
|
|
158
|
+
choices: [{ message: { content: '{"result": "ok"}' } }],
|
|
159
|
+
usage: { prompt_tokens: 100, completion_tokens: 50 },
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
const result = parseResponse(response);
|
|
163
|
+
|
|
164
|
+
expect(result.raw).toBe('{"result": "ok"}');
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
it('extracts token usage', () => {
|
|
168
|
+
const response = {
|
|
169
|
+
choices: [{ message: { content: '{}' } }],
|
|
170
|
+
usage: { prompt_tokens: 100, completion_tokens: 50 },
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
const result = parseResponse(response);
|
|
174
|
+
|
|
175
|
+
expect(result.tokenUsage).toEqual({ input: 100, output: 50 });
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
it('parses JSON content', () => {
|
|
179
|
+
const response = {
|
|
180
|
+
choices: [{ message: { content: '{"score": 85}' } }],
|
|
181
|
+
usage: { prompt_tokens: 100, completion_tokens: 50 },
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
const result = parseResponse(response);
|
|
185
|
+
|
|
186
|
+
expect(result.parsed).toEqual({ score: 85 });
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
it('handles non-JSON content', () => {
|
|
190
|
+
const response = {
|
|
191
|
+
choices: [{ message: { content: 'Plain text response' } }],
|
|
192
|
+
usage: { prompt_tokens: 100, completion_tokens: 50 },
|
|
193
|
+
};
|
|
194
|
+
|
|
195
|
+
const result = parseResponse(response);
|
|
196
|
+
|
|
197
|
+
expect(result.raw).toBe('Plain text response');
|
|
198
|
+
expect(result.parsed).toBeNull();
|
|
199
|
+
});
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
describe('calculateCost', () => {
|
|
203
|
+
it('uses provider pricing', () => {
|
|
204
|
+
const cost = calculateCost(
|
|
205
|
+
{ input: 1000, output: 500 },
|
|
206
|
+
{ input: 0.001, output: 0.002 }
|
|
207
|
+
);
|
|
208
|
+
|
|
209
|
+
// (1000 * 0.001 + 500 * 0.002) / 1000 = 0.002
|
|
210
|
+
expect(cost).toBeCloseTo(0.002);
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
it('handles zero tokens', () => {
|
|
214
|
+
const cost = calculateCost(
|
|
215
|
+
{ input: 0, output: 0 },
|
|
216
|
+
{ input: 0.001, output: 0.002 }
|
|
217
|
+
);
|
|
218
|
+
|
|
219
|
+
expect(cost).toBe(0);
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
it('returns null when no pricing', () => {
|
|
223
|
+
const cost = calculateCost({ input: 1000, output: 500 }, null);
|
|
224
|
+
|
|
225
|
+
expect(cost).toBeNull();
|
|
226
|
+
});
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
describe('rate limit handling', () => {
|
|
230
|
+
it('retries on rate limit', async () => {
|
|
231
|
+
let attempts = 0;
|
|
232
|
+
global.fetch.mockImplementation(() => {
|
|
233
|
+
attempts++;
|
|
234
|
+
if (attempts === 1) {
|
|
235
|
+
return Promise.resolve({
|
|
236
|
+
ok: false,
|
|
237
|
+
status: 429,
|
|
238
|
+
headers: { get: () => '1' }, // Retry-After
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
return Promise.resolve({
|
|
242
|
+
ok: true,
|
|
243
|
+
json: () => Promise.resolve({
|
|
244
|
+
choices: [{ message: { content: '{}' } }],
|
|
245
|
+
usage: { prompt_tokens: 10, completion_tokens: 5 },
|
|
246
|
+
}),
|
|
247
|
+
});
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
const result = await callAPI({
|
|
251
|
+
baseUrl: 'https://api.example.com',
|
|
252
|
+
model: 'test',
|
|
253
|
+
prompt: 'test',
|
|
254
|
+
apiKey: 'sk-test',
|
|
255
|
+
retryDelay: 10,
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
expect(attempts).toBe(2);
|
|
259
|
+
expect(result.exitCode).toBe(0);
|
|
260
|
+
});
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
describe('error handling', () => {
|
|
264
|
+
it('handles network errors gracefully', async () => {
|
|
265
|
+
global.fetch.mockRejectedValue(new Error('Network error'));
|
|
266
|
+
|
|
267
|
+
const result = await callAPI({
|
|
268
|
+
baseUrl: 'https://api.example.com',
|
|
269
|
+
model: 'test',
|
|
270
|
+
prompt: 'test',
|
|
271
|
+
apiKey: 'sk-test',
|
|
272
|
+
maxRetries: 1,
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
expect(result.exitCode).not.toBe(0);
|
|
276
|
+
expect(result.error).toBeDefined();
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
it('handles API errors', async () => {
|
|
280
|
+
global.fetch.mockResolvedValue({
|
|
281
|
+
ok: false,
|
|
282
|
+
status: 500,
|
|
283
|
+
statusText: 'Internal Server Error',
|
|
284
|
+
json: () => Promise.resolve({ error: { message: 'Server error' } }),
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
const result = await callAPI({
|
|
288
|
+
baseUrl: 'https://api.example.com',
|
|
289
|
+
model: 'test',
|
|
290
|
+
prompt: 'test',
|
|
291
|
+
apiKey: 'sk-test',
|
|
292
|
+
maxRetries: 1,
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
expect(result.exitCode).not.toBe(0);
|
|
296
|
+
});
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
describe('DeepSeek support', () => {
|
|
300
|
+
it('supports DeepSeek endpoint', async () => {
|
|
301
|
+
global.fetch.mockResolvedValue({
|
|
302
|
+
ok: true,
|
|
303
|
+
json: () => Promise.resolve({
|
|
304
|
+
choices: [{ message: { content: '{"review": "LGTM"}' } }],
|
|
305
|
+
usage: { prompt_tokens: 500, completion_tokens: 100 },
|
|
306
|
+
}),
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
const result = await callAPI({
|
|
310
|
+
baseUrl: 'https://api.deepseek.com',
|
|
311
|
+
model: 'deepseek-coder',
|
|
312
|
+
prompt: 'Review this code',
|
|
313
|
+
apiKey: 'sk-deepseek-key',
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
expect(result.parsed).toEqual({ review: 'LGTM' });
|
|
317
|
+
expect(result.tokenUsage).toEqual({ input: 500, output: 100 });
|
|
318
|
+
});
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
describe('API_PRICING', () => {
|
|
322
|
+
it('has pricing for deepseek', () => {
|
|
323
|
+
expect(API_PRICING.deepseek).toBeDefined();
|
|
324
|
+
expect(API_PRICING.deepseek.input).toBeDefined();
|
|
325
|
+
expect(API_PRICING.deepseek.output).toBeDefined();
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
it('has pricing for mistral', () => {
|
|
329
|
+
expect(API_PRICING.mistral).toBeDefined();
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
it('has default pricing', () => {
|
|
333
|
+
expect(API_PRICING.default).toBeDefined();
|
|
334
|
+
});
|
|
335
|
+
});
|
|
336
|
+
});
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CLI Detector - Detects locally installed AI CLI tools
|
|
3
|
+
*
|
|
4
|
+
* Supports detection of:
|
|
5
|
+
* - claude (Claude Code)
|
|
6
|
+
* - codex (Codex CLI)
|
|
7
|
+
* - gemini (Gemini CLI)
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { execSync } from 'child_process';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* CLI tool configurations
|
|
14
|
+
*/
|
|
15
|
+
export const CLI_TOOLS = {
|
|
16
|
+
claude: {
|
|
17
|
+
command: 'claude',
|
|
18
|
+
headlessArgs: ['-p', '--output-format', 'json'],
|
|
19
|
+
capabilities: ['review', 'code-gen', 'refactor', 'explain', 'test-gen'],
|
|
20
|
+
versionFlag: '--version',
|
|
21
|
+
versionParser: (output) => {
|
|
22
|
+
const match = output.match(/v?(\d+\.\d+\.\d+)/);
|
|
23
|
+
return match ? match[0] : output.trim();
|
|
24
|
+
},
|
|
25
|
+
},
|
|
26
|
+
codex: {
|
|
27
|
+
command: 'codex',
|
|
28
|
+
headlessArgs: ['exec', '--json', '--sandbox', 'read-only'],
|
|
29
|
+
capabilities: ['review', 'code-gen', 'refactor', 'explain'],
|
|
30
|
+
versionFlag: '--version',
|
|
31
|
+
versionParser: (output) => {
|
|
32
|
+
const match = output.match(/(\d+\.\d+\.\d+)/);
|
|
33
|
+
return match ? match[0] : output.trim();
|
|
34
|
+
},
|
|
35
|
+
},
|
|
36
|
+
gemini: {
|
|
37
|
+
command: 'gemini',
|
|
38
|
+
headlessArgs: ['-p', '--output-format', 'json'],
|
|
39
|
+
capabilities: ['design', 'image-gen', 'vision', 'review', 'explain'],
|
|
40
|
+
versionFlag: '--version',
|
|
41
|
+
versionParser: (output) => {
|
|
42
|
+
const match = output.match(/(\d+\.\d+\.\d+)/);
|
|
43
|
+
return match ? match[0] : output.trim();
|
|
44
|
+
},
|
|
45
|
+
},
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
// Detection cache
|
|
49
|
+
let detectionCache = null;
|
|
50
|
+
let cacheTimestamp = 0;
|
|
51
|
+
const CACHE_TTL = 60000; // 1 minute
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Get the 'which' command based on platform
|
|
55
|
+
* @returns {string} Command to find executables
|
|
56
|
+
*/
|
|
57
|
+
function getWhichCommand() {
|
|
58
|
+
return process.platform === 'win32' ? 'where' : 'which';
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Execute a command with timeout
|
|
63
|
+
* @param {string} command - Command to execute
|
|
64
|
+
* @param {number} timeout - Timeout in ms
|
|
65
|
+
* @returns {string|null} Output or null on failure
|
|
66
|
+
*/
|
|
67
|
+
function execWithTimeout(command, timeout = 5000) {
|
|
68
|
+
try {
|
|
69
|
+
const result = execSync(command, {
|
|
70
|
+
timeout,
|
|
71
|
+
encoding: 'utf8',
|
|
72
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
73
|
+
});
|
|
74
|
+
return result.toString().trim();
|
|
75
|
+
} catch (err) {
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Detect a single CLI tool
|
|
82
|
+
* @param {string} name - CLI tool name (claude, codex, gemini)
|
|
83
|
+
* @returns {Promise<Object|null>} Detection result or null if not found
|
|
84
|
+
*/
|
|
85
|
+
export async function detectCLI(name) {
|
|
86
|
+
const tool = CLI_TOOLS[name];
|
|
87
|
+
if (!tool) {
|
|
88
|
+
return null;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const whichCmd = getWhichCommand();
|
|
92
|
+
|
|
93
|
+
// Check if command exists
|
|
94
|
+
const path = execWithTimeout(`${whichCmd} ${tool.command}`);
|
|
95
|
+
if (!path) {
|
|
96
|
+
return null;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Get version
|
|
100
|
+
let version = 'unknown';
|
|
101
|
+
try {
|
|
102
|
+
const versionOutput = execWithTimeout(`${tool.command} ${tool.versionFlag}`);
|
|
103
|
+
if (versionOutput) {
|
|
104
|
+
version = tool.versionParser(versionOutput);
|
|
105
|
+
}
|
|
106
|
+
} catch (err) {
|
|
107
|
+
// Version detection failed, but CLI exists
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return {
|
|
111
|
+
name,
|
|
112
|
+
command: tool.command,
|
|
113
|
+
path: path.split('\n')[0].trim(),
|
|
114
|
+
version,
|
|
115
|
+
capabilities: tool.capabilities,
|
|
116
|
+
headlessArgs: tool.headlessArgs,
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Detect all CLI tools
|
|
122
|
+
* @param {boolean} [useCache=true] - Whether to use cached results
|
|
123
|
+
* @returns {Promise<Map<string, Object>>} Map of detected CLIs
|
|
124
|
+
*/
|
|
125
|
+
export async function detectAllCLIs(useCache = true) {
|
|
126
|
+
const now = Date.now();
|
|
127
|
+
|
|
128
|
+
// Return cached results if valid
|
|
129
|
+
if (useCache && detectionCache && (now - cacheTimestamp) < CACHE_TTL) {
|
|
130
|
+
return detectionCache;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const detected = new Map();
|
|
134
|
+
|
|
135
|
+
// Detect each CLI tool
|
|
136
|
+
for (const name of Object.keys(CLI_TOOLS)) {
|
|
137
|
+
const result = await detectCLI(name);
|
|
138
|
+
if (result) {
|
|
139
|
+
detected.set(name, result);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Update cache
|
|
144
|
+
detectionCache = detected;
|
|
145
|
+
cacheTimestamp = now;
|
|
146
|
+
|
|
147
|
+
return detected;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Clear the detection cache
|
|
152
|
+
*/
|
|
153
|
+
export function clearCache() {
|
|
154
|
+
detectionCache = null;
|
|
155
|
+
cacheTimestamp = 0;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Get capabilities for a CLI tool
|
|
160
|
+
* @param {string} name - CLI tool name
|
|
161
|
+
* @returns {string[]} List of capabilities
|
|
162
|
+
*/
|
|
163
|
+
export function getCapabilities(name) {
|
|
164
|
+
const tool = CLI_TOOLS[name];
|
|
165
|
+
return tool ? [...tool.capabilities] : [];
|
|
166
|
+
}
|