tlc-claude-code 1.2.26 → 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 (177) 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/PlanView.d.ts +7 -0
  42. package/dashboard/dist/components/PlanView.js +74 -2
  43. package/dashboard/dist/components/PlanView.test.js +70 -1
  44. package/dashboard/dist/components/PreviewPanel.d.ts +18 -0
  45. package/dashboard/dist/components/PreviewPanel.js +73 -0
  46. package/dashboard/dist/components/PreviewPanel.test.d.ts +1 -0
  47. package/dashboard/dist/components/PreviewPanel.test.js +124 -0
  48. package/dashboard/dist/components/ProjectCard.d.ts +18 -0
  49. package/dashboard/dist/components/ProjectCard.js +19 -0
  50. package/dashboard/dist/components/ProjectCard.test.d.ts +1 -0
  51. package/dashboard/dist/components/ProjectCard.test.js +53 -0
  52. package/dashboard/dist/components/ProjectDetail.d.ts +44 -0
  53. package/dashboard/dist/components/ProjectDetail.js +65 -0
  54. package/dashboard/dist/components/ProjectDetail.test.d.ts +1 -0
  55. package/dashboard/dist/components/ProjectDetail.test.js +196 -0
  56. package/dashboard/dist/components/ProjectList.d.ts +11 -0
  57. package/dashboard/dist/components/ProjectList.js +62 -0
  58. package/dashboard/dist/components/ProjectList.test.d.ts +1 -0
  59. package/dashboard/dist/components/ProjectList.test.js +93 -0
  60. package/dashboard/dist/components/SettingsPanel.d.ts +32 -0
  61. package/dashboard/dist/components/SettingsPanel.js +154 -0
  62. package/dashboard/dist/components/SettingsPanel.test.d.ts +1 -0
  63. package/dashboard/dist/components/SettingsPanel.test.js +196 -0
  64. package/dashboard/dist/components/StatusBar.d.ts +16 -0
  65. package/dashboard/dist/components/StatusBar.js +47 -0
  66. package/dashboard/dist/components/StatusBar.test.d.ts +1 -0
  67. package/dashboard/dist/components/StatusBar.test.js +123 -0
  68. package/dashboard/dist/components/TaskBoard.d.ts +22 -0
  69. package/dashboard/dist/components/TaskBoard.js +102 -0
  70. package/dashboard/dist/components/TaskBoard.test.d.ts +1 -0
  71. package/dashboard/dist/components/TaskBoard.test.js +113 -0
  72. package/dashboard/dist/components/TaskCard.d.ts +17 -0
  73. package/dashboard/dist/components/TaskCard.js +29 -0
  74. package/dashboard/dist/components/TaskCard.test.d.ts +1 -0
  75. package/dashboard/dist/components/TaskCard.test.js +109 -0
  76. package/dashboard/dist/components/TaskDetail.d.ts +36 -0
  77. package/dashboard/dist/components/TaskDetail.js +41 -0
  78. package/dashboard/dist/components/TaskDetail.test.d.ts +1 -0
  79. package/dashboard/dist/components/TaskDetail.test.js +164 -0
  80. package/dashboard/dist/components/TaskFilter.d.ts +12 -0
  81. package/dashboard/dist/components/TaskFilter.js +138 -0
  82. package/dashboard/dist/components/TaskFilter.test.d.ts +1 -0
  83. package/dashboard/dist/components/TaskFilter.test.js +109 -0
  84. package/dashboard/dist/components/TeamPanel.d.ts +15 -0
  85. package/dashboard/dist/components/TeamPanel.js +24 -0
  86. package/dashboard/dist/components/TeamPanel.test.d.ts +1 -0
  87. package/dashboard/dist/components/TeamPanel.test.js +109 -0
  88. package/dashboard/dist/components/TeamPresence.d.ts +14 -0
  89. package/dashboard/dist/components/TeamPresence.js +31 -0
  90. package/dashboard/dist/components/TeamPresence.test.d.ts +1 -0
  91. package/dashboard/dist/components/TeamPresence.test.js +144 -0
  92. package/dashboard/dist/components/layout/Header.d.ts +9 -0
  93. package/dashboard/dist/components/layout/Header.js +11 -0
  94. package/dashboard/dist/components/layout/Header.test.d.ts +1 -0
  95. package/dashboard/dist/components/layout/Header.test.js +35 -0
  96. package/dashboard/dist/components/layout/Shell.d.ts +10 -0
  97. package/dashboard/dist/components/layout/Shell.js +5 -0
  98. package/dashboard/dist/components/layout/Shell.test.d.ts +1 -0
  99. package/dashboard/dist/components/layout/Shell.test.js +34 -0
  100. package/dashboard/dist/components/layout/Sidebar.d.ts +14 -0
  101. package/dashboard/dist/components/layout/Sidebar.js +8 -0
  102. package/dashboard/dist/components/layout/Sidebar.test.d.ts +1 -0
  103. package/dashboard/dist/components/layout/Sidebar.test.js +40 -0
  104. package/dashboard/dist/components/ui/Badge.d.ts +9 -0
  105. package/dashboard/dist/components/ui/Badge.js +13 -0
  106. package/dashboard/dist/components/ui/Badge.test.d.ts +1 -0
  107. package/dashboard/dist/components/ui/Badge.test.js +69 -0
  108. package/dashboard/dist/components/ui/Button.d.ts +12 -0
  109. package/dashboard/dist/components/ui/Button.js +14 -0
  110. package/dashboard/dist/components/ui/Button.test.d.ts +1 -0
  111. package/dashboard/dist/components/ui/Button.test.js +81 -0
  112. package/dashboard/dist/components/ui/Card.d.ts +21 -0
  113. package/dashboard/dist/components/ui/Card.js +20 -0
  114. package/dashboard/dist/components/ui/Card.test.d.ts +1 -0
  115. package/dashboard/dist/components/ui/Card.test.js +82 -0
  116. package/dashboard/dist/components/ui/Input.d.ts +13 -0
  117. package/dashboard/dist/components/ui/Input.js +8 -0
  118. package/dashboard/dist/components/ui/Input.test.d.ts +1 -0
  119. package/dashboard/dist/components/ui/Input.test.js +68 -0
  120. package/dashboard/dist/styles/tokens.d.ts +150 -0
  121. package/dashboard/dist/styles/tokens.js +184 -0
  122. package/dashboard/dist/styles/tokens.test.d.ts +1 -0
  123. package/dashboard/dist/styles/tokens.test.js +95 -0
  124. package/dashboard/dist/test/setup.d.ts +1 -0
  125. package/dashboard/dist/test/setup.js +1 -0
  126. package/dashboard/package.json +3 -0
  127. package/package.json +1 -1
  128. package/server/dashboard/index.html +157 -2
  129. package/server/index.js +38 -21
  130. package/server/lib/adapters/base-adapter.js +114 -0
  131. package/server/lib/adapters/base-adapter.test.js +90 -0
  132. package/server/lib/adapters/claude-adapter.js +141 -0
  133. package/server/lib/adapters/claude-adapter.test.js +180 -0
  134. package/server/lib/adapters/deepseek-adapter.js +153 -0
  135. package/server/lib/adapters/deepseek-adapter.test.js +193 -0
  136. package/server/lib/adapters/openai-adapter.js +190 -0
  137. package/server/lib/adapters/openai-adapter.test.js +231 -0
  138. package/server/lib/budget-tracker.js +169 -0
  139. package/server/lib/budget-tracker.test.js +165 -0
  140. package/server/lib/claude-injector.js +85 -0
  141. package/server/lib/claude-injector.test.js +161 -0
  142. package/server/lib/consensus-engine.js +135 -0
  143. package/server/lib/consensus-engine.test.js +152 -0
  144. package/server/lib/context-builder.js +112 -0
  145. package/server/lib/context-builder.test.js +120 -0
  146. package/server/lib/file-collector.js +322 -0
  147. package/server/lib/file-collector.test.js +307 -0
  148. package/server/lib/memory-classifier.js +175 -0
  149. package/server/lib/memory-classifier.test.js +169 -0
  150. package/server/lib/memory-committer.js +138 -0
  151. package/server/lib/memory-committer.test.js +136 -0
  152. package/server/lib/memory-hooks.js +127 -0
  153. package/server/lib/memory-hooks.test.js +136 -0
  154. package/server/lib/memory-init.js +104 -0
  155. package/server/lib/memory-init.test.js +119 -0
  156. package/server/lib/memory-observer.js +149 -0
  157. package/server/lib/memory-observer.test.js +158 -0
  158. package/server/lib/memory-reader.js +243 -0
  159. package/server/lib/memory-reader.test.js +216 -0
  160. package/server/lib/memory-storage.js +120 -0
  161. package/server/lib/memory-storage.test.js +136 -0
  162. package/server/lib/memory-writer.js +176 -0
  163. package/server/lib/memory-writer.test.js +231 -0
  164. package/server/lib/overdrive-command.js +30 -6
  165. package/server/lib/overdrive-command.test.js +8 -1
  166. package/server/lib/pattern-detector.js +216 -0
  167. package/server/lib/pattern-detector.test.js +241 -0
  168. package/server/lib/relevance-scorer.js +175 -0
  169. package/server/lib/relevance-scorer.test.js +107 -0
  170. package/server/lib/review-command.js +238 -0
  171. package/server/lib/review-command.test.js +245 -0
  172. package/server/lib/review-orchestrator.js +273 -0
  173. package/server/lib/review-orchestrator.test.js +300 -0
  174. package/server/lib/review-reporter.js +288 -0
  175. package/server/lib/review-reporter.test.js +240 -0
  176. package/server/lib/session-summary.js +90 -0
  177. package/server/lib/session-summary.test.js +156 -0
