knoxis-collab 1.3.0 → 1.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/knoxis-collab.js +412 -53
- package/package.json +2 -2
package/knoxis-collab.js
CHANGED
|
@@ -38,8 +38,22 @@
|
|
|
38
38
|
const { spawn, spawnSync } = require('child_process');
|
|
39
39
|
const crypto = require('crypto');
|
|
40
40
|
const https = require('https');
|
|
41
|
+
|
|
42
|
+
// Create HTTP agents with connection keep-alive
|
|
43
|
+
const httpAgent = new (require('http').Agent)({
|
|
44
|
+
keepAlive: true,
|
|
45
|
+
keepAliveMsecs: 1000,
|
|
46
|
+
maxSockets: 10
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
const httpsAgent = new https.Agent({
|
|
50
|
+
keepAlive: true,
|
|
51
|
+
keepAliveMsecs: 1000,
|
|
52
|
+
maxSockets: 10
|
|
53
|
+
});
|
|
41
54
|
const readline = require('readline');
|
|
42
55
|
const fs = require('fs');
|
|
56
|
+
const fsPromises = require('fs').promises;
|
|
43
57
|
const path = require('path');
|
|
44
58
|
const os = require('os');
|
|
45
59
|
|
|
@@ -71,7 +85,20 @@ const cliArgs = parseArgs();
|
|
|
71
85
|
const WORKSPACE = cliArgs.workspace || process.env.KNOXIS_WORKSPACE || process.cwd();
|
|
72
86
|
|
|
73
87
|
// Load config
|
|
74
|
-
function loadConfig() {
|
|
88
|
+
async function loadConfig() {
|
|
89
|
+
try {
|
|
90
|
+
if (fs.existsSync(CONFIG_PATH)) {
|
|
91
|
+
const data = await fsPromises.readFile(CONFIG_PATH, 'utf8');
|
|
92
|
+
return JSON.parse(data);
|
|
93
|
+
}
|
|
94
|
+
} catch (e) {
|
|
95
|
+
console.error(`Failed to load config: ${e.message}`);
|
|
96
|
+
}
|
|
97
|
+
return {};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Synchronous config load for initialization
|
|
101
|
+
function loadConfigSync() {
|
|
75
102
|
try {
|
|
76
103
|
if (fs.existsSync(CONFIG_PATH)) {
|
|
77
104
|
return JSON.parse(fs.readFileSync(CONFIG_PATH, 'utf8'));
|
|
@@ -80,7 +107,7 @@ function loadConfig() {
|
|
|
80
107
|
return {};
|
|
81
108
|
}
|
|
82
109
|
|
|
83
|
-
const config =
|
|
110
|
+
const config = loadConfigSync();
|
|
84
111
|
const GROQ_API_KEY = process.env.GROQ_API_KEY || config.groqApiKey || '';
|
|
85
112
|
const BACKEND_URL = process.env.KNOXIS_BACKEND_URL || config.backendUrl || '';
|
|
86
113
|
const USER_ID = process.env.KNOXIS_USER_ID || config.userId || '';
|
|
@@ -153,6 +180,7 @@ let isClaudeRunning = false;
|
|
|
153
180
|
let activeClaudeProc = null;
|
|
154
181
|
const conversationHistory = []; // Groq message history
|
|
155
182
|
const dispatchSummaries = []; // Short summaries of what Claude did each dispatch
|
|
183
|
+
const MAX_DISPATCH_SUMMARIES = 20; // Limit dispatch summaries for memory
|
|
156
184
|
const aiInsights = { // AI decision-making layer state
|
|
157
185
|
projectType: null,
|
|
158
186
|
complexity: 'medium',
|
|
@@ -174,6 +202,16 @@ const feedbackState = {
|
|
|
174
202
|
awaitingRating: false // Whether we're waiting for a rating
|
|
175
203
|
};
|
|
176
204
|
|
|
205
|
+
// Code review and commenting state
|
|
206
|
+
const codeReview = {
|
|
207
|
+
pendingReviews: [], // Code blocks awaiting review
|
|
208
|
+
reviewHistory: [], // Past code reviews
|
|
209
|
+
inlineComments: new Map(), // File -> line -> comment mapping
|
|
210
|
+
activeReviewSession: null, // Current review session ID
|
|
211
|
+
autoReviewEnabled: false, // Auto-review on code changes
|
|
212
|
+
lastReviewedCode: null // Cache last reviewed code
|
|
213
|
+
};
|
|
214
|
+
|
|
177
215
|
// ═══════════════════════════════════════════════════════════════
|
|
178
216
|
// SESSION LOG & PERSISTENCE
|
|
179
217
|
// ═══════════════════════════════════════════════════════════════
|
|
@@ -187,16 +225,36 @@ const logFile = path.join(
|
|
|
187
225
|
`${new Date().toISOString().replace(/[:.]/g, '-')}-collab-${SESSION_ID.slice(0, 8)}.log`
|
|
188
226
|
);
|
|
189
227
|
|
|
228
|
+
// Async logging with queue for better performance
|
|
229
|
+
const logQueue = [];
|
|
230
|
+
let logTimer = null;
|
|
231
|
+
|
|
190
232
|
function log(entry) {
|
|
191
233
|
const line = `[${new Date().toISOString()}] ${entry}\n`;
|
|
192
|
-
|
|
234
|
+
logQueue.push(line);
|
|
235
|
+
|
|
236
|
+
// Batch write logs every 100ms
|
|
237
|
+
if (!logTimer) {
|
|
238
|
+
logTimer = setTimeout(async () => {
|
|
239
|
+
if (logQueue.length > 0) {
|
|
240
|
+
const batch = logQueue.splice(0, logQueue.length).join('');
|
|
241
|
+
try {
|
|
242
|
+
await fsPromises.appendFile(logFile, batch);
|
|
243
|
+
} catch (e) {
|
|
244
|
+
console.error(`Log write failed: ${e.message}`);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
logTimer = null;
|
|
248
|
+
}, 100);
|
|
249
|
+
}
|
|
193
250
|
}
|
|
194
251
|
|
|
195
252
|
// Load persisted feedback data
|
|
196
|
-
function loadFeedbackData() {
|
|
253
|
+
async function loadFeedbackData() {
|
|
197
254
|
try {
|
|
198
255
|
if (fs.existsSync(feedbackFile)) {
|
|
199
|
-
const
|
|
256
|
+
const content = await fsPromises.readFile(feedbackFile, 'utf8');
|
|
257
|
+
const data = JSON.parse(content);
|
|
200
258
|
|
|
201
259
|
// Restore pattern weights
|
|
202
260
|
if (data.patternWeights) {
|
|
@@ -225,7 +283,7 @@ function loadFeedbackData() {
|
|
|
225
283
|
}
|
|
226
284
|
|
|
227
285
|
// Save feedback data for persistence
|
|
228
|
-
function saveFeedbackData() {
|
|
286
|
+
async function saveFeedbackData() {
|
|
229
287
|
try {
|
|
230
288
|
const data = {
|
|
231
289
|
version: '1.0',
|
|
@@ -235,13 +293,147 @@ function saveFeedbackData() {
|
|
|
235
293
|
feedbackHistory: aiInsights.feedbackHistory.slice(-50) // Keep last 50
|
|
236
294
|
};
|
|
237
295
|
|
|
238
|
-
|
|
296
|
+
await fsPromises.writeFile(feedbackFile, JSON.stringify(data, null, 2));
|
|
239
297
|
log(`Saved feedback data: ${aiInsights.patternWeights.size} patterns, ${aiInsights.suggestionScores.size} suggestions`);
|
|
240
298
|
} catch (e) {
|
|
241
299
|
log(`Failed to save feedback data: ${e.message}`);
|
|
242
300
|
}
|
|
243
301
|
}
|
|
244
302
|
|
|
303
|
+
// ═══════════════════════════════════════════════════════════════
|
|
304
|
+
// CODE REVIEW AND COMMENTING FUNCTIONS
|
|
305
|
+
// ═══════════════════════════════════════════════════════════════
|
|
306
|
+
|
|
307
|
+
// Review code with inline comments
|
|
308
|
+
async function reviewCode(code, filePath, startLine = 1) {
|
|
309
|
+
const reviewId = crypto.randomUUID().substring(0, 8);
|
|
310
|
+
codeReview.activeReviewSession = reviewId;
|
|
311
|
+
|
|
312
|
+
const review = {
|
|
313
|
+
id: reviewId,
|
|
314
|
+
timestamp: Date.now(),
|
|
315
|
+
filePath: filePath,
|
|
316
|
+
code: code,
|
|
317
|
+
startLine: startLine,
|
|
318
|
+
comments: [],
|
|
319
|
+
summary: null
|
|
320
|
+
};
|
|
321
|
+
|
|
322
|
+
// Call Groq for code review
|
|
323
|
+
const userPrompt = `You are a senior code reviewer. Review the following code and provide:
|
|
324
|
+
1. Inline comments for specific lines (format: "Line X: comment")
|
|
325
|
+
2. General feedback and suggestions
|
|
326
|
+
3. Identify potential issues, bugs, or improvements
|
|
327
|
+
Be constructive and specific. Focus on code quality, performance, security, and maintainability.
|
|
328
|
+
|
|
329
|
+
Review this code from ${filePath} starting at line ${startLine}:
|
|
330
|
+
|
|
331
|
+
${code}`;
|
|
332
|
+
|
|
333
|
+
try {
|
|
334
|
+
const reviewResponse = await callGroq(
|
|
335
|
+
[{ role: 'user', content: userPrompt }],
|
|
336
|
+
projectContext
|
|
337
|
+
);
|
|
338
|
+
|
|
339
|
+
// Extract text from Groq response object
|
|
340
|
+
const reviewText = reviewResponse.message || JSON.stringify(reviewResponse);
|
|
341
|
+
|
|
342
|
+
// Parse inline comments from response
|
|
343
|
+
const lines = reviewText.split('\n');
|
|
344
|
+
const inlineCommentPattern = /^Line\s+(\d+):\s*(.+)/i;
|
|
345
|
+
|
|
346
|
+
lines.forEach(line => {
|
|
347
|
+
const match = line.match(inlineCommentPattern);
|
|
348
|
+
if (match) {
|
|
349
|
+
const lineNum = parseInt(match[1]) + startLine - 1;
|
|
350
|
+
const comment = match[2].trim();
|
|
351
|
+
|
|
352
|
+
review.comments.push({
|
|
353
|
+
line: lineNum,
|
|
354
|
+
comment: comment,
|
|
355
|
+
severity: detectSeverity(comment)
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
// Store in inline comments map
|
|
359
|
+
if (!codeReview.inlineComments.has(filePath)) {
|
|
360
|
+
codeReview.inlineComments.set(filePath, new Map());
|
|
361
|
+
}
|
|
362
|
+
codeReview.inlineComments.get(filePath).set(lineNum, comment);
|
|
363
|
+
}
|
|
364
|
+
});
|
|
365
|
+
|
|
366
|
+
review.summary = reviewText;
|
|
367
|
+
codeReview.reviewHistory.push(review);
|
|
368
|
+
|
|
369
|
+
// Keep review history bounded
|
|
370
|
+
if (codeReview.reviewHistory.length > 10) {
|
|
371
|
+
codeReview.reviewHistory.shift();
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
return review;
|
|
375
|
+
} catch (e) {
|
|
376
|
+
log(`Code review failed: ${e.message}`);
|
|
377
|
+
return null;
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
// Detect severity of comment (error, warning, info)
|
|
382
|
+
function detectSeverity(comment) {
|
|
383
|
+
const lowerComment = comment.toLowerCase();
|
|
384
|
+
if (lowerComment.includes('error') || lowerComment.includes('bug') ||
|
|
385
|
+
lowerComment.includes('critical') || lowerComment.includes('security')) {
|
|
386
|
+
return 'error';
|
|
387
|
+
} else if (lowerComment.includes('warning') || lowerComment.includes('potential') ||
|
|
388
|
+
lowerComment.includes('consider') || lowerComment.includes('performance')) {
|
|
389
|
+
return 'warning';
|
|
390
|
+
}
|
|
391
|
+
return 'info';
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
// Format and display code review results
|
|
395
|
+
function displayCodeReview(review) {
|
|
396
|
+
if (!review) return;
|
|
397
|
+
|
|
398
|
+
console.log(`\n ${C.cyan}━━━ Code Review (${review.id}) ━━━${C.reset}`);
|
|
399
|
+
console.log(` ${C.dim}File: ${review.filePath}${C.reset}`);
|
|
400
|
+
|
|
401
|
+
// Display inline comments
|
|
402
|
+
if (review.comments.length > 0) {
|
|
403
|
+
console.log(`\n ${C.bold}Inline Comments:${C.reset}`);
|
|
404
|
+
review.comments.forEach(comment => {
|
|
405
|
+
const icon = comment.severity === 'error' ? '❌' :
|
|
406
|
+
comment.severity === 'warning' ? '⚠️' : '💡';
|
|
407
|
+
console.log(` ${icon} ${C.dim}Line ${comment.line}:${C.reset} ${comment.comment}`);
|
|
408
|
+
});
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
// Display summary
|
|
412
|
+
console.log(`\n ${C.bold}Review Summary:${C.reset}`);
|
|
413
|
+
const summaryLines = review.summary.split('\n').slice(0, 10);
|
|
414
|
+
summaryLines.forEach(line => {
|
|
415
|
+
if (line.trim()) console.log(` ${line}`);
|
|
416
|
+
});
|
|
417
|
+
|
|
418
|
+
console.log(`\n ${C.dim}Review saved. Use /reviews to see history.${C.reset}\n`);
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
// Extract code from Claude's output for review
|
|
422
|
+
function extractCodeFromOutput(output) {
|
|
423
|
+
const codeBlocks = [];
|
|
424
|
+
const codeBlockPattern = /```(\w+)?\n([\s\S]*?)```/g;
|
|
425
|
+
let match;
|
|
426
|
+
|
|
427
|
+
while ((match = codeBlockPattern.exec(output)) !== null) {
|
|
428
|
+
codeBlocks.push({
|
|
429
|
+
language: match[1] || 'unknown',
|
|
430
|
+
code: match[2].trim()
|
|
431
|
+
});
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
return codeBlocks;
|
|
435
|
+
}
|
|
436
|
+
|
|
245
437
|
// ═══════════════════════════════════════════════════════════════
|
|
246
438
|
// TERMINAL UI
|
|
247
439
|
// ═══════════════════════════════════════════════════════════════
|
|
@@ -281,8 +473,12 @@ class LoadingSpinner {
|
|
|
281
473
|
// Clear line first
|
|
282
474
|
process.stdout.write('\r' + ' '.repeat(80) + '\r');
|
|
283
475
|
|
|
476
|
+
// Only update spinner if output is visible (not in verbose mode)
|
|
284
477
|
this.interval = setInterval(() => {
|
|
285
|
-
if (!this.active)
|
|
478
|
+
if (!this.active || verbose) {
|
|
479
|
+
// Stop updating if verbose mode is on
|
|
480
|
+
return;
|
|
481
|
+
}
|
|
286
482
|
|
|
287
483
|
const elapsed = Math.round((Date.now() - this.startTime) / 1000);
|
|
288
484
|
const frame = this.frames[this.frameIndex];
|
|
@@ -329,8 +525,9 @@ function printHeader() {
|
|
|
329
525
|
console.log(` ${C.dim}Log:${C.reset} ${path.basename(logFile)}`);
|
|
330
526
|
console.log('');
|
|
331
527
|
console.log(` ${C.dim}Talk to Knoxis naturally. He dispatches to Claude when needed.${C.reset}`);
|
|
332
|
-
console.log(` ${C.dim}Commands: /status /verbose /diff /log /ai /feedback /exit${C.reset}`);
|
|
528
|
+
console.log(` ${C.dim}Commands: /status /verbose /diff /log /ai /feedback /review /exit${C.reset}`);
|
|
333
529
|
console.log(` ${C.dim}Quick feedback: 👍 or +1 (good), 👎 or -1 (needs improvement)${C.reset}`);
|
|
530
|
+
console.log(` ${C.dim}Code review: /review [file] or /autoreview to toggle${C.reset}`);
|
|
334
531
|
console.log('');
|
|
335
532
|
}
|
|
336
533
|
|
|
@@ -574,6 +771,8 @@ Confidence: ${(insights.intent.confidence * 100).toFixed(0)}%\n\n`;
|
|
|
574
771
|
}
|
|
575
772
|
|
|
576
773
|
recordDecision(userInput, decision, outcome) {
|
|
774
|
+
const MAX_DECISION_HISTORY = 20;
|
|
775
|
+
|
|
577
776
|
aiInsights.decisionHistory.push({
|
|
578
777
|
timestamp: new Date().toISOString(),
|
|
579
778
|
input: userInput.slice(0, 100),
|
|
@@ -581,8 +780,8 @@ Confidence: ${(insights.intent.confidence * 100).toFixed(0)}%\n\n`;
|
|
|
581
780
|
outcome: outcome
|
|
582
781
|
});
|
|
583
782
|
|
|
584
|
-
// Keep only last
|
|
585
|
-
if (aiInsights.decisionHistory.length >
|
|
783
|
+
// Keep only last N decisions for memory management
|
|
784
|
+
if (aiInsights.decisionHistory.length > MAX_DECISION_HISTORY) {
|
|
586
785
|
aiInsights.decisionHistory.shift();
|
|
587
786
|
}
|
|
588
787
|
}
|
|
@@ -660,13 +859,15 @@ Confidence: ${(insights.intent.confidence * 100).toFixed(0)}%\n\n`;
|
|
|
660
859
|
});
|
|
661
860
|
}
|
|
662
861
|
|
|
663
|
-
// Keep feedback history bounded
|
|
664
|
-
|
|
665
|
-
|
|
862
|
+
// Keep feedback history bounded for memory management
|
|
863
|
+
const MAX_FEEDBACK_HISTORY = 100;
|
|
864
|
+
if (aiInsights.feedbackHistory.length > MAX_FEEDBACK_HISTORY) {
|
|
865
|
+
// Remove oldest entries
|
|
866
|
+
aiInsights.feedbackHistory.splice(0, aiInsights.feedbackHistory.length - MAX_FEEDBACK_HISTORY);
|
|
666
867
|
}
|
|
667
868
|
|
|
668
869
|
// Save feedback data after processing
|
|
669
|
-
saveFeedbackData();
|
|
870
|
+
saveFeedbackData(); // Fire and forget for performance
|
|
670
871
|
|
|
671
872
|
return {
|
|
672
873
|
processed: true,
|
|
@@ -754,12 +955,25 @@ function detectProject() {
|
|
|
754
955
|
}
|
|
755
956
|
} catch (e) {}
|
|
756
957
|
|
|
757
|
-
// Git branch
|
|
958
|
+
// Git branch - use cached value or async check
|
|
758
959
|
try {
|
|
759
|
-
|
|
760
|
-
if (
|
|
761
|
-
const
|
|
762
|
-
|
|
960
|
+
// For initial detection, still use sync but add caching
|
|
961
|
+
if (!aiInsights.contextCache.has('gitBranch')) {
|
|
962
|
+
const result = spawnSync('git', ['branch', '--show-current'], {
|
|
963
|
+
cwd: WORKSPACE,
|
|
964
|
+
stdio: 'pipe',
|
|
965
|
+
shell: false // Prevent shell injection
|
|
966
|
+
});
|
|
967
|
+
if (result.status === 0) {
|
|
968
|
+
const branch = result.stdout.toString().trim();
|
|
969
|
+
if (branch) {
|
|
970
|
+
aiInsights.contextCache.set('gitBranch', branch);
|
|
971
|
+
info.push(`Git branch: ${branch}`);
|
|
972
|
+
}
|
|
973
|
+
}
|
|
974
|
+
} else {
|
|
975
|
+
const cachedBranch = aiInsights.contextCache.get('gitBranch');
|
|
976
|
+
info.push(`Git branch: ${cachedBranch}`);
|
|
763
977
|
}
|
|
764
978
|
} catch (e) {}
|
|
765
979
|
|
|
@@ -866,8 +1080,10 @@ function callGroq(messages, projectContext) {
|
|
|
866
1080
|
method: 'POST',
|
|
867
1081
|
headers: {
|
|
868
1082
|
'Content-Type': 'application/json',
|
|
869
|
-
'Content-Length': Buffer.byteLength(payload)
|
|
870
|
-
|
|
1083
|
+
'Content-Length': Buffer.byteLength(payload),
|
|
1084
|
+
'Connection': 'keep-alive'
|
|
1085
|
+
},
|
|
1086
|
+
agent: url.protocol === 'https:' ? httpsAgent : httpAgent // Use keep-alive agent
|
|
871
1087
|
};
|
|
872
1088
|
} else {
|
|
873
1089
|
// Direct Groq API call (legacy / developer override)
|
|
@@ -878,8 +1094,10 @@ function callGroq(messages, projectContext) {
|
|
|
878
1094
|
headers: {
|
|
879
1095
|
'Content-Type': 'application/json',
|
|
880
1096
|
'Authorization': `Bearer ${GROQ_API_KEY}`,
|
|
881
|
-
'Content-Length': Buffer.byteLength(payload)
|
|
882
|
-
|
|
1097
|
+
'Content-Length': Buffer.byteLength(payload),
|
|
1098
|
+
'Connection': 'keep-alive'
|
|
1099
|
+
},
|
|
1100
|
+
agent: httpsAgent // Use keep-alive agent
|
|
883
1101
|
};
|
|
884
1102
|
}
|
|
885
1103
|
|
|
@@ -1038,21 +1256,35 @@ function dispatchToClaude(prompt) {
|
|
|
1038
1256
|
// CONVERSATION MANAGEMENT
|
|
1039
1257
|
// ═══════════════════════════════════════════════════════════════
|
|
1040
1258
|
|
|
1041
|
-
// Keep conversation history bounded
|
|
1042
|
-
//
|
|
1259
|
+
// Keep conversation history bounded with efficient memory management
|
|
1260
|
+
// Always keep first 2 messages (greeting context) and last 18 messages
|
|
1043
1261
|
function trimHistory() {
|
|
1044
1262
|
const MAX_MESSAGES = 24;
|
|
1045
|
-
|
|
1263
|
+
const MAX_MESSAGE_LENGTH = 5000; // Truncate individual messages too
|
|
1264
|
+
|
|
1265
|
+
if (conversationHistory.length <= MAX_MESSAGES) {
|
|
1266
|
+
// Still truncate long messages even if under message limit
|
|
1267
|
+
conversationHistory.forEach(msg => {
|
|
1268
|
+
if (msg.content && msg.content.length > MAX_MESSAGE_LENGTH) {
|
|
1269
|
+
msg.content = msg.content.substring(0, MAX_MESSAGE_LENGTH) + '... [truncated]';
|
|
1270
|
+
}
|
|
1271
|
+
});
|
|
1272
|
+
return;
|
|
1273
|
+
}
|
|
1046
1274
|
|
|
1047
1275
|
const keep = 2; // greeting exchange
|
|
1048
|
-
const tail = MAX_MESSAGES - keep;
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
conversationHistory.
|
|
1276
|
+
const tail = MAX_MESSAGES - keep - 1; // -1 for the trim notice
|
|
1277
|
+
|
|
1278
|
+
// Use splice for efficient in-place modification
|
|
1279
|
+
const trimNotice = { role: 'user', content: '[Earlier conversation trimmed for context. See dispatch summaries in system prompt for work done.]' };
|
|
1280
|
+
conversationHistory.splice(keep, conversationHistory.length - keep - tail, trimNotice);
|
|
1281
|
+
|
|
1282
|
+
// Truncate long messages
|
|
1283
|
+
conversationHistory.forEach(msg => {
|
|
1284
|
+
if (msg.content && msg.content.length > MAX_MESSAGE_LENGTH) {
|
|
1285
|
+
msg.content = msg.content.substring(0, MAX_MESSAGE_LENGTH) + '... [truncated]';
|
|
1286
|
+
}
|
|
1287
|
+
});
|
|
1056
1288
|
}
|
|
1057
1289
|
|
|
1058
1290
|
// Truncate Claude output for Groq review (Groq doesn't need the full thing)
|
|
@@ -1069,13 +1301,13 @@ function truncateForReview(output, maxChars) {
|
|
|
1069
1301
|
// COMMAND HANDLERS
|
|
1070
1302
|
// ═══════════════════════════════════════════════════════════════
|
|
1071
1303
|
|
|
1072
|
-
function handleCommand(input) {
|
|
1304
|
+
async function handleCommand(input) {
|
|
1073
1305
|
const cmd = input.toLowerCase().trim();
|
|
1074
1306
|
|
|
1075
1307
|
if (cmd === '/exit' || cmd === '/quit' || cmd === '/q') {
|
|
1076
1308
|
// Save feedback data before exit
|
|
1077
1309
|
if (aiInsights.feedbackHistory.length > 0) {
|
|
1078
|
-
saveFeedbackData();
|
|
1310
|
+
saveFeedbackData(); // Fire and forget on exit
|
|
1079
1311
|
console.log(` ${C.green}✓ AI learning saved${C.reset}`);
|
|
1080
1312
|
}
|
|
1081
1313
|
log('SESSION END (user exit)');
|
|
@@ -1108,18 +1340,37 @@ function handleCommand(input) {
|
|
|
1108
1340
|
|
|
1109
1341
|
if (cmd === '/diff') {
|
|
1110
1342
|
try {
|
|
1111
|
-
|
|
1112
|
-
const
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1343
|
+
// Use spawn instead of spawnSync for better performance
|
|
1344
|
+
const { spawn } = require('child_process');
|
|
1345
|
+
const gitDiff = spawn('git', ['diff', '--stat'], {
|
|
1346
|
+
cwd: WORKSPACE,
|
|
1347
|
+
stdio: 'pipe',
|
|
1348
|
+
shell: false // Prevent shell injection
|
|
1349
|
+
});
|
|
1350
|
+
|
|
1351
|
+
let output = '';
|
|
1352
|
+
gitDiff.stdout.on('data', (data) => {
|
|
1353
|
+
output += data.toString();
|
|
1354
|
+
});
|
|
1355
|
+
|
|
1356
|
+
gitDiff.on('close', (code) => {
|
|
1357
|
+
if (code === 0 && output.trim()) {
|
|
1358
|
+
console.log(`\n ${C.dim}Git changes in workspace:${C.reset}`);
|
|
1359
|
+
output.trim().split('\n').forEach(line => console.log(` ${C.dim} ${line}${C.reset}`));
|
|
1360
|
+
} else {
|
|
1361
|
+
console.log(`\n ${C.dim}No uncommitted changes.${C.reset}`);
|
|
1362
|
+
}
|
|
1363
|
+
console.log('');
|
|
1364
|
+
});
|
|
1365
|
+
|
|
1366
|
+
gitDiff.on('error', () => {
|
|
1367
|
+
console.log(`\n ${C.dim}Not a git repository or git not available.${C.reset}`);
|
|
1368
|
+
console.log('');
|
|
1369
|
+
});
|
|
1119
1370
|
} catch (e) {
|
|
1120
|
-
console.log(`\n ${C.dim}
|
|
1371
|
+
console.log(`\n ${C.dim}Error running git diff.${C.reset}`);
|
|
1372
|
+
console.log('');
|
|
1121
1373
|
}
|
|
1122
|
-
console.log('');
|
|
1123
1374
|
return true;
|
|
1124
1375
|
}
|
|
1125
1376
|
|
|
@@ -1205,6 +1456,70 @@ function handleCommand(input) {
|
|
|
1205
1456
|
return true;
|
|
1206
1457
|
}
|
|
1207
1458
|
|
|
1459
|
+
// Code review commands
|
|
1460
|
+
if (cmd === '/review') {
|
|
1461
|
+
const args = input.split(' ').slice(1);
|
|
1462
|
+
|
|
1463
|
+
if (args.length === 0) {
|
|
1464
|
+
// Review last Claude output if available
|
|
1465
|
+
if (dispatchSummaries.length > 0) {
|
|
1466
|
+
const lastOutput = dispatchSummaries[dispatchSummaries.length - 1];
|
|
1467
|
+
const codeBlocks = extractCodeFromOutput(lastOutput);
|
|
1468
|
+
|
|
1469
|
+
if (codeBlocks.length > 0) {
|
|
1470
|
+
console.log(`\n ${C.cyan}Reviewing code from last Claude output...${C.reset}\n`);
|
|
1471
|
+
for (const block of codeBlocks) {
|
|
1472
|
+
const review = await reviewCode(block.code, `<inline-${block.language}>`, 1);
|
|
1473
|
+
displayCodeReview(review);
|
|
1474
|
+
}
|
|
1475
|
+
} else {
|
|
1476
|
+
console.log(` ${C.yellow}No code found in last output. Use: /review [file]${C.reset}\n`);
|
|
1477
|
+
}
|
|
1478
|
+
} else {
|
|
1479
|
+
console.log(` ${C.yellow}No recent output to review. Use: /review [file]${C.reset}\n`);
|
|
1480
|
+
}
|
|
1481
|
+
} else {
|
|
1482
|
+
// Review specific file
|
|
1483
|
+
const filePath = path.resolve(WORKSPACE, args[0]);
|
|
1484
|
+
try {
|
|
1485
|
+
const fileContent = await fsPromises.readFile(filePath, 'utf8');
|
|
1486
|
+
console.log(`\n ${C.cyan}Reviewing ${path.basename(filePath)}...${C.reset}\n`);
|
|
1487
|
+
const review = await reviewCode(fileContent, filePath, 1);
|
|
1488
|
+
displayCodeReview(review);
|
|
1489
|
+
} catch (e) {
|
|
1490
|
+
console.log(` ${C.red}Error reading file: ${e.message}${C.reset}\n`);
|
|
1491
|
+
}
|
|
1492
|
+
}
|
|
1493
|
+
return true;
|
|
1494
|
+
}
|
|
1495
|
+
|
|
1496
|
+
if (cmd === '/autoreview') {
|
|
1497
|
+
codeReview.autoReviewEnabled = !codeReview.autoReviewEnabled;
|
|
1498
|
+
const status = codeReview.autoReviewEnabled ? 'ENABLED' : 'DISABLED';
|
|
1499
|
+
console.log(`\n ${C.green}Auto-review ${status}${C.reset}`);
|
|
1500
|
+
console.log(` ${C.dim}Code will ${codeReview.autoReviewEnabled ? '' : 'not '}be automatically reviewed after Claude outputs.${C.reset}\n`);
|
|
1501
|
+
return true;
|
|
1502
|
+
}
|
|
1503
|
+
|
|
1504
|
+
if (cmd === '/reviews') {
|
|
1505
|
+
if (codeReview.reviewHistory.length === 0) {
|
|
1506
|
+
console.log(`\n ${C.dim}No code reviews yet.${C.reset}\n`);
|
|
1507
|
+
} else {
|
|
1508
|
+
console.log(`\n ${C.cyan}━━━ Code Review History ━━━${C.reset}`);
|
|
1509
|
+
codeReview.reviewHistory.slice(-5).forEach(review => {
|
|
1510
|
+
const time = new Date(review.timestamp).toLocaleTimeString();
|
|
1511
|
+
const commentCount = review.comments.length;
|
|
1512
|
+
const errorCount = review.comments.filter(c => c.severity === 'error').length;
|
|
1513
|
+
const warningCount = review.comments.filter(c => c.severity === 'warning').length;
|
|
1514
|
+
|
|
1515
|
+
console.log(` ${C.dim}${time}${C.reset} [${review.id}] ${review.filePath}`);
|
|
1516
|
+
console.log(` ${errorCount} errors, ${warningCount} warnings, ${commentCount} total comments`);
|
|
1517
|
+
});
|
|
1518
|
+
console.log('');
|
|
1519
|
+
}
|
|
1520
|
+
return true;
|
|
1521
|
+
}
|
|
1522
|
+
|
|
1208
1523
|
return false;
|
|
1209
1524
|
}
|
|
1210
1525
|
|
|
@@ -1213,6 +1528,20 @@ function handleCommand(input) {
|
|
|
1213
1528
|
// ═══════════════════════════════════════════════════════════════
|
|
1214
1529
|
|
|
1215
1530
|
let projectContext = '';
|
|
1531
|
+
let inputDebounceTimer = null;
|
|
1532
|
+
const INPUT_DEBOUNCE_MS = 100;
|
|
1533
|
+
|
|
1534
|
+
// Debounced input handler
|
|
1535
|
+
function debouncedHandleUserInput(input, rl) {
|
|
1536
|
+
if (inputDebounceTimer) {
|
|
1537
|
+
clearTimeout(inputDebounceTimer);
|
|
1538
|
+
}
|
|
1539
|
+
|
|
1540
|
+
inputDebounceTimer = setTimeout(() => {
|
|
1541
|
+
inputDebounceTimer = null;
|
|
1542
|
+
handleUserInput(input, rl);
|
|
1543
|
+
}, INPUT_DEBOUNCE_MS);
|
|
1544
|
+
}
|
|
1216
1545
|
|
|
1217
1546
|
async function handleUserInput(input, rl) {
|
|
1218
1547
|
const trimmed = input.trim();
|
|
@@ -1269,7 +1598,7 @@ async function handleUserInput(input, rl) {
|
|
|
1269
1598
|
|
|
1270
1599
|
// Handle slash commands
|
|
1271
1600
|
if (trimmed.startsWith('/')) {
|
|
1272
|
-
if (handleCommand(trimmed)) return;
|
|
1601
|
+
if (await handleCommand(trimmed)) return;
|
|
1273
1602
|
// Unknown command — pass through to Knoxis
|
|
1274
1603
|
}
|
|
1275
1604
|
|
|
@@ -1337,6 +1666,20 @@ async function handleUserInput(input, rl) {
|
|
|
1337
1666
|
|
|
1338
1667
|
printDivider(`Claude finished (${result.elapsed}s, ${result.lines} lines)`, C.green);
|
|
1339
1668
|
|
|
1669
|
+
// Auto-review code if enabled
|
|
1670
|
+
if (codeReview.autoReviewEnabled) {
|
|
1671
|
+
const codeBlocks = extractCodeFromOutput(result.output);
|
|
1672
|
+
if (codeBlocks.length > 0) {
|
|
1673
|
+
console.log(`\n ${C.cyan}Auto-reviewing code...${C.reset}`);
|
|
1674
|
+
for (const block of codeBlocks) {
|
|
1675
|
+
const review = await reviewCode(block.code, `<${block.language}>`, 1);
|
|
1676
|
+
if (review && review.comments.length > 0) {
|
|
1677
|
+
displayCodeReview(review);
|
|
1678
|
+
}
|
|
1679
|
+
}
|
|
1680
|
+
}
|
|
1681
|
+
}
|
|
1682
|
+
|
|
1340
1683
|
// Send output to Knoxis for review
|
|
1341
1684
|
const reviewMessages = [
|
|
1342
1685
|
...conversationHistory,
|
|
@@ -1372,9 +1715,10 @@ async function handleUserInput(input, rl) {
|
|
|
1372
1715
|
console.log('');
|
|
1373
1716
|
}
|
|
1374
1717
|
|
|
1375
|
-
// Track dispatch summary
|
|
1718
|
+
// Track dispatch summary and full output for code review
|
|
1376
1719
|
const shortSummary = review.message.split('\n')[0].slice(0, 150);
|
|
1377
|
-
dispatchSummaries.push(
|
|
1720
|
+
dispatchSummaries.push(result.output); // Store full output for review command
|
|
1721
|
+
codeReview.lastReviewedCode = result.output; // Cache for quick access
|
|
1378
1722
|
|
|
1379
1723
|
// Add review to history with AI suggestions
|
|
1380
1724
|
conversationHistory.push({
|
|
@@ -1449,7 +1793,7 @@ async function main() {
|
|
|
1449
1793
|
projectContext = detectProject();
|
|
1450
1794
|
|
|
1451
1795
|
// Load feedback data from previous sessions
|
|
1452
|
-
loadFeedbackData();
|
|
1796
|
+
await loadFeedbackData();
|
|
1453
1797
|
|
|
1454
1798
|
// Print header
|
|
1455
1799
|
printHeader();
|
|
@@ -1487,7 +1831,7 @@ async function main() {
|
|
|
1487
1831
|
} else {
|
|
1488
1832
|
// Save feedback data before exit
|
|
1489
1833
|
if (aiInsights.feedbackHistory.length > 0) {
|
|
1490
|
-
saveFeedbackData();
|
|
1834
|
+
saveFeedbackData(); // Fire and forget on exit
|
|
1491
1835
|
}
|
|
1492
1836
|
log('SESSION END (Ctrl+C)');
|
|
1493
1837
|
console.log(`\n\n ${C.dim}Session ended. ${claudeDispatches} dispatches to Claude.${C.reset}`);
|
|
@@ -1528,6 +1872,20 @@ async function main() {
|
|
|
1528
1872
|
const result = await dispatchToClaude(enrichedPrompt);
|
|
1529
1873
|
printDivider(`Claude finished (${result.elapsed}s, ${result.lines} lines)`, C.green);
|
|
1530
1874
|
|
|
1875
|
+
// Auto-review code if enabled
|
|
1876
|
+
if (codeReview.autoReviewEnabled) {
|
|
1877
|
+
const codeBlocks = extractCodeFromOutput(result.output);
|
|
1878
|
+
if (codeBlocks.length > 0) {
|
|
1879
|
+
console.log(`\n ${C.cyan}Auto-reviewing code...${C.reset}`);
|
|
1880
|
+
for (const block of codeBlocks) {
|
|
1881
|
+
const review = await reviewCode(block.code, `<${block.language}>`, 1);
|
|
1882
|
+
if (review && review.comments.length > 0) {
|
|
1883
|
+
displayCodeReview(review);
|
|
1884
|
+
}
|
|
1885
|
+
}
|
|
1886
|
+
}
|
|
1887
|
+
}
|
|
1888
|
+
|
|
1531
1889
|
const reviewMessages = [
|
|
1532
1890
|
...conversationHistory,
|
|
1533
1891
|
{
|
|
@@ -1540,7 +1898,8 @@ async function main() {
|
|
|
1540
1898
|
const review = await callGroq(reviewMessages, projectContext);
|
|
1541
1899
|
spinner.stop();
|
|
1542
1900
|
printKnoxis(review.message);
|
|
1543
|
-
dispatchSummaries.push(
|
|
1901
|
+
dispatchSummaries.push(result.output); // Store full output for review command
|
|
1902
|
+
codeReview.lastReviewedCode = result.output; // Cache for quick access
|
|
1544
1903
|
conversationHistory.push({
|
|
1545
1904
|
role: 'assistant',
|
|
1546
1905
|
content: JSON.stringify({ action: 'respond', message: review.message })
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "knoxis-collab",
|
|
3
|
-
"version": "1.
|
|
4
|
-
"description": "AI-enhanced collaborative programming with feedback learning — User + Knoxis + Claude Code with adaptive intelligence",
|
|
3
|
+
"version": "1.4.0",
|
|
4
|
+
"description": "AI-enhanced collaborative programming with real-time code review and feedback learning — User + Knoxis + Claude Code with adaptive intelligence",
|
|
5
5
|
"main": "knoxis-collab.js",
|
|
6
6
|
"bin": {
|
|
7
7
|
"knoxis-collab": "./knoxis-collab.js"
|