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.
- package/dashboard/dist/components/ActivityFeed.d.ts +17 -0
- package/dashboard/dist/components/ActivityFeed.js +42 -0
- package/dashboard/dist/components/ActivityFeed.test.d.ts +1 -0
- package/dashboard/dist/components/ActivityFeed.test.js +162 -0
- package/dashboard/dist/components/BranchSelector.d.ts +16 -0
- package/dashboard/dist/components/BranchSelector.js +49 -0
- package/dashboard/dist/components/BranchSelector.test.d.ts +1 -0
- package/dashboard/dist/components/BranchSelector.test.js +166 -0
- package/dashboard/dist/components/CommandPalette.d.ts +17 -0
- package/dashboard/dist/components/CommandPalette.js +118 -0
- package/dashboard/dist/components/CommandPalette.test.d.ts +1 -0
- package/dashboard/dist/components/CommandPalette.test.js +181 -0
- package/dashboard/dist/components/ConnectionStatus.d.ts +16 -0
- package/dashboard/dist/components/ConnectionStatus.js +27 -0
- package/dashboard/dist/components/ConnectionStatus.test.d.ts +1 -0
- package/dashboard/dist/components/ConnectionStatus.test.js +121 -0
- package/dashboard/dist/components/DeviceFrame.d.ts +19 -0
- package/dashboard/dist/components/DeviceFrame.js +52 -0
- package/dashboard/dist/components/DeviceFrame.test.d.ts +1 -0
- package/dashboard/dist/components/DeviceFrame.test.js +118 -0
- package/dashboard/dist/components/EnvironmentBadge.d.ts +11 -0
- package/dashboard/dist/components/EnvironmentBadge.js +16 -0
- package/dashboard/dist/components/EnvironmentBadge.test.d.ts +1 -0
- package/dashboard/dist/components/EnvironmentBadge.test.js +102 -0
- package/dashboard/dist/components/FocusIndicator.d.ts +19 -0
- package/dashboard/dist/components/FocusIndicator.js +47 -0
- package/dashboard/dist/components/FocusIndicator.test.d.ts +1 -0
- package/dashboard/dist/components/FocusIndicator.test.js +117 -0
- package/dashboard/dist/components/KeyboardHelp.d.ts +15 -0
- package/dashboard/dist/components/KeyboardHelp.js +61 -0
- package/dashboard/dist/components/KeyboardHelp.test.d.ts +1 -0
- package/dashboard/dist/components/KeyboardHelp.test.js +131 -0
- package/dashboard/dist/components/LogSearch.d.ts +13 -0
- package/dashboard/dist/components/LogSearch.js +43 -0
- package/dashboard/dist/components/LogSearch.test.d.ts +1 -0
- package/dashboard/dist/components/LogSearch.test.js +100 -0
- package/dashboard/dist/components/LogStream.d.ts +21 -0
- package/dashboard/dist/components/LogStream.js +123 -0
- package/dashboard/dist/components/LogStream.test.d.ts +1 -0
- package/dashboard/dist/components/LogStream.test.js +159 -0
- package/dashboard/dist/components/PlanView.d.ts +7 -0
- package/dashboard/dist/components/PlanView.js +74 -2
- package/dashboard/dist/components/PlanView.test.js +70 -1
- package/dashboard/dist/components/PreviewPanel.d.ts +18 -0
- package/dashboard/dist/components/PreviewPanel.js +73 -0
- package/dashboard/dist/components/PreviewPanel.test.d.ts +1 -0
- package/dashboard/dist/components/PreviewPanel.test.js +124 -0
- package/dashboard/dist/components/ProjectCard.d.ts +18 -0
- package/dashboard/dist/components/ProjectCard.js +19 -0
- package/dashboard/dist/components/ProjectCard.test.d.ts +1 -0
- package/dashboard/dist/components/ProjectCard.test.js +53 -0
- package/dashboard/dist/components/ProjectDetail.d.ts +44 -0
- package/dashboard/dist/components/ProjectDetail.js +65 -0
- package/dashboard/dist/components/ProjectDetail.test.d.ts +1 -0
- package/dashboard/dist/components/ProjectDetail.test.js +196 -0
- package/dashboard/dist/components/ProjectList.d.ts +11 -0
- package/dashboard/dist/components/ProjectList.js +62 -0
- package/dashboard/dist/components/ProjectList.test.d.ts +1 -0
- package/dashboard/dist/components/ProjectList.test.js +93 -0
- package/dashboard/dist/components/SettingsPanel.d.ts +32 -0
- package/dashboard/dist/components/SettingsPanel.js +154 -0
- package/dashboard/dist/components/SettingsPanel.test.d.ts +1 -0
- package/dashboard/dist/components/SettingsPanel.test.js +196 -0
- package/dashboard/dist/components/StatusBar.d.ts +16 -0
- package/dashboard/dist/components/StatusBar.js +47 -0
- package/dashboard/dist/components/StatusBar.test.d.ts +1 -0
- package/dashboard/dist/components/StatusBar.test.js +123 -0
- package/dashboard/dist/components/TaskBoard.d.ts +22 -0
- package/dashboard/dist/components/TaskBoard.js +102 -0
- package/dashboard/dist/components/TaskBoard.test.d.ts +1 -0
- package/dashboard/dist/components/TaskBoard.test.js +113 -0
- package/dashboard/dist/components/TaskCard.d.ts +17 -0
- package/dashboard/dist/components/TaskCard.js +29 -0
- package/dashboard/dist/components/TaskCard.test.d.ts +1 -0
- package/dashboard/dist/components/TaskCard.test.js +109 -0
- package/dashboard/dist/components/TaskDetail.d.ts +36 -0
- package/dashboard/dist/components/TaskDetail.js +41 -0
- package/dashboard/dist/components/TaskDetail.test.d.ts +1 -0
- package/dashboard/dist/components/TaskDetail.test.js +164 -0
- package/dashboard/dist/components/TaskFilter.d.ts +12 -0
- package/dashboard/dist/components/TaskFilter.js +138 -0
- package/dashboard/dist/components/TaskFilter.test.d.ts +1 -0
- package/dashboard/dist/components/TaskFilter.test.js +109 -0
- package/dashboard/dist/components/TeamPanel.d.ts +15 -0
- package/dashboard/dist/components/TeamPanel.js +24 -0
- package/dashboard/dist/components/TeamPanel.test.d.ts +1 -0
- package/dashboard/dist/components/TeamPanel.test.js +109 -0
- package/dashboard/dist/components/TeamPresence.d.ts +14 -0
- package/dashboard/dist/components/TeamPresence.js +31 -0
- package/dashboard/dist/components/TeamPresence.test.d.ts +1 -0
- package/dashboard/dist/components/TeamPresence.test.js +144 -0
- package/dashboard/dist/components/layout/Header.d.ts +9 -0
- package/dashboard/dist/components/layout/Header.js +11 -0
- package/dashboard/dist/components/layout/Header.test.d.ts +1 -0
- package/dashboard/dist/components/layout/Header.test.js +35 -0
- package/dashboard/dist/components/layout/Shell.d.ts +10 -0
- package/dashboard/dist/components/layout/Shell.js +5 -0
- package/dashboard/dist/components/layout/Shell.test.d.ts +1 -0
- package/dashboard/dist/components/layout/Shell.test.js +34 -0
- package/dashboard/dist/components/layout/Sidebar.d.ts +14 -0
- package/dashboard/dist/components/layout/Sidebar.js +8 -0
- package/dashboard/dist/components/layout/Sidebar.test.d.ts +1 -0
- package/dashboard/dist/components/layout/Sidebar.test.js +40 -0
- package/dashboard/dist/components/ui/Badge.d.ts +9 -0
- package/dashboard/dist/components/ui/Badge.js +13 -0
- package/dashboard/dist/components/ui/Badge.test.d.ts +1 -0
- package/dashboard/dist/components/ui/Badge.test.js +69 -0
- package/dashboard/dist/components/ui/Button.d.ts +12 -0
- package/dashboard/dist/components/ui/Button.js +14 -0
- package/dashboard/dist/components/ui/Button.test.d.ts +1 -0
- package/dashboard/dist/components/ui/Button.test.js +81 -0
- package/dashboard/dist/components/ui/Card.d.ts +21 -0
- package/dashboard/dist/components/ui/Card.js +20 -0
- package/dashboard/dist/components/ui/Card.test.d.ts +1 -0
- package/dashboard/dist/components/ui/Card.test.js +82 -0
- package/dashboard/dist/components/ui/Input.d.ts +13 -0
- package/dashboard/dist/components/ui/Input.js +8 -0
- package/dashboard/dist/components/ui/Input.test.d.ts +1 -0
- package/dashboard/dist/components/ui/Input.test.js +68 -0
- package/dashboard/dist/styles/tokens.d.ts +150 -0
- package/dashboard/dist/styles/tokens.js +184 -0
- package/dashboard/dist/styles/tokens.test.d.ts +1 -0
- package/dashboard/dist/styles/tokens.test.js +95 -0
- package/dashboard/dist/test/setup.d.ts +1 -0
- package/dashboard/dist/test/setup.js +1 -0
- package/dashboard/package.json +3 -0
- package/package.json +1 -1
- package/server/dashboard/index.html +157 -2
- package/server/index.js +38 -21
- package/server/lib/adapters/base-adapter.js +114 -0
- package/server/lib/adapters/base-adapter.test.js +90 -0
- package/server/lib/adapters/claude-adapter.js +141 -0
- package/server/lib/adapters/claude-adapter.test.js +180 -0
- package/server/lib/adapters/deepseek-adapter.js +153 -0
- package/server/lib/adapters/deepseek-adapter.test.js +193 -0
- package/server/lib/adapters/openai-adapter.js +190 -0
- package/server/lib/adapters/openai-adapter.test.js +231 -0
- package/server/lib/budget-tracker.js +169 -0
- package/server/lib/budget-tracker.test.js +165 -0
- package/server/lib/claude-injector.js +85 -0
- package/server/lib/claude-injector.test.js +161 -0
- package/server/lib/consensus-engine.js +135 -0
- package/server/lib/consensus-engine.test.js +152 -0
- package/server/lib/context-builder.js +112 -0
- package/server/lib/context-builder.test.js +120 -0
- package/server/lib/file-collector.js +322 -0
- package/server/lib/file-collector.test.js +307 -0
- package/server/lib/memory-classifier.js +175 -0
- package/server/lib/memory-classifier.test.js +169 -0
- package/server/lib/memory-committer.js +138 -0
- package/server/lib/memory-committer.test.js +136 -0
- package/server/lib/memory-hooks.js +127 -0
- package/server/lib/memory-hooks.test.js +136 -0
- package/server/lib/memory-init.js +104 -0
- package/server/lib/memory-init.test.js +119 -0
- package/server/lib/memory-observer.js +149 -0
- package/server/lib/memory-observer.test.js +158 -0
- package/server/lib/memory-reader.js +243 -0
- package/server/lib/memory-reader.test.js +216 -0
- package/server/lib/memory-storage.js +120 -0
- package/server/lib/memory-storage.test.js +136 -0
- package/server/lib/memory-writer.js +176 -0
- package/server/lib/memory-writer.test.js +231 -0
- package/server/lib/overdrive-command.js +30 -6
- package/server/lib/overdrive-command.test.js +8 -1
- package/server/lib/pattern-detector.js +216 -0
- package/server/lib/pattern-detector.test.js +241 -0
- package/server/lib/relevance-scorer.js +175 -0
- package/server/lib/relevance-scorer.test.js +107 -0
- package/server/lib/review-command.js +238 -0
- package/server/lib/review-command.test.js +245 -0
- package/server/lib/review-orchestrator.js +273 -0
- package/server/lib/review-orchestrator.test.js +300 -0
- package/server/lib/review-reporter.js +288 -0
- package/server/lib/review-reporter.test.js +240 -0
- package/server/lib/session-summary.js +90 -0
- 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({
|
|
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 => ``).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
|
-
${
|
|
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
|
+
};
|