@@ -248,6 +248,60 @@
248
248
  color: #e6edf3;
249
249
  margin-right: 10px;
250
250
  }
251
+ .image-drop-zone {
252
+ border: 2px dashed #30363d;
253
+ border-radius: 8px;
254
+ padding: 15px;
255
+ margin-bottom: 10px;
256
+ text-align: center;
257
+ cursor: pointer;
258
+ transition: all 0.2s;
259
+ min-height: 60px;
260
+ }
261
+ .image-drop-zone:hover, .image-drop-zone.dragover {
262
+ border-color: #58a6ff;
263
+ background: rgba(88, 166, 255, 0.1);
264
+ }
265
+ .image-drop-zone .drop-hint {
266
+ color: #8b949e;
267
+ font-size: 13px;
268
+ }
269
+ .image-previews {
270
+ display: flex;
271
+ flex-wrap: wrap;
272
+ gap: 8px;
273
+ margin-top: 10px;
274
+ }
275
+ .image-preview {
276
+ position: relative;
277
+ width: 80px;
278
+ height: 80px;
279
+ border-radius: 6px;
280
+ overflow: hidden;
281
+ border: 1px solid #30363d;
282
+ }
283
+ .image-preview img {
284
+ width: 100%;
285
+ height: 100%;
286
+ object-fit: cover;
287
+ }
288
+ .image-preview .remove-btn {
289
+ position: absolute;
290
+ top: 2px;
291
+ right: 2px;
292
+ width: 20px;
293
+ height: 20px;
294
+ background: rgba(248, 81, 73, 0.9);
295
+ border: none;
296
+ border-radius: 50%;
297
+ color: white;
298
+ cursor: pointer;
299
+ font-size: 12px;
300
+ line-height: 18px;
301
+ }
302
+ .image-preview .remove-btn:hover {
303
+ background: #f85149;
304
+ }
251
305
 
