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.
Files changed (2) hide show
  1. package/knoxis-collab.js +412 -53
  2. 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 = loadConfig();
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
- try { fs.appendFileSync(logFile, line); } catch (e) {}
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 data = JSON.parse(fs.readFileSync(feedbackFile, 'utf8'));
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
- fs.writeFileSync(feedbackFile, JSON.stringify(data, null, 2));
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) return;
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 20 decisions
585
- if (aiInsights.decisionHistory.length > 20) {
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
- if (aiInsights.feedbackHistory.length > 100) {
665
- aiInsights.feedbackHistory.shift();
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
- const result = spawnSync('git', ['branch', '--show-current'], { cwd: WORKSPACE, stdio: 'pipe' });
760
- if (result.status === 0) {
761
- const branch = result.stdout.toString().trim();
762
- if (branch) info.push(`Git branch: ${branch}`);
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. Always keep first 2 messages (greeting context)
1042
- // and last 18 messages. Truncate long assistant messages.
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
- if (conversationHistory.length <= MAX_MESSAGES) return;
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
- const trimmed = [
1050
- ...conversationHistory.slice(0, keep),
1051
- { role: 'user', content: '[Earlier conversation trimmed for context. See dispatch summaries in system prompt for work done.]' },
1052
- ...conversationHistory.slice(-tail)
1053
- ];
1054
- conversationHistory.length = 0;
1055
- conversationHistory.push(...trimmed);
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
- const result = spawnSync('git', ['diff', '--stat'], { cwd: WORKSPACE, stdio: 'pipe' });
1112
- const output = result.stdout.toString().trim();
1113
- if (output) {
1114
- console.log(`\n ${C.dim}Git changes in workspace:${C.reset}`);
1115
- output.split('\n').forEach(line => console.log(` ${C.dim} ${line}${C.reset}`));
1116
- } else {
1117
- console.log(`\n ${C.dim}No uncommitted changes.${C.reset}`);
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}Not a git repository or git not available.${C.reset}`);
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 (short version for system prompt context)
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(shortSummary);
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(review.message.split('\n')[0].slice(0, 150));
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.3.0",
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"