knoxis-helper 1.3.4 → 1.4.1

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.
@@ -84,6 +84,7 @@ function ask(rl, question) {
84
84
  function installAgentLocally(force) {
85
85
  const sourceAgent = path.join(__dirname, '..', 'lib', 'knoxis-local-agent.js');
86
86
  const sourcePairProgram = path.join(__dirname, '..', 'lib', 'knoxis-pair-program.js');
87
+ const sourceInteractivePair = path.join(__dirname, '..', 'lib', 'knoxis-interactive-pair.js');
87
88
  const sourcePackage = path.join(__dirname, '..', 'package.json');
88
89
 
89
90
  if (!fs.existsSync(sourceAgent)) {
@@ -119,6 +120,11 @@ function installAgentLocally(force) {
119
120
  console.log(' Installed: knoxis-pair-program.js');
120
121
  }
121
122
 
123
+ if (fs.existsSync(sourceInteractivePair)) {
124
+ fs.copyFileSync(sourceInteractivePair, path.join(AGENT_DIR, 'knoxis-interactive-pair.js'));
125
+ console.log(' Installed: knoxis-interactive-pair.js');
126
+ }
127
+
122
128
  if (fs.existsSync(sourcePackage)) {
123
129
  fs.copyFileSync(sourcePackage, path.join(AGENT_DIR, 'package.json'));
124
130
  }
@@ -0,0 +1,484 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Knoxis Interactive Pair Programming
5
+ *
6
+ * Multi-turn pair programming with Groq acting as your pair programmer
7
+ * (reviewing plans, answering questions, giving feedback) between
8
+ * Claude Code coding phases.
9
+ *
10
+ * Flow:
11
+ * Phase 1: Claude reads codebase + creates implementation plan
12
+ * Knoxis (Groq): Reviews plan, answers questions, approves/adjusts
13
+ * Phase 2: Claude implements with feedback applied
14
+ * Knoxis (Groq): Reviews implementation, flags issues
15
+ * Phase 3: Claude addresses feedback + verifies build
16
+ *
17
+ * Usage:
18
+ * KNOXIS_TASK_FILE=/tmp/task.txt node knoxis-interactive-pair.js
19
+ * node knoxis-interactive-pair.js "add a health check endpoint"
20
+ *
21
+ * Env:
22
+ * GROQ_API_KEY - Required for Knoxis feedback (falls back to single-shot without it)
23
+ * KNOXIS_TASK_FILE - Path to file containing the task description
24
+ * KNOXIS_GROQ_MODEL - Groq model (default: llama-3.3-70b-versatile)
25
+ * KNOXIS_MAX_PHASE_MS - Max time per Claude phase in ms (default: 600000 = 10min)
26
+ *
27
+ * ZERO EXTERNAL DEPENDENCIES - uses only Node.js built-in modules
28
+ */
29
+
30
+ const { spawn, spawnSync } = require('child_process');
31
+ const crypto = require('crypto');
32
+ const https = require('https');
33
+ const fs = require('fs');
34
+ const path = require('path');
35
+ const os = require('os');
36
+
37
+ // === CONFIG ===
38
+ const CONFIG_PATH = path.join(os.homedir(), '.knoxis', 'config.json');
39
+ const SESSION_ID = crypto.randomUUID();
40
+ const MAX_PHASE_MS = parseInt(process.env.KNOXIS_MAX_PHASE_MS || '600000', 10);
41
+ const GROQ_MODEL = process.env.KNOXIS_GROQ_MODEL || 'llama-3.3-70b-versatile';
42
+
43
+ function loadConfig() {
44
+ try {
45
+ if (fs.existsSync(CONFIG_PATH)) {
46
+ return JSON.parse(fs.readFileSync(CONFIG_PATH, 'utf8'));
47
+ }
48
+ } catch (e) {}
49
+ return {};
50
+ }
51
+
52
+ const config = loadConfig();
53
+ const GROQ_API_KEY = process.env.GROQ_API_KEY || config.groqApiKey || '';
54
+
55
+ // === LOAD TASK ===
56
+ function loadTask() {
57
+ // 1. Task file (set by local agent)
58
+ const taskFile = process.env.KNOXIS_TASK_FILE;
59
+ if (taskFile && fs.existsSync(taskFile)) {
60
+ return fs.readFileSync(taskFile, 'utf8').trim();
61
+ }
62
+
63
+ // 2. CLI argument
64
+ if (process.argv.length > 2) {
65
+ return process.argv.slice(2).join(' ');
66
+ }
67
+
68
+ // 3. Extract from CLAUDE.md
69
+ const claudeMd = path.join(process.cwd(), 'CLAUDE.md');
70
+ if (fs.existsSync(claudeMd)) {
71
+ const content = fs.readFileSync(claudeMd, 'utf8');
72
+ const match = content.match(/## Current Task\n([\s\S]*?)(?=\n## )/);
73
+ if (match) return match[1].trim();
74
+ }
75
+
76
+ return null;
77
+ }
78
+
79
+ // === LOAD PROJECT CONTEXT (from CLAUDE.md) ===
80
+ function loadProjectContext() {
81
+ const claudeMd = path.join(process.cwd(), 'CLAUDE.md');
82
+ if (fs.existsSync(claudeMd)) {
83
+ return fs.readFileSync(claudeMd, 'utf8');
84
+ }
85
+ return '';
86
+ }
87
+
88
+ // === SESSION LOG ===
89
+ let sessionLog = '';
90
+ const logDir = path.join(process.cwd(), '.knoxis', 'sessions');
91
+
92
+ function initSessionLog() {
93
+ try {
94
+ if (!fs.existsSync(logDir)) {
95
+ fs.mkdirSync(logDir, { recursive: true });
96
+ }
97
+ } catch (e) {}
98
+ }
99
+
100
+ function appendLog(text) {
101
+ sessionLog += text + '\n';
102
+ }
103
+
104
+ function saveSessionLog() {
105
+ try {
106
+ const logFile = path.join(logDir, `${new Date().toISOString().replace(/[:.]/g, '-')}-${SESSION_ID.substring(0, 8)}.log`);
107
+ fs.writeFileSync(logFile, sessionLog, 'utf8');
108
+ return logFile;
109
+ } catch (e) {
110
+ return null;
111
+ }
112
+ }
113
+
114
+ // === GROQ API ===
115
+ function callGroq(systemPrompt, userMessage) {
116
+ return new Promise((resolve) => {
117
+ if (!GROQ_API_KEY) {
118
+ resolve('Looks good. Go ahead and implement it following the existing patterns in the codebase.');
119
+ return;
120
+ }
121
+
122
+ const payload = JSON.stringify({
123
+ model: GROQ_MODEL,
124
+ messages: [
125
+ { role: 'system', content: systemPrompt },
126
+ { role: 'user', content: userMessage }
127
+ ],
128
+ temperature: 0.3,
129
+ max_tokens: 2000
130
+ });
131
+
132
+ const options = {
133
+ hostname: 'api.groq.com',
134
+ path: '/openai/v1/chat/completions',
135
+ method: 'POST',
136
+ headers: {
137
+ 'Content-Type': 'application/json',
138
+ 'Authorization': `Bearer ${GROQ_API_KEY}`,
139
+ 'Content-Length': Buffer.byteLength(payload)
140
+ }
141
+ };
142
+
143
+ const req = https.request(options, (res) => {
144
+ let data = '';
145
+ res.on('data', chunk => data += chunk);
146
+ res.on('end', () => {
147
+ try {
148
+ const json = JSON.parse(data);
149
+ if (json.choices && json.choices[0]) {
150
+ resolve(json.choices[0].message.content);
151
+ } else if (json.error) {
152
+ console.error(' Groq error:', json.error.message || JSON.stringify(json.error));
153
+ resolve('Proceed with your best judgment following existing patterns.');
154
+ } else {
155
+ resolve('Proceed with your best judgment following existing patterns.');
156
+ }
157
+ } catch (e) {
158
+ resolve('Proceed with your best judgment following existing patterns.');
159
+ }
160
+ });
161
+ });
162
+
163
+ req.on('error', (err) => {
164
+ console.error(' Groq request failed:', err.message);
165
+ resolve('Proceed with your best judgment following existing patterns.');
166
+ });
167
+ req.setTimeout(30000, () => {
168
+ req.destroy();
169
+ resolve('Proceed with your best judgment following existing patterns.');
170
+ });
171
+
172
+ req.write(payload);
173
+ req.end();
174
+ });
175
+ }
176
+
177
+ // === RUN CLAUDE TURN ===
178
+ function runClaudeTurn(message, isResume) {
179
+ return new Promise((resolve) => {
180
+ // Check claude is available
181
+ const which = spawnSync('which', ['claude'], { stdio: 'pipe' });
182
+ if (which.status !== 0) {
183
+ resolve({ stdout: '', stderr: 'claude CLI not found', code: 127 });
184
+ return;
185
+ }
186
+
187
+ const args = ['-p', '--dangerously-skip-permissions'];
188
+ if (isResume) {
189
+ args.push('--resume', SESSION_ID);
190
+ } else {
191
+ args.push('--session-id', SESSION_ID);
192
+ }
193
+
194
+ const proc = spawn('claude', args, {
195
+ cwd: process.cwd(),
196
+ env: process.env,
197
+ stdio: ['pipe', 'pipe', 'pipe']
198
+ });
199
+
200
+ let stdout = '';
201
+ let stderr = '';
202
+
203
+ proc.stdout.on('data', chunk => {
204
+ const text = chunk.toString();
205
+ process.stdout.write(text);
206
+ stdout += text;
207
+ });
208
+
209
+ proc.stderr.on('data', chunk => {
210
+ const text = chunk.toString();
211
+ // Only show non-debug stderr
212
+ if (!text.includes('Debug:') && !text.includes('trace')) {
213
+ process.stderr.write(text);
214
+ }
215
+ stderr += text;
216
+ });
217
+
218
+ const timeout = setTimeout(() => {
219
+ console.log('\n Phase timed out after ' + (MAX_PHASE_MS / 60000).toFixed(0) + ' minutes');
220
+ try { proc.kill('SIGTERM'); } catch (e) {}
221
+ setTimeout(() => {
222
+ try { proc.kill('SIGKILL'); } catch (e) {}
223
+ }, 5000);
224
+ }, MAX_PHASE_MS);
225
+
226
+ proc.on('close', code => {
227
+ clearTimeout(timeout);
228
+ resolve({ stdout: stdout.trim(), stderr: stderr.trim(), code: code || 0 });
229
+ });
230
+
231
+ proc.on('error', err => {
232
+ clearTimeout(timeout);
233
+ resolve({ stdout: stdout.trim(), stderr: err.message, code: 1 });
234
+ });
235
+
236
+ proc.stdin.write(message);
237
+ proc.stdin.end();
238
+ });
239
+ }
240
+
241
+ // === FALLBACK: SINGLE-SHOT (existing behavior) ===
242
+ async function runSingleShot(task) {
243
+ console.log('');
244
+ console.log(' Running in single-shot mode (no Groq pair programmer)');
245
+ console.log('');
246
+
247
+ const result = await runClaudeTurn(task, false);
248
+ return result.code || 0;
249
+ }
250
+
251
+ // === MAIN ===
252
+ async function main() {
253
+ const task = loadTask();
254
+ if (!task) {
255
+ console.error('No task found. Set KNOXIS_TASK_FILE, pass as CLI argument, or include in CLAUDE.md.');
256
+ process.exit(1);
257
+ }
258
+
259
+ const projectContext = loadProjectContext();
260
+ const hasGroq = !!GROQ_API_KEY;
261
+
262
+ initSessionLog();
263
+
264
+ console.log('');
265
+ console.log('╔══════════════════════════════════════════════════════════════╗');
266
+ console.log('║ KNOXIS INTERACTIVE PAIR PROGRAMMING ║');
267
+ console.log('╚══════════════════════════════════════════════════════════════╝');
268
+ console.log('');
269
+ console.log(' Task: ' + task.substring(0, 100) + (task.length > 100 ? '...' : ''));
270
+ console.log(' Session: ' + SESSION_ID);
271
+ console.log(' Pair: ' + (hasGroq ? 'Groq (' + GROQ_MODEL + ')' : 'Disabled (no GROQ_API_KEY)'));
272
+ console.log(' Timeout: ' + (MAX_PHASE_MS / 60000).toFixed(0) + ' min per phase');
273
+ console.log('');
274
+
275
+ appendLog('# Knoxis Interactive Pair Programming Session');
276
+ appendLog('Session: ' + SESSION_ID);
277
+ appendLog('Task: ' + task);
278
+ appendLog('Date: ' + new Date().toISOString());
279
+ appendLog('');
280
+
281
+ // If no Groq, fall back to enhanced single-shot
282
+ if (!hasGroq) {
283
+ const code = await runSingleShot(task);
284
+ process.exit(code);
285
+ }
286
+
287
+ // Build Groq system prompt
288
+ // Trim project context to avoid exceeding Groq limits
289
+ const contextForGroq = projectContext.substring(0, 5000);
290
+ const groqSystem = [
291
+ 'You are Knoxis, an experienced senior developer pair programming with Claude Code (an AI coding assistant).',
292
+ 'The developer submitted this task remotely and is not available. You make decisions on their behalf.',
293
+ '',
294
+ 'ORIGINAL TASK:',
295
+ task,
296
+ '',
297
+ 'PROJECT CONTEXT (from CLAUDE.md):',
298
+ contextForGroq,
299
+ '',
300
+ 'YOUR ROLE:',
301
+ '- Answer questions decisively. Pick the most pragmatic option.',
302
+ '- Review plans critically but constructively.',
303
+ '- If the plan is solid, approve it quickly: "Good plan. Proceed with implementation."',
304
+ '- If there are issues, be specific about what to change.',
305
+ '- Prefer approaches that follow existing patterns in the codebase.',
306
+ '- Keep responses concise (3-8 sentences). Claude has full context.',
307
+ '- NEVER ask Claude questions. You provide answers and direction only.',
308
+ '- If Claude lists multiple options, pick the most practical one.',
309
+ '- Push for minimal, focused changes. No scope creep.',
310
+ ].join('\n');
311
+
312
+
313
+ // ═══════════════════════════════════════════
314
+ // PHASE 1: PLANNING
315
+ // ═══════════════════════════════════════════
316
+ console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
317
+ console.log(' PHASE 1: Understanding & Planning');
318
+ console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
319
+ console.log('');
320
+ appendLog('## Phase 1: Planning\n');
321
+
322
+ const planPrompt = [
323
+ task,
324
+ '',
325
+ 'Before implementing, I need you to:',
326
+ '1. Read the relevant existing code in this workspace',
327
+ '2. Understand the current patterns and conventions',
328
+ '3. Create a brief implementation plan (which files to change, what approach)',
329
+ '4. Note any key decisions or trade-offs you see',
330
+ '',
331
+ 'Share your plan and then STOP. Do not implement yet. I will review it first.',
332
+ ].join('\n');
333
+
334
+ const phase1 = await runClaudeTurn(planPrompt, false);
335
+ appendLog(phase1.stdout + '\n');
336
+
337
+ if (phase1.code !== 0 && !phase1.stdout) {
338
+ console.log('');
339
+ console.log(' Phase 1 failed (exit ' + phase1.code + '). Falling back to single-shot.');
340
+ appendLog('Phase 1 failed. Falling back to single-shot.\n');
341
+ const code = await runSingleShot(task);
342
+ const logFile = saveSessionLog();
343
+ if (logFile) console.log(' Log: ' + logFile);
344
+ process.exit(code);
345
+ }
346
+
347
+
348
+ // ═══════════════════════════════════════════
349
+ // KNOXIS REVIEWS PLAN
350
+ // ═══════════════════════════════════════════
351
+ console.log('');
352
+ console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
353
+ console.log(' KNOXIS: Reviewing plan...');
354
+ console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
355
+
356
+ const planReview = await callGroq(
357
+ groqSystem,
358
+ 'Claude produced the following plan:\n\n'
359
+ + phase1.stdout.substring(0, 8000)
360
+ + '\n\nReview this plan. Answer any questions Claude asked. Approve or suggest specific changes. Then tell Claude to proceed with implementation.'
361
+ );
362
+
363
+ console.log('');
364
+ console.log(' Knoxis: ' + planReview);
365
+ console.log('');
366
+ appendLog('## Knoxis Plan Review\n' + planReview + '\n');
367
+
368
+
369
+ // ═══════════════════════════════════════════
370
+ // PHASE 2: IMPLEMENTATION
371
+ // ═══════════════════════════════════════════
372
+ console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
373
+ console.log(' PHASE 2: Implementation');
374
+ console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
375
+ console.log('');
376
+ appendLog('## Phase 2: Implementation\n');
377
+
378
+ const phase2 = await runClaudeTurn(planReview, true);
379
+ appendLog(phase2.stdout.substring(0, 10000) + '\n');
380
+
381
+ // If resume failed (session not found), try context accumulation fallback
382
+ if (phase2.code !== 0 && phase2.stderr && phase2.stderr.includes('session')) {
383
+ console.log('');
384
+ console.log(' Session resume failed. Using context accumulation fallback.');
385
+ appendLog('Session resume failed. Using context accumulation.\n');
386
+
387
+ const fallbackPrompt = [
388
+ 'Previously you created this plan:',
389
+ phase1.stdout.substring(0, 4000),
390
+ '',
391
+ 'Feedback from your pair programmer:',
392
+ planReview,
393
+ '',
394
+ 'Now implement the solution. Follow existing patterns in the codebase.',
395
+ ].join('\n');
396
+
397
+ const phase2b = await runClaudeTurn(fallbackPrompt, false);
398
+ // Use new session for subsequent turns
399
+ appendLog(phase2b.stdout.substring(0, 10000) + '\n');
400
+
401
+ // Skip to verification with this result
402
+ const verifyPrompt = 'Verify the changes compile/build correctly. Run the most relevant test or build command. Give a brief summary of what was done.';
403
+ console.log('');
404
+ console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
405
+ console.log(' PHASE 3: Verification');
406
+ console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
407
+ console.log('');
408
+
409
+ const phase3b = await runClaudeTurn(verifyPrompt, true);
410
+ appendLog('## Phase 3: Verification\n' + phase3b.stdout.substring(0, 5000) + '\n');
411
+
412
+ console.log('');
413
+ console.log('╔══════════════════════════════════════════════════════════════╗');
414
+ console.log('║ PAIR PROGRAMMING SESSION COMPLETE ║');
415
+ console.log('╚══════════════════════════════════════════════════════════════╝');
416
+ const logFile = saveSessionLog();
417
+ if (logFile) console.log(' Log: ' + logFile);
418
+ console.log(' Resume: claude --resume ' + SESSION_ID);
419
+ console.log('');
420
+ process.exit(phase3b.code || 0);
421
+ }
422
+
423
+
424
+ // ═══════════════════════════════════════════
425
+ // KNOXIS REVIEWS IMPLEMENTATION
426
+ // ═══════════════════════════════════════════
427
+ console.log('');
428
+ console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
429
+ console.log(' KNOXIS: Reviewing implementation...');
430
+ console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
431
+
432
+ const implReview = await callGroq(
433
+ groqSystem,
434
+ 'Claude implemented the following:\n\n'
435
+ + phase2.stdout.substring(0, 8000)
436
+ + '\n\nReview the implementation. If there are issues, describe specifically what needs fixing. If it looks correct, tell Claude to verify the build/tests and summarize what was done.'
437
+ );
438
+
439
+ console.log('');
440
+ console.log(' Knoxis: ' + implReview);
441
+ console.log('');
442
+ appendLog('## Knoxis Implementation Review\n' + implReview + '\n');
443
+
444
+
445
+ // ═══════════════════════════════════════════
446
+ // PHASE 3: VERIFICATION & FIXES
447
+ // ═══════════════════════════════════════════
448
+ console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
449
+ console.log(' PHASE 3: Review & Verification');
450
+ console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
451
+ console.log('');
452
+ appendLog('## Phase 3: Verification\n');
453
+
454
+ const phase3 = await runClaudeTurn(
455
+ implReview
456
+ + '\n\nAfter addressing any feedback above, verify the changes compile/build correctly. Run the most relevant test or build command. Give a brief summary of everything that was done.',
457
+ true
458
+ );
459
+ appendLog(phase3.stdout.substring(0, 5000) + '\n');
460
+
461
+
462
+ // ═══════════════════════════════════════════
463
+ // DONE
464
+ // ═══════════════════════════════════════════
465
+ console.log('');
466
+ console.log('╔══════════════════════════════════════════════════════════════╗');
467
+ console.log('║ PAIR PROGRAMMING SESSION COMPLETE ║');
468
+ console.log('╚══════════════════════════════════════════════════════════════╝');
469
+ console.log('');
470
+ console.log(' Session: ' + SESSION_ID);
471
+ console.log(' Resume: claude --resume ' + SESSION_ID);
472
+ const logFile = saveSessionLog();
473
+ if (logFile) console.log(' Log: ' + logFile);
474
+ console.log('');
475
+
476
+ process.exit(phase3.code || 0);
477
+ }
478
+
479
+ main().catch(err => {
480
+ console.error('Fatal error:', err.message || err);
481
+ const logFile = saveSessionLog();
482
+ if (logFile) console.error('Log: ' + logFile);
483
+ process.exit(1);
484
+ });
@@ -446,7 +446,7 @@ async function handleRequest(req, res) {
446
446
  status: 'healthy',
447
447
  platform: os.platform(),
448
448
  agent: 'knoxis-local-agent',
449
- version: '2.3.0-stable',
449
+ version: '2.4.0-interactive',
450
450
  secure: serverMeta.secure,
451
451
  port: serverMeta.port,
452
452
  dependencies: 'none',
@@ -478,16 +478,40 @@ async function handleRequest(req, res) {
478
478
  return sendJSON(res, result.success ? 200 : 500, result, requestOrigin);
479
479
  }
480
480
 
481
+ // Write CLAUDE.md if provided
482
+ if (body.claudeMdContent && workspace && fs.existsSync(workspace)) {
483
+ try {
484
+ fs.writeFileSync(path.join(workspace, 'CLAUDE.md'), body.claudeMdContent, 'utf8');
485
+ console.log('📄 Wrote CLAUDE.md (' + body.claudeMdContent.length + ' chars)');
486
+ } catch (writeErr) {
487
+ console.warn('⚠️ Failed to write CLAUDE.md:', writeErr.message);
488
+ }
489
+ }
490
+
491
+ // If prompt is provided, write to temp file and pipe via stdin
492
+ // (same approach as WebSocket relay — avoids shell escaping issues)
493
+ let finalCommand = command;
494
+ let promptFile = null;
495
+ if (prompt && prompt.trim().length > 0) {
496
+ promptFile = path.join(os.tmpdir(), 'knoxis-task-' + (sessionId || Date.now()) + '.txt');
497
+ fs.writeFileSync(promptFile, prompt, 'utf8');
498
+ finalCommand = 'cat "' + promptFile + '" | claude --dangerously-skip-permissions';
499
+ console.log('📝 Task written to ' + promptFile + ' (' + prompt.length + ' chars)');
500
+ }
501
+
481
502
  if (platform === 'darwin') {
482
- await openMacTerminal(workspace, command);
503
+ await openMacTerminal(workspace, finalCommand);
504
+ if (promptFile) setTimeout(() => { try { fs.unlinkSync(promptFile); } catch (e) {} }, 30000);
483
505
  return sendJSON(res, 200, { success: true, message: 'Terminal opened on macOS', platform: 'darwin' }, requestOrigin);
484
506
  }
485
507
  if (platform === 'win32') {
486
- await openWindowsTerminal(workspace, command);
508
+ await openWindowsTerminal(workspace, finalCommand);
509
+ if (promptFile) setTimeout(() => { try { fs.unlinkSync(promptFile); } catch (e) {} }, 30000);
487
510
  return sendJSON(res, 200, { success: true, message: 'Terminal opened on Windows', platform: 'win32' }, requestOrigin);
488
511
  }
489
512
 
490
- await openLinuxTerminal(workspace, command);
513
+ await openLinuxTerminal(workspace, finalCommand);
514
+ if (promptFile) setTimeout(() => { try { fs.unlinkSync(promptFile); } catch (e) {} }, 30000);
491
515
  return sendJSON(res, 200, { success: true, message: 'Terminal opened on Linux', platform: 'linux' }, requestOrigin);
492
516
 
493
517
  } catch (error) {
@@ -655,28 +679,54 @@ async function handleRequest(req, res) {
655
679
 
656
680
  // ===== PAIR PROGRAMMING ENDPOINTS =====
657
681
 
682
+ // Resolve the interactive pair programming script
683
+ function resolveInteractiveScript() {
684
+ const candidates = [
685
+ path.join(__dirname, 'knoxis-interactive-pair.js'),
686
+ path.join(__dirname, '..', 'knoxis-interactive-pair.js'),
687
+ path.join(os.homedir(), '.knoxis', 'agent', 'knoxis-interactive-pair.js'),
688
+ path.join(__dirname, '..', '..', 'knoxis-interactive-pair.js'),
689
+ ];
690
+ for (const candidate of candidates) {
691
+ if (fs.existsSync(candidate)) return candidate;
692
+ }
693
+ return null;
694
+ }
695
+
658
696
  // Start pair programming session (opens terminal)
659
697
  if (pathname === '/pair/start' && method === 'POST') {
660
698
  // Build CLAUDE.md content from task when backend doesn't provide claudeMdContent
661
- function buildClaudeMdFromTask(taskText, workspacePath) {
699
+ function buildClaudeMdFromTask(taskText, workspacePath, interactive) {
662
700
  const lines = [];
663
701
  lines.push('# Project Instructions\n');
664
702
  lines.push('## Current Task');
665
703
  lines.push(taskText);
666
704
  lines.push('');
667
- lines.push('## Working Agreement');
668
- lines.push('- Work autonomously. Do not ask clarifying questions - make your best engineering judgment and proceed.');
669
- lines.push('- Read and understand existing code before making changes.');
670
- lines.push('- Follow existing patterns in the codebase.');
671
- lines.push('- Keep changes minimal and focused on the task.');
672
- lines.push('- Verify your work compiles/runs where possible.');
673
- lines.push('- Be direct and concise in any output.');
705
+ if (interactive) {
706
+ lines.push('## Working Agreement');
707
+ lines.push('- You are pair programming with Knoxis, a senior developer who reviews your work between phases.');
708
+ lines.push('- Phase 1: Read the codebase and share your implementation plan with key decisions.');
709
+ lines.push('- Phase 2: After receiving feedback, implement the solution following existing patterns.');
710
+ lines.push('- Phase 3: After review, address any feedback and verify the build.');
711
+ lines.push('- Share your reasoning on decisions - your pair programmer will provide feedback.');
712
+ lines.push('- Follow existing patterns in the codebase.');
713
+ lines.push('- Keep changes minimal and focused on the task.');
714
+ } else {
715
+ lines.push('## Working Agreement');
716
+ lines.push('- Work autonomously. Do not ask clarifying questions - make your best engineering judgment and proceed.');
717
+ lines.push('- Read and understand existing code before making changes.');
718
+ lines.push('- Follow existing patterns in the codebase.');
719
+ lines.push('- Keep changes minimal and focused on the task.');
720
+ lines.push('- Verify your work compiles/runs where possible.');
721
+ lines.push('- Be direct and concise in any output.');
722
+ }
674
723
  return lines.join('\n');
675
724
  }
676
725
 
677
726
  try {
678
727
  const body = await parseBody(req);
679
728
  const { workspace, task, file, provider, headless, sessionId, claudeMdContent } = body;
729
+ const interactive = body.interactive === true || body.interactive === 'true';
680
730
 
681
731
  if (!task) {
682
732
  return sendJSON(res, 400, { success: false, error: 'Task description required' }, requestOrigin);
@@ -695,7 +745,7 @@ async function handleRequest(req, res) {
695
745
  // Write CLAUDE.md for supplementary context.
696
746
  // If claudeMdContent was provided (from backend relay), use it directly.
697
747
  // Otherwise, build it from the task field.
698
- const effectiveClaudeMd = claudeMdContent || buildClaudeMdFromTask(task, workspace);
748
+ const effectiveClaudeMd = claudeMdContent || buildClaudeMdFromTask(task, workspace, interactive);
699
749
  if (effectiveClaudeMd && fs.existsSync(workspaceDir)) {
700
750
  try {
701
751
  fs.writeFileSync(path.join(workspaceDir, 'CLAUDE.md'), effectiveClaudeMd, 'utf8');
@@ -705,14 +755,42 @@ async function handleRequest(req, res) {
705
755
  }
706
756
  }
707
757
 
708
- // Write the actual task to a temp file and pipe it to Claude via stdin.
709
- // This avoids shell escaping issues with quotes/backticks/etc in the task text.
758
+ // Write the actual task to a temp file
710
759
  const promptFile = path.join(os.tmpdir(), `knoxis-task-${sessionId || Date.now()}.txt`);
711
760
  const promptText = file ? `Working on file: ${file}\n\nTask: ${task}` : task;
712
761
  fs.writeFileSync(promptFile, promptText, 'utf8');
713
- const command = `cat "${promptFile}" | claude --dangerously-skip-permissions`;
762
+
763
+ // Determine the command to run
764
+ let command;
765
+ let mode = 'single-shot';
766
+
767
+ if (interactive) {
768
+ const scriptPath = resolveInteractiveScript();
769
+ if (scriptPath) {
770
+ // Interactive mode: multi-turn with Groq pair programmer
771
+ command = `KNOXIS_TASK_FILE="${promptFile}" node "${scriptPath}"`;
772
+ mode = 'interactive';
773
+ console.log(`🤝 Interactive mode: ${scriptPath}`);
774
+ } else {
775
+ // Interactive requested but script not found - fall back to single-shot
776
+ console.warn('⚠️ Interactive script not found, falling back to single-shot');
777
+ command = `cat "${promptFile}" | claude --dangerously-skip-permissions`;
778
+ }
779
+ } else {
780
+ // Standard single-shot mode: pipe task to Claude
781
+ command = `cat "${promptFile}" | claude --dangerously-skip-permissions`;
782
+ }
714
783
 
715
784
  if (headless) {
785
+ if (interactive && mode === 'interactive') {
786
+ // Headless interactive: run the script directly as a process
787
+ const result = await runHeadlessProcess({
788
+ workspace: workspaceDir,
789
+ command,
790
+ sessionLabel: sessionId || 'interactive-pair'
791
+ });
792
+ return sendJSON(res, result.success ? 200 : 500, { ...result, mode }, requestOrigin);
793
+ }
716
794
  const result = await runHeadlessProcess({
717
795
  workspace: workspaceDir,
718
796
  command: provider && String(provider).toLowerCase() === 'codex' ? 'codex' : 'claude',
@@ -731,9 +809,15 @@ async function handleRequest(req, res) {
731
809
  await openLinuxTerminal(workspaceDir, command);
732
810
  }
733
811
 
812
+ // Clean up prompt file after a delay (terminal needs time to read it)
813
+ if (!interactive) {
814
+ setTimeout(() => { try { fs.unlinkSync(promptFile); } catch (e) {} }, 30000);
815
+ }
816
+
734
817
  return sendJSON(res, 200, {
735
818
  success: true,
736
- message: 'Pair programming session started',
819
+ message: interactive ? 'Interactive pair programming session started' : 'Pair programming session started',
820
+ mode,
737
821
  workspace: workspaceDir,
738
822
  task,
739
823
  file: file || null
@@ -1060,10 +1144,25 @@ function connectRelayWebSocket() {
1060
1144
  }
1061
1145
  }
1062
1146
 
1147
+ const interactive = msg.interactive === true;
1148
+
1063
1149
  if (taskPrompt) {
1064
1150
  promptFile = path.join(os.tmpdir(), `knoxis-task-${msg.requestId || Date.now()}.txt`);
1065
1151
  fs.writeFileSync(promptFile, taskPrompt, 'utf8');
1066
- command = `cat "${promptFile}" | claude --dangerously-skip-permissions`;
1152
+
1153
+ if (interactive) {
1154
+ // Interactive mode: use multi-turn pair programming script
1155
+ const scriptPath = resolveInteractiveScript();
1156
+ if (scriptPath) {
1157
+ command = `KNOXIS_TASK_FILE="${promptFile}" node "${scriptPath}"`;
1158
+ console.log(` 🤝 Interactive mode: ${scriptPath}`);
1159
+ } else {
1160
+ command = `cat "${promptFile}" | claude --dangerously-skip-permissions`;
1161
+ console.warn(` ⚠️ Interactive script not found, falling back to single-shot`);
1162
+ }
1163
+ } else {
1164
+ command = `cat "${promptFile}" | claude --dangerously-skip-permissions`;
1165
+ }
1067
1166
  console.log(` 📝 Task written to ${promptFile} (${taskPrompt.length} chars)`);
1068
1167
  } else {
1069
1168
  console.warn(` ⚠️ No task prompt found — Claude will run with: ${command.substring(0, 80)}`);
@@ -1075,7 +1174,7 @@ function connectRelayWebSocket() {
1075
1174
  result = await runHeadlessProcess({
1076
1175
  workspace: wsDir,
1077
1176
  command,
1078
- prompt: msg.prompt,
1177
+ prompt: interactive ? undefined : msg.prompt,
1079
1178
  sessionLabel: msg.requestId || 'relay'
1080
1179
  });
1081
1180
  } else {
@@ -1087,17 +1186,17 @@ function connectRelayWebSocket() {
1087
1186
  } else {
1088
1187
  await openLinuxTerminal(wsDir, command);
1089
1188
  }
1090
- result = { success: true, message: 'Terminal opened via relay' };
1189
+ result = { success: true, message: interactive ? 'Interactive pair programming started via relay' : 'Terminal opened via relay', mode: interactive ? 'interactive' : 'single-shot' };
1091
1190
  }
1092
1191
  } catch (err) {
1093
1192
  result = { success: false, error: err.message };
1094
1193
  }
1095
1194
 
1096
- // Clean up temp prompt file (Claude already read it)
1097
- if (promptFile) {
1195
+ // Clean up temp prompt file after delay (interactive script reads it at startup)
1196
+ if (promptFile && !interactive) {
1098
1197
  setTimeout(() => {
1099
1198
  try { fs.unlinkSync(promptFile); } catch (e) {}
1100
- }, 5000); // Delay to ensure cat has finished piping
1199
+ }, 5000);
1101
1200
  }
1102
1201
 
1103
1202
  // Send result back to backend
@@ -1240,7 +1339,7 @@ server.listen(serverMeta.port, () => {
1240
1339
  const scheme = serverMeta.secure ? 'https' : 'http';
1241
1340
  console.log('');
1242
1341
  console.log('╔══════════════════════════════════════════════════════════════╗');
1243
- console.log('║ 🚀 KNOXIS LOCAL AGENT v2.3.0 (Prompt Pipe Fix) ║');
1342
+ console.log('║ 🚀 KNOXIS LOCAL AGENT v2.4.0 (Interactive Pair) ║');
1244
1343
  console.log('╚══════════════════════════════════════════════════════════════╝');
1245
1344
  console.log('');
1246
1345
  console.log(`🔒 Mode: ${serverMeta.secure ? 'HTTPS (Secure)' : 'HTTP (Insecure - see warning below)'}`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "knoxis-helper",
3
- "version": "1.3.4",
3
+ "version": "1.4.1",
4
4
  "description": "Local helper for Knoxis pair programming - connects your machine to Knoxis on qig.ai",
5
5
  "bin": {
6
6
  "knoxis-helper": "./bin/knoxis-helper.js"