252
306
  /* Logs Panel */
253
307
  .logs-tabs {
@@ -581,6 +635,13 @@
581
635
  <div class="bug-form">
582
636
  <h3>Report New Bug</h3>
583
637
  <textarea id="bug-desc" placeholder="Describe the bug..."></textarea>
638
+ <div class="image-drop-zone" id="image-drop-zone" onclick="document.getElementById('image-input').click()">
639
+ <input type="file" id="image-input" accept="image/*" multiple style="display:none" onchange="handleImageSelect(event)">
640
+ <div class="drop-hint" id="drop-hint">
641
+ 📷 Paste (Ctrl+V) or drop images here
642
+ </div>
643
+ <div class="image-previews" id="image-previews"></div>
644
+ </div>
584
645
  <div>
585
646
  <select id="bug-severity">
586
647
  <option value="low">Low</option>
@@ -589,6 +650,7 @@
589
650
  <option value="critical">Critical</option>
590
651
  </select>
591
652
  <button class="btn btn-primary" onclick="submitBug()">Submit Bug</button>
653
+ <button class="btn" onclick="clearImages()" id="clear-images-btn" style="display:none">Clear Images</button>
592
654
  </div>
593
655
  </div>
594
656
  <div id="bugs-list">
@@ -843,6 +905,93 @@
843
905
  }
