tlc-claude-code 1.7.0 → 1.8.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.
@@ -0,0 +1,95 @@
1
+ /**
2
+ * API Adapter
3
+ *
4
+ * HTTP adapter for OpenAI-compatible APIs (LiteLLM, direct).
5
+ *
6
+ * @module llm/adapters/api-adapter
7
+ */
8
+
9
+ const DEFAULT_TIMEOUT = 60000;
10
+
11
+ /**
12
+ * Build HTTP request for OpenAI-compatible API
13
+ * @param {string} prompt - Prompt text
14
+ * @param {Object} options - Adapter options
15
+ * @returns {Object} Request specification
16
+ */
17
+ function buildRequest(prompt, options = {}) {
18
+ const headers = { 'Content-Type': 'application/json' };
19
+
20
+ if (options.apiKey) {
21
+ headers['Authorization'] = 'Bearer ' + options.apiKey;
22
+ }
23
+
24
+ return {
25
+ url: options.url,
26
+ method: 'POST',
27
+ headers,
28
+ body: {
29
+ model: options.model,
30
+ messages: [{ role: 'user', content: prompt }],
31
+ },
32
+ timeout: options.timeout || DEFAULT_TIMEOUT,
33
+ };
34
+ }
35
+
36
+ /**
37
+ * Parse OpenAI-format API response
38
+ * @param {Object|null} data - Response JSON body
39
+ * @param {Object} httpResponse - HTTP response metadata
40
+ * @returns {Object} Parsed response
41
+ */
42
+ function parseApiResponse(data, httpResponse = {}) {
43
+ if (httpResponse.status === 429) {
44
+ return {
45
+ error: 'Rate limited (429)',
46
+ retryable: true,
47
+ response: '',
48
+ };
49
+ }
50
+
51
+ if (!data || !data.choices) {
52
+ return {
53
+ error: 'Invalid API response',
54
+ retryable: false,
55
+ response: '',
56
+ };
57
+ }
58
+
59
+ const content = data.choices[0]?.message?.content || '';
60
+
61
+ return {
62
+ response: content,
63
+ model: data.model || 'unknown',
64
+ tokens: data.usage?.total_tokens || 0,
65
+ };
66
+ }
67
+
68
+ /**
69
+ * Create an API adapter instance
70
+ * @param {Object} config - Adapter config
71
+ * @returns {Object} Adapter with execute interface
72
+ */
73
+ function createAdapter(config = {}) {
74
+ return {
75
+ name: 'api',
76
+ execute: async (prompt, deps = {}) => {
77
+ const req = buildRequest(prompt, config);
78
+ const { executeApiProvider } = deps;
79
+ if (!executeApiProvider) {
80
+ throw new Error('executeApiProvider dependency required');
81
+ }
82
+ const result = await executeApiProvider(prompt, {
83
+ ...config,
84
+ fetch: deps.fetch,
85
+ });
86
+ return result;
87
+ },
88
+ };
89
+ }
90
+
91
+ module.exports = {
92
+ buildRequest,
93
+ parseApiResponse,
94
+ createAdapter,
95
+ };
@@ -0,0 +1,81 @@
1
+ /**
2
+ * API Adapter Tests
3
+ *
4
+ * HTTP adapter for OpenAI-compatible APIs (LiteLLM, direct).
5
+ */
6
+ import { describe, it, expect, vi } from 'vitest';
7
+
8
+ const {
9
+ buildRequest,
10
+ parseApiResponse,
11
+ createAdapter,
12
+ } = require('./api-adapter.js');
13
+
14
+ describe('API Adapter', () => {
15
+ describe('buildRequest', () => {
16
+ it('sends correct HTTP request body', () => {
17
+ const req = buildRequest('Review code', {
18
+ url: 'http://localhost:4000/v1/chat/completions',
19
+ model: 'gpt-4o',
20
+ });
21
+
22
+ expect(req.url).toBe('http://localhost:4000/v1/chat/completions');
23
+ expect(req.body.model).toBe('gpt-4o');
24
+ expect(req.body.messages).toBeDefined();
25
+ expect(req.body.messages[0].content).toContain('Review code');
26
+ });
27
+
28
+ it('includes auth header from config', () => {
29
+ const req = buildRequest('prompt', {
30
+ url: 'http://api.example.com/v1/chat/completions',
31
+ model: 'gpt-4o',
32
+ apiKey: 'sk-test-key',
33
+ });
34
+
35
+ expect(req.headers['Authorization']).toBe('Bearer sk-test-key');
36
+ });
37
+
38
+ it('respects timeout', () => {
39
+ const req = buildRequest('prompt', {
40
+ url: 'http://localhost:4000/v1/chat/completions',
41
+ model: 'gpt-4o',
42
+ timeout: 30000,
43
+ });
44
+
45
+ expect(req.timeout).toBe(30000);
46
+ });
47
+ });
48
+
49
+ describe('parseApiResponse', () => {
50
+ it('parses OpenAI-format response', () => {
51
+ const apiResp = {
52
+ choices: [{ message: { content: '{"findings": [], "summary": "Clean"}' } }],
53
+ model: 'gpt-4o',
54
+ usage: { total_tokens: 500 },
55
+ };
56
+
57
+ const result = parseApiResponse(apiResp);
58
+ expect(result.response).toContain('Clean');
59
+ expect(result.model).toBe('gpt-4o');
60
+ expect(result.tokens).toBe(500);
61
+ });
62
+
63
+ it('handles rate limiting (429)', () => {
64
+ const result = parseApiResponse(null, { status: 429 });
65
+ expect(result.error).toBeDefined();
66
+ expect(result.retryable).toBe(true);
67
+ });
68
+ });
69
+
70
+ describe('createAdapter', () => {
71
+ it('implements execute interface', () => {
72
+ const adapter = createAdapter({
73
+ url: 'http://localhost:4000/v1/chat/completions',
74
+ model: 'gpt-4o',
75
+ });
76
+
77
+ expect(adapter.name).toBe('api');
78
+ expect(adapter.execute).toBeDefined();
79
+ });
80
+ });
81
+ });
@@ -0,0 +1,85 @@
1
+ /**
2
+ * Codex CLI Adapter
3
+ *
4
+ * Format prompts for Codex CLI and parse its output.
5
+ *
6
+ * @module llm/adapters/codex-adapter
7
+ */
8
+
9
+ const DEFAULT_TIMEOUT = 60000;
10
+
11
+ /**
12
+ * Build Codex CLI command
13
+ * @param {string} prompt - Prompt text
14
+ * @param {Object} options - Adapter options
15
+ * @returns {Object} Command specification
16
+ */
17
+ function buildCommand(prompt, options = {}) {
18
+ const args = ['--quiet'];
19
+
20
+ if (options.model) {
21
+ args.push('--model', options.model);
22
+ }
23
+
24
+ return {
25
+ command: 'codex',
26
+ args,
27
+ prompt,
28
+ timeout: options.timeout || DEFAULT_TIMEOUT,
29
+ };
30
+ }
31
+
32
+ /**
33
+ * Parse Codex response
34
+ * @param {string} output - Raw CLI output
35
+ * @returns {Object} Parsed response
36
+ */
37
+ function parseResponse(output) {
38
+ const trimmed = output.trim();
39
+
40
+ // Try JSON parse first
41
+ try {
42
+ const parsed = JSON.parse(trimmed);
43
+ return {
44
+ findings: parsed.findings || [],
45
+ summary: parsed.summary || '',
46
+ raw: trimmed,
47
+ };
48
+ } catch {
49
+ // Not JSON — return raw
50
+ return {
51
+ raw: trimmed,
52
+ findings: [],
53
+ summary: trimmed,
54
+ };
55
+ }
56
+ }
57
+
58
+ /**
59
+ * Create a Codex adapter instance
60
+ * @param {Object} config - Adapter config
61
+ * @returns {Object} Adapter with execute interface
62
+ */
63
+ function createAdapter(config = {}) {
64
+ return {
65
+ name: 'codex',
66
+ execute: async (prompt, deps = {}) => {
67
+ const cmd = buildCommand(prompt, config);
68
+ const { executeCliProvider } = deps;
69
+ if (!executeCliProvider) {
70
+ throw new Error('executeCliProvider dependency required');
71
+ }
72
+ const result = await executeCliProvider(prompt, {
73
+ ...cmd,
74
+ spawn: deps.spawn,
75
+ });
76
+ return parseResponse(result.response);
77
+ },
78
+ };
79
+ }
80
+
81
+ module.exports = {
82
+ buildCommand,
83
+ parseResponse,
84
+ createAdapter,
85
+ };
@@ -0,0 +1,54 @@
1
+ /**
2
+ * Codex CLI Adapter Tests
3
+ *
4
+ * Format prompts for Codex CLI, parse its output.
5
+ */
6
+ import { describe, it, expect, vi } from 'vitest';
7
+
8
+ const {
9
+ buildCommand,
10
+ parseResponse,
11
+ createAdapter,
12
+ } = require('./codex-adapter.js');
13
+
14
+ describe('Codex Adapter', () => {
15
+ describe('buildCommand', () => {
16
+ it('builds correct CLI command', () => {
17
+ const cmd = buildCommand('Review this code', { model: 'gpt-4o' });
18
+ expect(cmd.command).toBe('codex');
19
+ expect(cmd.args).toContain('--quiet');
20
+ });
21
+
22
+ it('passes model flag from config', () => {
23
+ const cmd = buildCommand('prompt', { model: 'o3-mini' });
24
+ expect(cmd.args).toContain('--model');
25
+ expect(cmd.args).toContain('o3-mini');
26
+ });
27
+
28
+ it('respects timeout', () => {
29
+ const cmd = buildCommand('prompt', { timeout: 30000 });
30
+ expect(cmd.timeout).toBe(30000);
31
+ });
32
+ });
33
+
34
+ describe('parseResponse', () => {
35
+ it('parses JSON response', () => {
36
+ const json = JSON.stringify({ findings: [{ severity: 'high', message: 'XSS' }], summary: 'Issues found' });
37
+ const result = parseResponse(json);
38
+ expect(result.findings).toHaveLength(1);
39
+ });
40
+
41
+ it('handles non-JSON output gracefully', () => {
42
+ const result = parseResponse('This is a plain text review response with some issues noted.');
43
+ expect(result.raw).toBeDefined();
44
+ });
45
+ });
46
+
47
+ describe('createAdapter', () => {
48
+ it('implements execute interface', () => {
49
+ const adapter = createAdapter({ model: 'gpt-4o' });
50
+ expect(adapter.name).toBe('codex');
51
+ expect(adapter.execute).toBeDefined();
52
+ });
53
+ });
54
+ });
@@ -0,0 +1,100 @@
1
+ /**
2
+ * Gemini CLI Adapter
3
+ *
4
+ * Format prompts for Gemini CLI and parse its output.
5
+ *
6
+ * @module llm/adapters/gemini-adapter
7
+ */
8
+
9
+ const DEFAULT_TIMEOUT = 60000;
10
+
11
+ /**
12
+ * Build Gemini CLI command
13
+ * @param {string} prompt - Prompt text
14
+ * @param {Object} options - Adapter options
15
+ * @returns {Object} Command specification
16
+ */
17
+ function buildCommand(prompt, options = {}) {
18
+ const args = [];
19
+
20
+ if (options.model) {
21
+ args.push('--model', options.model);
22
+ }
23
+
24
+ return {
25
+ command: 'gemini',
26
+ args,
27
+ prompt,
28
+ timeout: options.timeout || DEFAULT_TIMEOUT,
29
+ };
30
+ }
31
+
32
+ /**
33
+ * Parse Gemini response (may be markdown or JSON in code block)
34
+ * @param {string} output - Raw CLI output
35
+ * @returns {Object} Parsed response
36
+ */
37
+ function parseResponse(output) {
38
+ const trimmed = output.trim();
39
+
40
+ // Try to extract JSON from markdown code block
41
+ const codeBlockMatch = trimmed.match(/```(?:json)?\s*\n?([\s\S]*?)\n?```/);
42
+ if (codeBlockMatch) {
43
+ try {
44
+ const parsed = JSON.parse(codeBlockMatch[1].trim());
45
+ return {
46
+ findings: parsed.findings || [],
47
+ summary: parsed.summary || '',
48
+ raw: trimmed,
49
+ };
50
+ } catch {
51
+ // JSON in code block but invalid — fall through
52
+ }
53
+ }
54
+
55
+ // Try direct JSON parse
56
+ try {
57
+ const parsed = JSON.parse(trimmed);
58
+ return {
59
+ findings: parsed.findings || [],
60
+ summary: parsed.summary || '',
61
+ raw: trimmed,
62
+ };
63
+ } catch {
64
+ // Markdown/text response
65
+ return {
66
+ raw: trimmed,
67
+ findings: [],
68
+ summary: trimmed,
69
+ };
70
+ }
71
+ }
72
+
73
+ /**
74
+ * Create a Gemini adapter instance
75
+ * @param {Object} config - Adapter config
76
+ * @returns {Object} Adapter with execute interface
77
+ */
78
+ function createAdapter(config = {}) {
79
+ return {
80
+ name: 'gemini',
81
+ execute: async (prompt, deps = {}) => {
82
+ const cmd = buildCommand(prompt, config);
83
+ const { executeCliProvider } = deps;
84
+ if (!executeCliProvider) {
85
+ throw new Error('executeCliProvider dependency required');
86
+ }
87
+ const result = await executeCliProvider(prompt, {
88
+ ...cmd,
89
+ spawn: deps.spawn,
90
+ });
91
+ return parseResponse(result.response);
92
+ },
93
+ };
94
+ }
95
+
96
+ module.exports = {
97
+ buildCommand,
98
+ parseResponse,
99
+ createAdapter,
100
+ };
@@ -0,0 +1,54 @@
1
+ /**
2
+ * Gemini CLI Adapter Tests
3
+ *
4
+ * Format prompts for Gemini CLI, parse its output.
5
+ */
6
+ import { describe, it, expect, vi } from 'vitest';
7
+
8
+ const {
9
+ buildCommand,
10
+ parseResponse,
11
+ createAdapter,
12
+ } = require('./gemini-adapter.js');
13
+
14
+ describe('Gemini Adapter', () => {
15
+ describe('buildCommand', () => {
16
+ it('builds correct CLI command', () => {
17
+ const cmd = buildCommand('Review this code', {});
18
+ expect(cmd.command).toBe('gemini');
19
+ });
20
+
21
+ it('passes model flag from config', () => {
22
+ const cmd = buildCommand('prompt', { model: 'gemini-2.5-pro' });
23
+ expect(cmd.args).toContain('--model');
24
+ expect(cmd.args).toContain('gemini-2.5-pro');
25
+ });
26
+
27
+ it('respects timeout', () => {
28
+ const cmd = buildCommand('prompt', { timeout: 45000 });
29
+ expect(cmd.timeout).toBe(45000);
30
+ });
31
+ });
32
+
33
+ describe('parseResponse', () => {
34
+ it('parses markdown response', () => {
35
+ const markdown = '## Issues\n- Line 5: XSS vulnerability\n- Line 12: Missing validation';
36
+ const result = parseResponse(markdown);
37
+ expect(result.raw).toContain('XSS');
38
+ });
39
+
40
+ it('handles structured output mode', () => {
41
+ const json = '```json\n{"findings": [{"severity": "high", "message": "SQL injection"}]}\n```';
42
+ const result = parseResponse(json);
43
+ expect(result.findings).toHaveLength(1);
44
+ });
45
+ });
46
+
47
+ describe('createAdapter', () => {
48
+ it('implements execute interface', () => {
49
+ const adapter = createAdapter({ model: 'gemini-2.5-pro' });
50
+ expect(adapter.name).toBe('gemini');
51
+ expect(adapter.execute).toBeDefined();
52
+ });
53
+ });
54
+ });
@@ -0,0 +1,109 @@
1
+ /**
2
+ * LLM Service - Unified API
3
+ *
4
+ * createLLMService(config) → { review(diff), execute(prompt), health() }
5
+ *
6
+ * @module llm
7
+ */
8
+
9
+ const { createExecutor } = require('./provider-executor.js');
10
+ const { createRegistry } = require('./provider-registry.js');
11
+ const { createReviewService } = require('./review-service.js');
12
+
13
+ /**
14
+ * Create the unified LLM service
15
+ * @param {Object} config - Service configuration
16
+ * @param {Object} config.providers - Provider configs { name: { type, command, capabilities } }
17
+ * @param {boolean} config.multiModel - Enable multi-model review
18
+ * @param {number} config.timeout - Default timeout
19
+ * @param {Object} deps - Injectable dependencies
20
+ * @param {Function} deps.healthCheck - Health check function
21
+ * @param {Function} deps.spawn - Process spawn function
22
+ * @param {Function} deps.fetch - HTTP fetch function
23
+ * @returns {Object} Service with review, execute, health methods
24
+ */
25
+ function createLLMService(config = {}, deps = {}) {
26
+ const { providers = {}, multiModel = false, timeout } = config;
27
+
28
+ // Build health check from deps
29
+ const healthCheck = deps.healthCheck || (() => Promise.resolve({ available: false }));
30
+
31
+ // Create registry and load providers
32
+ const registry = createRegistry({ healthCheck, cacheTTL: config.cacheTTL });
33
+ registry.loadFromConfig({ providers });
34
+
35
+ // Create executor with real spawn/fetch
36
+ const executor = createExecutor({
37
+ spawn: deps.spawn,
38
+ fetch: deps.fetch,
39
+ });
40
+
41
+ // Create review service
42
+ const reviewService = createReviewService({
43
+ registry,
44
+ executor,
45
+ multiModel,
46
+ timeout,
47
+ standards: config.standards || '',
48
+ });
49
+
50
+ return {
51
+ /**
52
+ * Review a diff using configured providers
53
+ * @param {string} diff - Git diff content
54
+ * @param {Object} options - Review options
55
+ * @returns {Promise<Object>} { findings, summary, provider, latency }
56
+ */
57
+ review: (diff, options) => reviewService.review(diff, options),
58
+
59
+ /**
60
+ * Execute a generic prompt against best available provider
61
+ * @param {string} prompt - Prompt text
62
+ * @param {Object} options - Execution options
63
+ * @returns {Promise<Object>} { response, model, latency }
64
+ */
65
+ execute: async (prompt, options = {}) => {
66
+ const capability = options.capability || 'code-gen';
67
+ const provider = await registry.getBestProvider(capability);
68
+
69
+ if (!provider) {
70
+ // Fall back to first available provider
71
+ const allProviders = registry.list();
72
+ if (allProviders.length === 0) {
73
+ return { response: '', error: 'No providers configured' };
74
+ }
75
+
76
+ try {
77
+ return await executor.execute(prompt, { ...allProviders[0], timeout });
78
+ } catch (err) {
79
+ return { response: '', error: err.message };
80
+ }
81
+ }
82
+
83
+ try {
84
+ return await executor.execute(prompt, { ...provider, timeout });
85
+ } catch (err) {
86
+ return { response: '', error: err.message };
87
+ }
88
+ },
89
+
90
+ /**
91
+ * Get health status of all providers
92
+ * @returns {Promise<Object>} { providers: { name: status } }
93
+ */
94
+ health: async () => {
95
+ const allProviders = registry.list();
96
+ const statuses = {};
97
+
98
+ for (const provider of allProviders) {
99
+ statuses[provider.name] = await registry.checkHealth(provider.name);
100
+ }
101
+
102
+ return { providers: statuses };
103
+ },
104
+ };
105
+ }
106
+
107
+ module.exports = {
108
+ createLLMService,
109
+ };