tlc-claude-code 1.2.27 → 1.2.28

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 (172) hide show
  1. package/dashboard/dist/components/ActivityFeed.d.ts +17 -0
  2. package/dashboard/dist/components/ActivityFeed.js +42 -0
  3. package/dashboard/dist/components/ActivityFeed.test.d.ts +1 -0
  4. package/dashboard/dist/components/ActivityFeed.test.js +162 -0
  5. package/dashboard/dist/components/BranchSelector.d.ts +16 -0
  6. package/dashboard/dist/components/BranchSelector.js +49 -0
  7. package/dashboard/dist/components/BranchSelector.test.d.ts +1 -0
  8. package/dashboard/dist/components/BranchSelector.test.js +166 -0
  9. package/dashboard/dist/components/CommandPalette.d.ts +17 -0
  10. package/dashboard/dist/components/CommandPalette.js +118 -0
  11. package/dashboard/dist/components/CommandPalette.test.d.ts +1 -0
  12. package/dashboard/dist/components/CommandPalette.test.js +181 -0
  13. package/dashboard/dist/components/ConnectionStatus.d.ts +16 -0
  14. package/dashboard/dist/components/ConnectionStatus.js +27 -0
  15. package/dashboard/dist/components/ConnectionStatus.test.d.ts +1 -0
  16. package/dashboard/dist/components/ConnectionStatus.test.js +121 -0
  17. package/dashboard/dist/components/DeviceFrame.d.ts +19 -0
  18. package/dashboard/dist/components/DeviceFrame.js +52 -0
  19. package/dashboard/dist/components/DeviceFrame.test.d.ts +1 -0
  20. package/dashboard/dist/components/DeviceFrame.test.js +118 -0
  21. package/dashboard/dist/components/EnvironmentBadge.d.ts +11 -0
  22. package/dashboard/dist/components/EnvironmentBadge.js +16 -0
  23. package/dashboard/dist/components/EnvironmentBadge.test.d.ts +1 -0
  24. package/dashboard/dist/components/EnvironmentBadge.test.js +102 -0
  25. package/dashboard/dist/components/FocusIndicator.d.ts +19 -0
  26. package/dashboard/dist/components/FocusIndicator.js +47 -0
  27. package/dashboard/dist/components/FocusIndicator.test.d.ts +1 -0
  28. package/dashboard/dist/components/FocusIndicator.test.js +117 -0
  29. package/dashboard/dist/components/KeyboardHelp.d.ts +15 -0
  30. package/dashboard/dist/components/KeyboardHelp.js +61 -0
  31. package/dashboard/dist/components/KeyboardHelp.test.d.ts +1 -0
  32. package/dashboard/dist/components/KeyboardHelp.test.js +131 -0
  33. package/dashboard/dist/components/LogSearch.d.ts +13 -0
  34. package/dashboard/dist/components/LogSearch.js +43 -0
  35. package/dashboard/dist/components/LogSearch.test.d.ts +1 -0
  36. package/dashboard/dist/components/LogSearch.test.js +100 -0
  37. package/dashboard/dist/components/LogStream.d.ts +21 -0
  38. package/dashboard/dist/components/LogStream.js +123 -0
  39. package/dashboard/dist/components/LogStream.test.d.ts +1 -0
  40. package/dashboard/dist/components/LogStream.test.js +159 -0
  41. package/dashboard/dist/components/PreviewPanel.d.ts +18 -0
  42. package/dashboard/dist/components/PreviewPanel.js +73 -0
  43. package/dashboard/dist/components/PreviewPanel.test.d.ts +1 -0
  44. package/dashboard/dist/components/PreviewPanel.test.js +124 -0
  45. package/dashboard/dist/components/ProjectCard.d.ts +18 -0
  46. package/dashboard/dist/components/ProjectCard.js +19 -0
  47. package/dashboard/dist/components/ProjectCard.test.d.ts +1 -0
  48. package/dashboard/dist/components/ProjectCard.test.js +53 -0
  49. package/dashboard/dist/components/ProjectDetail.d.ts +44 -0
  50. package/dashboard/dist/components/ProjectDetail.js +65 -0
  51. package/dashboard/dist/components/ProjectDetail.test.d.ts +1 -0
  52. package/dashboard/dist/components/ProjectDetail.test.js +196 -0
  53. package/dashboard/dist/components/ProjectList.d.ts +11 -0
  54. package/dashboard/dist/components/ProjectList.js +62 -0
  55. package/dashboard/dist/components/ProjectList.test.d.ts +1 -0
  56. package/dashboard/dist/components/ProjectList.test.js +93 -0
  57. package/dashboard/dist/components/SettingsPanel.d.ts +32 -0
  58. package/dashboard/dist/components/SettingsPanel.js +154 -0
  59. package/dashboard/dist/components/SettingsPanel.test.d.ts +1 -0
  60. package/dashboard/dist/components/SettingsPanel.test.js +196 -0
  61. package/dashboard/dist/components/StatusBar.d.ts +16 -0
  62. package/dashboard/dist/components/StatusBar.js +47 -0
  63. package/dashboard/dist/components/StatusBar.test.d.ts +1 -0
  64. package/dashboard/dist/components/StatusBar.test.js +123 -0
  65. package/dashboard/dist/components/TaskBoard.d.ts +22 -0
  66. package/dashboard/dist/components/TaskBoard.js +102 -0
  67. package/dashboard/dist/components/TaskBoard.test.d.ts +1 -0
  68. package/dashboard/dist/components/TaskBoard.test.js +113 -0
  69. package/dashboard/dist/components/TaskCard.d.ts +17 -0
  70. package/dashboard/dist/components/TaskCard.js +29 -0
  71. package/dashboard/dist/components/TaskCard.test.d.ts +1 -0
  72. package/dashboard/dist/components/TaskCard.test.js +109 -0
  73. package/dashboard/dist/components/TaskDetail.d.ts +36 -0
  74. package/dashboard/dist/components/TaskDetail.js +41 -0
  75. package/dashboard/dist/components/TaskDetail.test.d.ts +1 -0
  76. package/dashboard/dist/components/TaskDetail.test.js +164 -0
  77. package/dashboard/dist/components/TaskFilter.d.ts +12 -0
  78. package/dashboard/dist/components/TaskFilter.js +138 -0
  79. package/dashboard/dist/components/TaskFilter.test.d.ts +1 -0
  80. package/dashboard/dist/components/TaskFilter.test.js +109 -0
  81. package/dashboard/dist/components/TeamPanel.d.ts +15 -0
  82. package/dashboard/dist/components/TeamPanel.js +24 -0
  83. package/dashboard/dist/components/TeamPanel.test.d.ts +1 -0
  84. package/dashboard/dist/components/TeamPanel.test.js +109 -0
  85. package/dashboard/dist/components/TeamPresence.d.ts +14 -0
  86. package/dashboard/dist/components/TeamPresence.js +31 -0
  87. package/dashboard/dist/components/TeamPresence.test.d.ts +1 -0
  88. package/dashboard/dist/components/TeamPresence.test.js +144 -0
  89. package/dashboard/dist/components/layout/Header.d.ts +9 -0
  90. package/dashboard/dist/components/layout/Header.js +11 -0
  91. package/dashboard/dist/components/layout/Header.test.d.ts +1 -0
  92. package/dashboard/dist/components/layout/Header.test.js +35 -0
  93. package/dashboard/dist/components/layout/Shell.d.ts +10 -0
  94. package/dashboard/dist/components/layout/Shell.js +5 -0
  95. package/dashboard/dist/components/layout/Shell.test.d.ts +1 -0
  96. package/dashboard/dist/components/layout/Shell.test.js +34 -0
  97. package/dashboard/dist/components/layout/Sidebar.d.ts +14 -0
  98. package/dashboard/dist/components/layout/Sidebar.js +8 -0
  99. package/dashboard/dist/components/layout/Sidebar.test.d.ts +1 -0
  100. package/dashboard/dist/components/layout/Sidebar.test.js +40 -0
  101. package/dashboard/dist/components/ui/Badge.d.ts +9 -0
  102. package/dashboard/dist/components/ui/Badge.js +13 -0
  103. package/dashboard/dist/components/ui/Badge.test.d.ts +1 -0
  104. package/dashboard/dist/components/ui/Badge.test.js +69 -0
  105. package/dashboard/dist/components/ui/Button.d.ts +12 -0
  106. package/dashboard/dist/components/ui/Button.js +14 -0
  107. package/dashboard/dist/components/ui/Button.test.d.ts +1 -0
  108. package/dashboard/dist/components/ui/Button.test.js +81 -0
  109. package/dashboard/dist/components/ui/Card.d.ts +21 -0
  110. package/dashboard/dist/components/ui/Card.js +20 -0
  111. package/dashboard/dist/components/ui/Card.test.d.ts +1 -0
  112. package/dashboard/dist/components/ui/Card.test.js +82 -0
  113. package/dashboard/dist/components/ui/Input.d.ts +13 -0
  114. package/dashboard/dist/components/ui/Input.js +8 -0
  115. package/dashboard/dist/components/ui/Input.test.d.ts +1 -0
  116. package/dashboard/dist/components/ui/Input.test.js +68 -0
  117. package/dashboard/dist/styles/tokens.d.ts +150 -0
  118. package/dashboard/dist/styles/tokens.js +184 -0
  119. package/dashboard/dist/styles/tokens.test.d.ts +1 -0
  120. package/dashboard/dist/styles/tokens.test.js +95 -0
  121. package/dashboard/dist/test/setup.d.ts +1 -0
  122. package/dashboard/dist/test/setup.js +1 -0
  123. package/dashboard/package.json +3 -0
  124. package/package.json +1 -1
  125. package/server/lib/adapters/base-adapter.js +114 -0
  126. package/server/lib/adapters/base-adapter.test.js +90 -0
  127. package/server/lib/adapters/claude-adapter.js +141 -0
  128. package/server/lib/adapters/claude-adapter.test.js +180 -0
  129. package/server/lib/adapters/deepseek-adapter.js +153 -0
  130. package/server/lib/adapters/deepseek-adapter.test.js +193 -0
  131. package/server/lib/adapters/openai-adapter.js +190 -0
  132. package/server/lib/adapters/openai-adapter.test.js +231 -0
  133. package/server/lib/budget-tracker.js +169 -0
  134. package/server/lib/budget-tracker.test.js +165 -0
  135. package/server/lib/claude-injector.js +85 -0
  136. package/server/lib/claude-injector.test.js +161 -0
  137. package/server/lib/consensus-engine.js +135 -0
  138. package/server/lib/consensus-engine.test.js +152 -0
  139. package/server/lib/context-builder.js +112 -0
  140. package/server/lib/context-builder.test.js +120 -0
  141. package/server/lib/file-collector.js +322 -0
  142. package/server/lib/file-collector.test.js +307 -0
  143. package/server/lib/memory-classifier.js +175 -0
  144. package/server/lib/memory-classifier.test.js +169 -0
  145. package/server/lib/memory-committer.js +138 -0
  146. package/server/lib/memory-committer.test.js +136 -0
  147. package/server/lib/memory-hooks.js +127 -0
  148. package/server/lib/memory-hooks.test.js +136 -0
  149. package/server/lib/memory-init.js +104 -0
  150. package/server/lib/memory-init.test.js +119 -0
  151. package/server/lib/memory-observer.js +149 -0
  152. package/server/lib/memory-observer.test.js +158 -0
  153. package/server/lib/memory-reader.js +243 -0
  154. package/server/lib/memory-reader.test.js +216 -0
  155. package/server/lib/memory-storage.js +120 -0
  156. package/server/lib/memory-storage.test.js +136 -0
  157. package/server/lib/memory-writer.js +176 -0
  158. package/server/lib/memory-writer.test.js +231 -0
  159. package/server/lib/overdrive-command.js +30 -6
  160. package/server/lib/overdrive-command.test.js +8 -1
  161. package/server/lib/pattern-detector.js +216 -0
  162. package/server/lib/pattern-detector.test.js +241 -0
  163. package/server/lib/relevance-scorer.js +175 -0
  164. package/server/lib/relevance-scorer.test.js +107 -0
  165. package/server/lib/review-command.js +238 -0
  166. package/server/lib/review-command.test.js +245 -0
  167. package/server/lib/review-orchestrator.js +273 -0
  168. package/server/lib/review-orchestrator.test.js +300 -0
  169. package/server/lib/review-reporter.js +288 -0
  170. package/server/lib/review-reporter.test.js +240 -0
  171. package/server/lib/session-summary.js +90 -0
  172. package/server/lib/session-summary.test.js +156 -0