844
906
  }
845
907
 
908
+ // Bug image handling
909
+ let bugImages = [];
910
+
911
+ function setupImageHandlers() {
912
+ const dropZone = document.getElementById('image-drop-zone');
913
+
914
+ // Paste handler (global)
915
+ document.addEventListener('paste', (e) => {
916
+ const items = e.clipboardData?.items;
917
+ if (!items) return;
918
+
919
+ for (const item of items) {
920
+ if (item.type.startsWith('image/')) {
921
+ e.preventDefault();
922
+ const file = item.getAsFile();
923
+ if (file) addImage(file);
924
+ }
925
+ }
926
+ });
927
+
928
+ // Drag & drop
929
+ dropZone.addEventListener('dragover', (e) => {
930
+ e.preventDefault();
931
+ dropZone.classList.add('dragover');
932
+ });
933
+ dropZone.addEventListener('dragleave', () => {
934
+ dropZone.classList.remove('dragover');
935
+ });
936
+ dropZone.addEventListener('drop', (e) => {
937
+ e.preventDefault();
938
+ dropZone.classList.remove('dragover');
939
+ const files = e.dataTransfer.files;
940
+ for (const file of files) {
941
+ if (file.type.startsWith('image/')) addImage(file);
942
+ }
943
+ });
944
+ }
945
+
946
+ function handleImageSelect(e) {
947
+ const files = e.target.files;
948
+ for (const file of files) {
949
+ if (file.type.startsWith('image/')) addImage(file);
950
+ }
951
+ e.target.value = ''; // Reset for re-selection
952
+ }
953
+
954
+ function addImage(file) {
955
+ const reader = new FileReader();
956
+ reader.onload = (e) => {
957
+ bugImages.push({ name: file.name, data: e.target.result });
958
+ renderImagePreviews();
959
+ };
960
+ reader.readAsDataURL(file);
961
+ }
962
+
963
+ function removeImage(index) {
964
+ bugImages.splice(index, 1);
965
+ renderImagePreviews();
966
+ }
967
+
968
+ function clearImages() {
969
+ bugImages = [];
970
+ renderImagePreviews();
971
+ }
972
+
973
+ function renderImagePreviews() {
974
+ const container = document.getElementById('image-previews');
975
+ const hint = document.getElementById('drop-hint');
976
+ const clearBtn = document.getElementById('clear-images-btn');
977
+
978
+ if (bugImages.length === 0) {
979
+ container.innerHTML = '';
980
+ hint.style.display = 'block';
981
+ clearBtn.style.display = 'none';
982
+ return;
983
+ }
984
+
985
+ hint.style.display = 'none';
986
+ clearBtn.style.display = 'inline-block';
987
+ container.innerHTML = bugImages.map((img, i) => `
988
+ <div class="image-preview">
989
+ <img src="${img.data}" alt="${img.name}" title="${img.name}">
990
+ <button class="remove-btn" onclick="event.stopPropagation(); removeImage(${i})">×</button>
991
+ </div>
992
+ `).join('');
993
+ }
994
+
846
995
  async function submitBug() {
847
996
  const desc = document.getElementById('bug-desc').value.trim();
848
997
  const severity = document.getElementById('bug-severity').value;
@@ -852,12 +1001,17 @@
852
1001
  const res = await fetch('/api/bug', {
853
1002
  method: 'POST',
854
1003
  headers: { 'Content-Type': 'application/json' },
855
- body: JSON.stringify({ description: desc, severity })
1004
+ body: JSON.stringify({
1005
+ description: desc,
1006
+ severity,
1007
+ images: bugImages.map(img => img.data)
1008
+ })
856
1009
  });
857
1010
  const data = await res.json();
858
1011
  if (data.success) {
859
- alert(`Bug ${data.bugId} created!`);
1012
+ alert(`Bug ${data.bugId} created!` + (bugImages.length ? ` (${bugImages.length} image${bugImages.length > 1 ? 's' : ''} attached)` : ''));
860
1013
  document.getElementById('bug-desc').value = '';
1014
+ clearImages();
861
1015
  refreshBugs();
862
1016
  }
863
1017
  } catch (e) {
@@ -1060,6 +1214,7 @@
1060
1214
  // Initialize
1061
1215
  connect();
1062
1216
  refreshAll();
1217
+ setupImageHandlers();
1063
1218
  setInterval(refreshStats, 30000);
1064
1219
  setInterval(refreshProgress, 30000);
1065
1220
  </script>
package/server/index.js CHANGED
@@ -373,7 +373,7 @@ app.post('/api/playwright', (req, res) => {
373
373
  });
374
374
 
375
375
  app.post('/api/bug', (req, res) => {
376
- const { description, url, screenshot, severity } = req.body;
376
+ const { description, url, screenshot, severity, images } = req.body;
377
377
 
378
378
  if (!description) {
379
379
  return res.status(400).json({ error: 'Description required' });
@@ -386,40 +386,57 @@ app.post('/api/bug', (req, res) => {
386
386
  const nextId = bugs.length + 1;
387
387
  const bugId = `BUG-${String(nextId).padStart(3, '0')}`;
388
388
 
389
+ // Ensure .planning directory exists
390
+ const planningDir = path.join(PROJECT_DIR, '.planning');
391
+ if (!fs.existsSync(planningDir)) {
392
+ fs.mkdirSync(planningDir, { recursive: true });
393
+ }
394
+
395
+ // Handle multiple images (new) or single screenshot (legacy)
396
+ const allImages = images || (screenshot ? [screenshot] : []);
397
+ const savedImages = [];
398
+
399
+ if (allImages.length > 0) {
400
+ const screenshotDir = path.join(planningDir, 'screenshots');
401
+ if (!fs.existsSync(screenshotDir)) {
402
+ fs.mkdirSync(screenshotDir, { recursive: true });
403
+ }
404
+
405
+ allImages.forEach((imgData, index) => {
406
+ if (imgData && imgData.startsWith('data:image')) {
407
+ const ext = imgData.includes('image/png') ? 'png' : 'jpg';
408
+ const filename = allImages.length === 1
409
+ ? `${bugId}.${ext}`
410
+ : `${bugId}-${index + 1}.${ext}`;
411
+ const base64Data = imgData.split(',')[1];
412
+ fs.writeFileSync(
413
+ path.join(screenshotDir, filename),
414
+ Buffer.from(base64Data, 'base64')
415
+ );
416
+ savedImages.push(`screenshots/${filename}`);
417
+ }
418
+ });
419
+ }
420
+
389
421
  // Create bug entry
390
422
  const timestamp = new Date().toISOString().split('T')[0];
423
+ const imagesMarkdown = savedImages.length > 0
424
+ ? `- **Attachments:** ${savedImages.map(img => `![](${img})`).join(' ')}`
425
+ : '';
426
+
391
427
  const bugEntry = `
392
428
  ### ${bugId}: ${description.split('\n')[0].slice(0, 50)} [open]
393
429
 
394
430
  - **Reported:** ${timestamp}
395
431
  - **Severity:** ${severity || 'medium'}
396
432
  - **URL:** ${url || 'N/A'}
397
- ${screenshot ? `- **Screenshot:** screenshots/${bugId}.png` : ''}
433
+ ${imagesMarkdown}
398
434
 
399
435
  ${description}
400
436
 
401
437
  ---
402
438
  `;
403
439
 
404
- // Ensure .planning directory exists
405
- const planningDir = path.join(PROJECT_DIR, '.planning');
406
- if (!fs.existsSync(planningDir)) {
407
- fs.mkdirSync(planningDir, { recursive: true });
408
- }
409
-
410
- // Save screenshot if provided
411
- if (screenshot && screenshot.startsWith('data:image')) {
412
- const screenshotDir = path.join(planningDir, 'screenshots');
413
- if (!fs.existsSync(screenshotDir)) {
414
- fs.mkdirSync(screenshotDir, { recursive: true });
415
- }
416
- const base64Data = screenshot.split(',')[1];
417
- fs.writeFileSync(
418
- path.join(screenshotDir, `${bugId}.png`),
419
- Buffer.from(base64Data, 'base64')
420
- );
421
- }
422
-
423
440
  // Append to BUGS.md
424
441
  let content = '';
425
442
  if (fs.existsSync(bugsFile)) {
@@ -0,0 +1,114 @@
1
+ /**
2
+ * Base Adapter - Common interface for all model adapters
3
+ */
4
+
5
+ /**
6
+ * Schema for standardized review response
7
+ */
8
+ const REVIEW_RESPONSE_SCHEMA = {
9
+ issues: 'array', // Array of { id, severity, message, line?, suggestion? }
10
+ suggestions: 'array', // Array of strings
11
+ score: 'number', // 0-100 quality score
12
+ model: 'string', // Model name
13
+ tokensUsed: 'number', // Tokens consumed
14
+ cost: 'number', // Cost in USD
15
+ };
16
+
17
+ /**
18
+ * Base adapter class - all model adapters extend this
19
+ */
20
+ class BaseAdapter {
21
+ constructor(config = {}) {
22
+ this.config = config;
23
+ this.name = config.name || 'unknown';
24
+ }
25
+
26
+ /**
27
+ * Review code for issues and suggestions
28
+ * @param {string} code - Code to review
29
+ * @param {Object} context - Additional context
30
+ * @returns {Promise<Object>} Standardized review response
31
+ */
32
+ async review(code, context = {}) {
33
+ throw new Error('Not implemented: review');
34
+ }
35
+
36
+ /**
37
+ * Analyze codebase for patterns
38
+ * @param {string} codebase - Codebase path or content
39
+ * @param {string} query - Analysis query
40
+ * @returns {Promise<Object>} Analysis result
41
+ */
42
+ async analyze(codebase, query) {
43
+ throw new Error('Not implemented: analyze');
44
+ }
45
+
46
+ /**
47
+ * Get current usage stats
48
+ * @returns {Object} Usage stats { daily, monthly, requests }
49
+ */
50
+ getUsage() {
51
+ return { daily: 0, monthly: 0, requests: 0 };
52
+ }
53
+
54
+ /**
55
+ * Estimate cost for a request
56
+ * @param {number} tokens - Estimated tokens
57
+ * @returns {number} Estimated cost in USD
58
+ */
59
+ estimateCost(tokens) {
60
+ return 0;
61
+ }
62
+
63
+ /**
64
+ * Check if adapter can afford a request
65
+ * @param {number} estimatedCost - Estimated cost
66
+ * @returns {boolean} Whether request is within budget
67
+ */
68
+ canAfford(estimatedCost = 0) {
69
+ return true;
70
+ }
71
+
72
+ /**
73
+ * Create empty but valid response
74
+ * @returns {Object} Empty review response
75
+ */
76
+ createEmptyResponse() {
77
+ return {
78
+ issues: [],
79
+ suggestions: [],
80
+ score: 100,
81
+ model: this.name,
82
+ tokensUsed: 0,
83
+ cost: 0,
84
+ };
85
+ }
86
+
87
+ /**
88
+ * Validate response against schema
89
+ * @param {Object} response - Response to validate
90
+ * @returns {boolean} Whether response is valid
91
+ */
92
+ static validateResponse(response) {
93
+ if (!response) return false;
94
+
95
+ // Check required fields
96
+ for (const [field, type] of Object.entries(REVIEW_RESPONSE_SCHEMA)) {
97
+ if (!(field in response)) return false;
98
+
99
+ if (type === 'array' && !Array.isArray(response[field])) return false;
100
+ if (type === 'number' && typeof response[field] !== 'number') return false;
101
+ if (type === 'string' && typeof response[field] !== 'string') return false;
102
+ }
103
+
104
+ // Validate score range
105
+ if (response.score < 0 || response.score > 100) return false;
106
+
107
+ return true;
108
+ }
109
+ }
110
+
111
+ module.exports = {
112
+ BaseAdapter,
113
+ REVIEW_RESPONSE_SCHEMA,
114
+ };
@@ -0,0 +1,90 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { BaseAdapter, REVIEW_RESPONSE_SCHEMA } from './base-adapter.js';
3
+
4
+ describe('BaseAdapter', () => {
5
+ describe('interface', () => {
6
+ it('has required methods', () => {
7
+ const adapter = new BaseAdapter({ name: 'test' });
8
+
9
+ expect(typeof adapter.review).toBe('function');
10
+ expect(typeof adapter.analyze).toBe('function');
11
+ expect(typeof adapter.getUsage).toBe('function');
12
+ expect(typeof adapter.estimateCost).toBe('function');
13
+ expect(typeof adapter.canAfford).toBe('function');
14
+ });
15
+
16
+ it('has name property', () => {
17
+ const adapter = new BaseAdapter({ name: 'claude' });
18
+ expect(adapter.name).toBe('claude');
19
+ });
20
+
21
+ it('throws on unimplemented review', async () => {
22
+ const adapter = new BaseAdapter({ name: 'test' });
23
+ await expect(adapter.review('')).rejects.toThrow('Not implemented');
24
+ });
25
+
26
+ it('throws on unimplemented analyze', async () => {
27
+ const adapter = new BaseAdapter({ name: 'test' });
28
+ await expect(adapter.analyze('', '')).rejects.toThrow('Not implemented');
29
+ });
30
+ });
31
+
32
+ describe('response validation', () => {
33
+ it('validates correct response', () => {
34
+ const response = {
35
+ issues: [{ id: '1', severity: 'high', message: 'Test' }],
36
+ suggestions: ['Suggestion 1'],
37
+ score: 75,
38
+ model: 'claude',
39
+ tokensUsed: 1000,
40
+ cost: 0.01,
41
+ };
42
+
43
+ expect(BaseAdapter.validateResponse(response)).toBe(true);
44
+ });
45
+
46
+ it('rejects missing required fields', () => {
47
+ const response = {
48
+ issues: [],
49
+ // missing: suggestions, score, model, tokensUsed, cost
50
+ };
51
+
52
+ expect(BaseAdapter.validateResponse(response)).toBe(false);
53
+ });
54
+
55
+ it('rejects invalid score', () => {
56
+ const response = {
57
+ issues: [],
58
+ suggestions: [],
59
+ score: 150, // Invalid: > 100
60
+ model: 'test',
61
+ tokensUsed: 0,
62
+ cost: 0,
63
+ };
64
+
65
+ expect(BaseAdapter.validateResponse(response)).toBe(false);
66
+ });
67
+ });
68
+
69
+ describe('REVIEW_RESPONSE_SCHEMA', () => {
70
+ it('defines all required fields', () => {
71
+ expect(REVIEW_RESPONSE_SCHEMA).toHaveProperty('issues');
72
+ expect(REVIEW_RESPONSE_SCHEMA).toHaveProperty('suggestions');
73
+ expect(REVIEW_RESPONSE_SCHEMA).toHaveProperty('score');
74
+ expect(REVIEW_RESPONSE_SCHEMA).toHaveProperty('model');
75
+ expect(REVIEW_RESPONSE_SCHEMA).toHaveProperty('tokensUsed');
76
+ expect(REVIEW_RESPONSE_SCHEMA).toHaveProperty('cost');
77
+ });
78
+ });
79
+
80
+ describe('createEmptyResponse', () => {
81
+ it('creates valid empty response', () => {
82
+ const adapter = new BaseAdapter({ name: 'test' });
83
+ const response = adapter.createEmptyResponse();
84
+
85
+ expect(BaseAdapter.validateResponse(response)).toBe(true);
86
+ expect(response.issues).toEqual([]);
87
+ expect(response.model).toBe('test');
88
+ });
89
+ });
90
+ });
@@ -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
+ };