@@ -0,0 +1,141 @@
1
+ /**
2
+ * Claude Adapter - Adapter for Claude API
3
+ * Note: In TLC context, Claude is subscription-based so no per-request cost
4
+ */
5
+
6
+ const { BaseAdapter } = require('./base-adapter.js');
7
+
8
+ // Latest model: claude-opus-4-5-20251101 (Claude Opus 4.5)
9
+ const CLAUDE_MODEL = 'claude-opus-4-5-20251101';
10
+
11
+ const CLAUDE_PRICING = {
12
+ // Pricing per 1M tokens (Claude Opus 4.5)
13
+ inputPerMillion: 15.00,
14
+ outputPerMillion: 75.00,
15
+ };
16
+
17
+ class ClaudeAdapter extends BaseAdapter {
18
+ constructor(config = {}) {
19
+ super({
20
+ name: 'claude',
21
+ ...config,
22
+ });
23
+ this.budgetTracker = config.budgetTracker || null;
24
+ this.pricing = config.pricing || CLAUDE_PRICING;
25
+ this.model = config.model || CLAUDE_MODEL;
26
+ this.trackCost = config.trackCost !== false;
27
+ }
28
+
29
+ /**
30
+ * Review code for issues and suggestions
31
+ * @param {string} code - Code to review
32
+ * @param {Object} context - Additional context
33
+ * @returns {Promise<Object>} Standardized review response
34
+ */
35
+ async review(code, context = {}) {
36
+ if (!code || code.trim() === '') {
37
+ return this.createEmptyResponse();
38
+ }
39
+
40
+ // Check budget if tracking
41
+ const estimatedTokens = this.estimateTokens(code);
42
+ const estimatedCost = this.estimateCost(estimatedTokens);
43
+
44
+ if (this.trackCost && !this.canAfford(estimatedCost)) {
45
+ throw new Error('Budget exceeded for Claude');
46
+ }
47
+
48
+ try {
49
+ // Simulate API call - in real implementation, call Claude API
50
+ const result = await this.callAPI(code, context);
51
+
52
+ // Record cost if tracking
53
+ if (this.trackCost && this.budgetTracker) {
54
+ this.budgetTracker.record('claude', result.cost || 0);
55
+ }
56
+
57
+ return result;
58
+ } catch (error) {
59
+ // Return empty response on error, don't throw
60
+ return {
61
+ ...this.createEmptyResponse(),
62
+ error: error.message,
63
+ };
64
+ }
65
+ }
66
+
67
+ /**
68
+ * Call Claude API (mock implementation)
69
+ * @param {string} code - Code to review
70
+ * @param {Object} context - Additional context
71
+ * @returns {Promise<Object>} API response
72
+ */
73
+ async callAPI(code, context) {
74
+ // This is a mock - in production, call actual Claude API
75
+ // For now, simulate a response
76
+ const tokensUsed = this.estimateTokens(code);
77
+ const cost = this.estimateCost(tokensUsed);
78
+
79
+ return {
80
+ issues: [],
81
+ suggestions: [],
82
+ score: 100,
83
+ model: this.name,
84
+ tokensUsed,
85
+ cost,
86
+ };
87
+ }
88
+
89
+ /**
90
+ * Estimate tokens for code
91
+ * @param {string} code - Code to estimate
92
+ * @returns {number} Estimated tokens
93
+ */
94
+ estimateTokens(code) {
95
+ // Rough estimate: ~4 chars per token for code
96
+ return Math.ceil(code.length / 4);
97
+ }
98
+
99
+ /**
100
+ * Estimate cost for tokens
101
+ * @param {number} tokens - Number of tokens
102
+ * @returns {number} Estimated cost in USD
103
+ */
104
+ estimateCost(tokens) {
105
+ // Assume 50% input, 50% output for review
106
+ const inputCost = (tokens * 0.5 * this.pricing.inputPerMillion) / 1_000_000;
107
+ const outputCost = (tokens * 0.5 * this.pricing.outputPerMillion) / 1_000_000;
108
+ return inputCost + outputCost;
109
+ }
110
+
111
+ /**
112
+ * Check if adapter can afford a request
113
+ * @param {number} estimatedCost - Estimated cost
114
+ * @returns {boolean} Whether request is within budget
115
+ */
116
+ canAfford(estimatedCost = 0) {
117
+ if (!this.trackCost || !this.budgetTracker) {
118
+ return true;
119
+ }
120
+
121
+ const budgetConfig = this.config.budget || { budgetDaily: 10, budgetMonthly: 100 };
122
+ return this.budgetTracker.canSpend('claude', estimatedCost, budgetConfig);
123
+ }
124
+
125
+ /**
126
+ * Get current usage stats
127
+ * @returns {Object} Usage stats
128
+ */
129
+ getUsage() {
130
+ if (!this.budgetTracker) {
131
+ return { daily: 0, monthly: 0, requests: 0 };
132
+ }
133
+ return this.budgetTracker.getUsage('claude');
134
+ }
135
+ }
136
+
137
+ module.exports = {
138
+ ClaudeAdapter,
139
+ CLAUDE_PRICING,
140
+ CLAUDE_MODEL,
141
+ };
@@ -0,0 +1,180 @@
1
+ import { describe, it, expect, vi, beforeEach } from 'vitest';
2
+ import { ClaudeAdapter, CLAUDE_PRICING, CLAUDE_MODEL } from './claude-adapter.js';
3
+ import { BaseAdapter } from './base-adapter.js';
4
+
5
+ describe('ClaudeAdapter', () => {
6
+ describe('constructor', () => {
7
+ it('sets name to claude by default', () => {
8
+ const adapter = new ClaudeAdapter();
9
+ expect(adapter.name).toBe('claude');
10
+ });
11
+
12
+ it('accepts custom config', () => {
13
+ const adapter = new ClaudeAdapter({ apiKey: 'test-key' });
14
+ expect(adapter.config.apiKey).toBe('test-key');
15
+ });
16
+
17
+ it('accepts budget tracker', () => {
18
+ const mockTracker = { canSpend: vi.fn() };
19
+ const adapter = new ClaudeAdapter({ budgetTracker: mockTracker });
20
+ expect(adapter.budgetTracker).toBe(mockTracker);
21
+ });
22
+ });
23
+
24
+ describe('review', () => {
25
+ it('returns empty response for empty code', async () => {
26
+ const adapter = new ClaudeAdapter();
27
+ const result = await adapter.review('');
28
+
29
+ expect(result.issues).toEqual([]);
30
+ expect(result.score).toBe(100);
31
+ expect(result.model).toBe('claude');
32
+ });
33
+
34
+ it('returns empty response for whitespace-only code', async () => {
35
+ const adapter = new ClaudeAdapter();
36
+ const result = await adapter.review(' \n\t ');
37
+
38
+ expect(result.issues).toEqual([]);
39
+ });
40
+
41
+ it('returns standardized response format', async () => {
42
+ const adapter = new ClaudeAdapter();
43
+ const result = await adapter.review('const x = 1;');
44
+
45
+ expect(BaseAdapter.validateResponse(result)).toBe(true);
46
+ expect(result.model).toBe('claude');
47
+ expect(typeof result.tokensUsed).toBe('number');
48
+ expect(typeof result.cost).toBe('number');
49
+ });
50
+
51
+ it('checks budget before review', async () => {
52
+ const mockTracker = {
53
+ canSpend: vi.fn(() => false),
54
+ record: vi.fn(),
55
+ getUsage: vi.fn(() => ({ daily: 10, monthly: 100, requests: 50 })),
56
+ };
57
+ const adapter = new ClaudeAdapter({
58
+ budgetTracker: mockTracker,
59
+ budget: { budgetDaily: 5, budgetMonthly: 50 },
60
+ });
61
+
62
+ await expect(adapter.review('const x = 1;')).rejects.toThrow('Budget exceeded');
63
+ });
64
+
65
+ it('records cost after successful review', async () => {
66
+ const mockTracker = {
67
+ canSpend: vi.fn(() => true),
68
+ record: vi.fn(),
69
+ getUsage: vi.fn(() => ({ daily: 0, monthly: 0, requests: 0 })),
70
+ };
71
+ const adapter = new ClaudeAdapter({ budgetTracker: mockTracker });
72
+
73
+ await adapter.review('const x = 1;');
74
+
75
+ expect(mockTracker.record).toHaveBeenCalledWith('claude', expect.any(Number));
76
+ });
77
+
78
+ it('handles API errors gracefully', async () => {
79
+ const adapter = new ClaudeAdapter();
80
+ adapter.callAPI = vi.fn().mockRejectedValue(new Error('API timeout'));
81
+
82
+ const result = await adapter.review('const x = 1;');
83
+
84
+ expect(result.error).toBe('API timeout');
85
+ expect(result.issues).toEqual([]);
86
+ });
87
+ });
88
+
89
+ describe('estimateTokens', () => {
90
+ it('estimates tokens based on code length', () => {
91
+ const adapter = new ClaudeAdapter();
92
+
93
+ // ~4 chars per token
94
+ expect(adapter.estimateTokens('abcd')).toBe(1);
95
+ expect(adapter.estimateTokens('abcdefgh')).toBe(2);
96
+ expect(adapter.estimateTokens('a'.repeat(100))).toBe(25);
97
+ });
98
+ });
99
+
100
+ describe('estimateCost', () => {
101
+ it('calculates cost based on token count', () => {
102
+ const adapter = new ClaudeAdapter();
103
+ const cost = adapter.estimateCost(1000);
104
+
105
+ // At default pricing (Opus 4.5): (500 * 15 + 500 * 75) / 1M = 0.045
106
+ expect(cost).toBeCloseTo(0.045, 4);
107
+ });
108
+
109
+ it('uses custom pricing if provided', () => {
110
+ const adapter = new ClaudeAdapter({
111
+ pricing: { inputPerMillion: 1.00, outputPerMillion: 2.00 },
112
+ });
113
+ const cost = adapter.estimateCost(1000);
114
+
115
+ // (500 * 1 + 500 * 2) / 1M = 0.0015
116
+ expect(cost).toBeCloseTo(0.0015, 5);
117
+ });
118
+ });
119
+
120
+ describe('canAfford', () => {
121
+ it('returns true without budget tracker', () => {
122
+ const adapter = new ClaudeAdapter();
123
+ expect(adapter.canAfford(100)).toBe(true);
124
+ });
125
+
126
+ it('returns true when tracking disabled', () => {
127
+ const adapter = new ClaudeAdapter({ trackCost: false });
128
+ expect(adapter.canAfford(100)).toBe(true);
129
+ });
130
+
131
+ it('checks budget tracker when enabled', () => {
132
+ const mockTracker = {
133
+ canSpend: vi.fn(() => true),
134
+ };
135
+ const adapter = new ClaudeAdapter({
136
+ budgetTracker: mockTracker,
137
+ budget: { budgetDaily: 10, budgetMonthly: 100 },
138
+ });
139
+
140
+ adapter.canAfford(0.5);
141
+
142
+ expect(mockTracker.canSpend).toHaveBeenCalledWith('claude', 0.5, expect.any(Object));
143
+ });
144
+ });
145
+
146
+ describe('getUsage', () => {
147
+ it('returns zero without budget tracker', () => {
148
+ const adapter = new ClaudeAdapter();
149
+ const usage = adapter.getUsage();
150
+
151
+ expect(usage).toEqual({ daily: 0, monthly: 0, requests: 0 });
152
+ });
153
+
154
+ it('returns tracker usage when available', () => {
155
+ const mockTracker = {
156
+ getUsage: vi.fn(() => ({ daily: 2.50, monthly: 25.00, requests: 10 })),
157
+ };
158
+ const adapter = new ClaudeAdapter({ budgetTracker: mockTracker });
159
+
160
+ const usage = adapter.getUsage();
161
+
162
+ expect(usage.daily).toBe(2.50);
163
+ expect(usage.monthly).toBe(25.00);
164
+ expect(usage.requests).toBe(10);
165
+ });
166
+ });
167
+
168
+ describe('CLAUDE_PRICING', () => {
169
+ it('exports default pricing for Opus 4.5', () => {
170
+ expect(CLAUDE_PRICING.inputPerMillion).toBe(15.00);
171
+ expect(CLAUDE_PRICING.outputPerMillion).toBe(75.00);
172
+ });
173
+ });
174
+
175
+ describe('CLAUDE_MODEL', () => {
176
+ it('exports latest model identifier', () => {
177
+ expect(CLAUDE_MODEL).toBe('claude-opus-4-5-20251101');
178
+ });
179
+ });
180
+ });
@@ -0,0 +1,153 @@
1
+ /**
2
+ * DeepSeek Adapter - Adapter for DeepSeek API (budget-friendly model)
3
+ */
4
+
5
+ const { BaseAdapter } = require('./base-adapter.js');
6
+
7
+ // Latest model: deepseek-r1 (January 2025)
8
+ const DEEPSEEK_MODEL = 'deepseek-r1';
9
+
10
+ const DEEPSEEK_PRICING = {
11
+ // Pricing per 1M tokens (DeepSeek R1)
12
+ // Significantly cheaper than OpenAI
13
+ inputPerMillion: 0.55,
14
+ outputPerMillion: 2.19,
15
+ };
16
+
17
+ class DeepSeekAdapter extends BaseAdapter {
18
+ constructor(config = {}) {
19
+ super({
20
+ name: 'deepseek',
21
+ ...config,
22
+ });
23
+ this.budgetTracker = config.budgetTracker || null;
24
+ this.pricing = config.pricing || DEEPSEEK_PRICING;
25
+ this.model = config.model || DEEPSEEK_MODEL;
26
+ }
27
+
28
+ /**
29
+ * Review code for issues and suggestions
30
+ * @param {string} code - Code to review
31
+ * @param {Object} context - Additional context
32
+ * @returns {Promise<Object>} Standardized review response
33
+ */
34
+ async review(code, context = {}) {
35
+ if (!code || code.trim() === '') {
36
+ return this.createEmptyResponse();
37
+ }
38
+
39
+ const estimatedTokens = this.estimateTokens(code);
40
+ const estimatedCost = this.estimateCost(estimatedTokens);
41
+
42
+ if (!this.canAfford(estimatedCost)) {
43
+ throw new Error('Budget exceeded for DeepSeek');
44
+ }
45
+
46
+ try {
47
+ const result = await this.callAPI(code, context);
48
+
49
+ if (this.budgetTracker) {
50
+ this.budgetTracker.record('deepseek', result.cost || 0);
51
+ }
52
+
53
+ return result;
54
+ } catch (error) {
55
+ // DeepSeek errors return empty response with warning
56
+ return {
57
+ ...this.createEmptyResponse(),
58
+ warning: `DeepSeek unavailable: ${error.message}`,
59
+ };
60
+ }
61
+ }
62
+
63
+ /**
64
+ * Call DeepSeek API (mock implementation)
65
+ * @param {string} code - Code to review
66
+ * @param {Object} context - Additional context
67
+ * @returns {Promise<Object>} API response
68
+ */
69
+ async callAPI(code, context) {
70
+ const tokensUsed = this.estimateTokens(code);
71
+ const cost = this.estimateCost(tokensUsed);
72
+
73
+ return {
74
+ issues: [],
75
+ suggestions: [],
76
+ score: 100,
77
+ model: this.name,
78
+ tokensUsed,
79
+ cost,
80
+ };
81
+ }
82
+
83
+ /**
84
+ * Estimate tokens for code
85
+ * @param {string} code - Code to estimate
86
+ * @returns {number} Estimated tokens
87
+ */
88
+ estimateTokens(code) {
89
+ return Math.ceil(code.length / 4);
90
+ }
91
+
92
+ /**
93
+ * Estimate cost for tokens
94
+ * @param {number} tokens - Number of tokens
95
+ * @returns {number} Estimated cost in USD
96
+ */
97
+ estimateCost(tokens) {
98
+ const inputCost = (tokens * 0.5 * this.pricing.inputPerMillion) / 1_000_000;
99
+ const outputCost = (tokens * 0.5 * this.pricing.outputPerMillion) / 1_000_000;
100
+ return inputCost + outputCost;
101
+ }
102
+
103
+ /**
104
+ * Check if adapter can afford a request
105
+ * @param {number} estimatedCost - Estimated cost
106
+ * @returns {boolean}
107
+ */
108
+ canAfford(estimatedCost = 0) {
109
+ if (!this.budgetTracker) {
110
+ return true;
111
+ }
112
+
113
+ const budgetConfig = this.config.budget || { budgetDaily: 5, budgetMonthly: 50 };
114
+ return this.budgetTracker.canSpend('deepseek', estimatedCost, budgetConfig);
115
+ }
116
+
117
+ /**
118
+ * Get current usage stats
119
+ * @returns {Object} Usage stats
120
+ */
121
+ getUsage() {
122
+ if (!this.budgetTracker) {
123
+ return { daily: 0, monthly: 0, requests: 0 };
124
+ }
125
+ return this.budgetTracker.getUsage('deepseek');
126
+ }
127
+
128
+ /**
129
+ * Compare cost with OpenAI equivalent
130
+ * @param {number} tokens - Token count
131
+ * @returns {Object} Cost comparison
132
+ */
133
+ compareCostWithOpenAI(tokens) {
134
+ const deepseekCost = this.estimateCost(tokens);
135
+ // OpenAI GPT-4 Turbo pricing
136
+ const openaiCost = (tokens * 0.5 * 10.00 + tokens * 0.5 * 30.00) / 1_000_000;
137
+ const savings = openaiCost - deepseekCost;
138
+ const savingsPercent = (savings / openaiCost) * 100;
139
+
140
+ return {
141
+ deepseek: deepseekCost,
142
+ openai: openaiCost,
143
+ savings,
144
+ savingsPercent,
145
+ };
146
+ }
147
+ }
148
+
149
+ module.exports = {
150
+ DeepSeekAdapter,
151
+ DEEPSEEK_PRICING,
152
+ DEEPSEEK_MODEL,
153
+ };
@@ -0,0 +1,193 @@
1
+ import { describe, it, expect, vi } from 'vitest';
2
+ import { DeepSeekAdapter, DEEPSEEK_PRICING, DEEPSEEK_MODEL } from './deepseek-adapter.js';
3
+ import { BaseAdapter } from './base-adapter.js';
4
+
5
+ describe('DeepSeekAdapter', () => {
6
+ describe('constructor', () => {
7
+ it('sets name to deepseek by default', () => {
8
+ const adapter = new DeepSeekAdapter();
9
+ expect(adapter.name).toBe('deepseek');
10
+ });
11
+
12
+ it('uses default pricing', () => {
13
+ const adapter = new DeepSeekAdapter();
14
+ expect(adapter.pricing).toBe(DEEPSEEK_PRICING);
15
+ });
16
+
17
+ it('accepts custom pricing', () => {
18
+ const customPricing = { inputPerMillion: 0.20, outputPerMillion: 0.40 };
19
+ const adapter = new DeepSeekAdapter({ pricing: customPricing });
20
+ expect(adapter.pricing).toBe(customPricing);
21
+ });
22
+ });
23
+
24
+ describe('review', () => {
25
+ it('returns empty response for empty code', async () => {
26
+ const adapter = new DeepSeekAdapter();
27
+ const result = await adapter.review('');
28
+
29
+ expect(result.issues).toEqual([]);
30
+ expect(result.model).toBe('deepseek');
31
+ });
32
+
33
+ it('returns standardized response format', async () => {
34
+ const adapter = new DeepSeekAdapter();
35
+ const result = await adapter.review('const x = 1;');
36
+
37
+ expect(BaseAdapter.validateResponse(result)).toBe(true);
38
+ expect(result.model).toBe('deepseek');
39
+ });
40
+
41
+ it('throws when budget exceeded', async () => {
42
+ const mockTracker = {
43
+ canSpend: vi.fn(() => false),
44
+ record: vi.fn(),
45
+ getUsage: vi.fn(() => ({ daily: 5, monthly: 50, requests: 100 })),
46
+ };
47
+ const adapter = new DeepSeekAdapter({ budgetTracker: mockTracker });
48
+
49
+ await expect(adapter.review('const x = 1;')).rejects.toThrow('Budget exceeded');
50
+ });
51
+
52
+ it('records cost after successful review', async () => {
53
+ const mockTracker = {
54
+ canSpend: vi.fn(() => true),
55
+ record: vi.fn(),
56
+ getUsage: vi.fn(() => ({ daily: 0, monthly: 0, requests: 0 })),
57
+ };
58
+ const adapter = new DeepSeekAdapter({ budgetTracker: mockTracker });
59
+
60
+ await adapter.review('const x = 1;');
61
+
62
+ expect(mockTracker.record).toHaveBeenCalledWith('deepseek', expect.any(Number));
63
+ });
64
+
65
+ it('handles API errors with warning instead of error', async () => {
66
+ const adapter = new DeepSeekAdapter();
67
+ adapter.callAPI = vi.fn().mockRejectedValue(new Error('Connection failed'));
68
+
69
+ const result = await adapter.review('const x = 1;');
70
+
71
+ expect(result.warning).toBe('DeepSeek unavailable: Connection failed');
72
+ expect(result.issues).toEqual([]);
73
+ expect(result.error).toBeUndefined();
74
+ });
75
+ });
76
+
77
+ describe('estimateCost', () => {
78
+ it('calculates cost based on token count', () => {
79
+ const adapter = new DeepSeekAdapter();
80
+ const cost = adapter.estimateCost(1000);
81
+
82
+ // At default pricing (R1): (500 * 0.55 + 500 * 2.19) / 1M = 0.00137
83
+ expect(cost).toBeCloseTo(0.00137, 5);
84
+ });
85
+
86
+ it('is much cheaper than OpenAI', () => {
87
+ const adapter = new DeepSeekAdapter();
88
+ const comparison = adapter.compareCostWithOpenAI(1000);
89
+
90
+ expect(comparison.deepseek).toBeLessThan(comparison.openai);
91
+ expect(comparison.savingsPercent).toBeGreaterThan(90); // >90% savings
92
+ });
93
+ });
94
+
95
+ describe('compareCostWithOpenAI', () => {
96
+ it('returns detailed cost comparison', () => {
97
+ const adapter = new DeepSeekAdapter();
98
+ const comparison = adapter.compareCostWithOpenAI(10000);
99
+
100
+ expect(comparison).toHaveProperty('deepseek');
101
+ expect(comparison).toHaveProperty('openai');
102
+ expect(comparison).toHaveProperty('savings');
103
+ expect(comparison).toHaveProperty('savingsPercent');
104
+ });
105
+
106
+ it('calculates savings correctly', () => {
107
+ const adapter = new DeepSeekAdapter();
108
+ const comparison = adapter.compareCostWithOpenAI(1000000);
109
+
110
+ // For 1M tokens:
111
+ // DeepSeek R1: (500k * 0.55 + 500k * 2.19) / 1M = 1.37
112
+ // OpenAI (comparison uses hardcoded $10/$30): (500k * 10 + 500k * 30) / 1M = 20
113
+ expect(comparison.deepseek).toBeCloseTo(1.37, 2);
114
+ expect(comparison.openai).toBeCloseTo(20, 2);
115
+ expect(comparison.savings).toBeCloseTo(18.63, 2);
116
+ });
117
+ });
118
+
119
+ describe('canAfford', () => {
120
+ it('returns true without budget tracker', () => {
121
+ const adapter = new DeepSeekAdapter();
122
+ expect(adapter.canAfford(100)).toBe(true);
123
+ });
124
+
125
+ it('checks budget tracker when available', () => {
126
+ const mockTracker = {
127
+ canSpend: vi.fn(() => true),
128
+ };
129
+ const adapter = new DeepSeekAdapter({
130
+ budgetTracker: mockTracker,
131
+ budget: { budgetDaily: 5, budgetMonthly: 50 },
132
+ });
133
+
134
+ adapter.canAfford(0.01);
135
+
136
+ expect(mockTracker.canSpend).toHaveBeenCalledWith('deepseek', 0.01, expect.any(Object));
137
+ });
138
+
139
+ it('uses lower default budget than OpenAI', () => {
140
+ const mockTracker = {
141
+ canSpend: vi.fn(() => true),
142
+ };
143
+ const adapter = new DeepSeekAdapter({ budgetTracker: mockTracker });
144
+
145
+ adapter.canAfford(0.01);
146
+
147
+ // Default budget is 5 daily, 50 monthly (lower than typical OpenAI)
148
+ expect(mockTracker.canSpend).toHaveBeenCalledWith(
149
+ 'deepseek',
150
+ 0.01,
151
+ expect.objectContaining({ budgetDaily: 5, budgetMonthly: 50 })
152
+ );
153
+ });
154
+ });
155
+
156
+ describe('getUsage', () => {
157
+ it('returns zero without budget tracker', () => {
158
+ const adapter = new DeepSeekAdapter();
159
+ expect(adapter.getUsage()).toEqual({ daily: 0, monthly: 0, requests: 0 });
160
+ });
161
+
162
+ it('returns tracker usage when available', () => {
163
+ const mockTracker = {
164
+ getUsage: vi.fn(() => ({ daily: 0.50, monthly: 5.00, requests: 100 })),
165
+ };
166
+ const adapter = new DeepSeekAdapter({ budgetTracker: mockTracker });
167
+
168
+ const usage = adapter.getUsage();
169
+
170
+ expect(usage.daily).toBe(0.50);
171
+ expect(usage.requests).toBe(100);
172
+ });
173
+ });
174
+
175
+ describe('DEEPSEEK_PRICING', () => {
176
+ it('exports pricing constants for R1', () => {
177
+ expect(DEEPSEEK_PRICING.inputPerMillion).toBe(0.55);
178
+ expect(DEEPSEEK_PRICING.outputPerMillion).toBe(2.19);
179
+ });
180
+
181
+ it('is significantly cheaper than OpenAI', () => {
182
+ // OpenAI o3: $10/1M input, $40/1M output
183
+ expect(DEEPSEEK_PRICING.inputPerMillion).toBeLessThan(10);
184
+ expect(DEEPSEEK_PRICING.outputPerMillion).toBeLessThan(40);
185
+ });
186
+ });
187
+
188
+ describe('DEEPSEEK_MODEL', () => {
189
+ it('exports latest model identifier', () => {
190
+ expect(DEEPSEEK_MODEL).toBe('deepseek-r1');
191
+ });
192
+ });
193
+ });