knoxis-helper 1.3.6 → 1.4.2
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/bin/knoxis-helper.js +6 -0
- package/lib/knoxis-interactive-pair.js +484 -0
- package/lib/knoxis-local-agent.js +96 -21
- package/package.json +1 -1
package/bin/knoxis-helper.js
CHANGED
|
@@ -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.
|
|
449
|
+
version: '2.4.0-interactive',
|
|
450
450
|
secure: serverMeta.secure,
|
|
451
451
|
port: serverMeta.port,
|
|
452
452
|
dependencies: 'none',
|
|
@@ -679,28 +679,54 @@ async function handleRequest(req, res) {
|
|
|
679
679
|
|
|
680
680
|
// ===== PAIR PROGRAMMING ENDPOINTS =====
|
|
681
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
|
+
|
|
682
696
|
// Start pair programming session (opens terminal)
|
|
683
697
|
if (pathname === '/pair/start' && method === 'POST') {
|
|
684
698
|
// Build CLAUDE.md content from task when backend doesn't provide claudeMdContent
|
|
685
|
-
function buildClaudeMdFromTask(taskText, workspacePath) {
|
|
699
|
+
function buildClaudeMdFromTask(taskText, workspacePath, interactive) {
|
|
686
700
|
const lines = [];
|
|
687
701
|
lines.push('# Project Instructions\n');
|
|
688
702
|
lines.push('## Current Task');
|
|
689
703
|
lines.push(taskText);
|
|
690
704
|
lines.push('');
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
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
|
+
}
|
|
698
723
|
return lines.join('\n');
|
|
699
724
|
}
|
|
700
725
|
|
|
701
726
|
try {
|
|
702
727
|
const body = await parseBody(req);
|
|
703
728
|
const { workspace, task, file, provider, headless, sessionId, claudeMdContent } = body;
|
|
729
|
+
const interactive = body.interactive === true || body.interactive === 'true';
|
|
704
730
|
|
|
705
731
|
if (!task) {
|
|
706
732
|
return sendJSON(res, 400, { success: false, error: 'Task description required' }, requestOrigin);
|
|
@@ -719,7 +745,7 @@ async function handleRequest(req, res) {
|
|
|
719
745
|
// Write CLAUDE.md for supplementary context.
|
|
720
746
|
// If claudeMdContent was provided (from backend relay), use it directly.
|
|
721
747
|
// Otherwise, build it from the task field.
|
|
722
|
-
const effectiveClaudeMd = claudeMdContent || buildClaudeMdFromTask(task, workspace);
|
|
748
|
+
const effectiveClaudeMd = claudeMdContent || buildClaudeMdFromTask(task, workspace, interactive);
|
|
723
749
|
if (effectiveClaudeMd && fs.existsSync(workspaceDir)) {
|
|
724
750
|
try {
|
|
725
751
|
fs.writeFileSync(path.join(workspaceDir, 'CLAUDE.md'), effectiveClaudeMd, 'utf8');
|
|
@@ -729,14 +755,42 @@ async function handleRequest(req, res) {
|
|
|
729
755
|
}
|
|
730
756
|
}
|
|
731
757
|
|
|
732
|
-
// Write the actual task to a temp file
|
|
733
|
-
// This avoids shell escaping issues with quotes/backticks/etc in the task text.
|
|
758
|
+
// Write the actual task to a temp file
|
|
734
759
|
const promptFile = path.join(os.tmpdir(), `knoxis-task-${sessionId || Date.now()}.txt`);
|
|
735
760
|
const promptText = file ? `Working on file: ${file}\n\nTask: ${task}` : task;
|
|
736
761
|
fs.writeFileSync(promptFile, promptText, 'utf8');
|
|
737
|
-
|
|
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
|
+
}
|
|
738
783
|
|
|
739
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
|
+
}
|
|
740
794
|
const result = await runHeadlessProcess({
|
|
741
795
|
workspace: workspaceDir,
|
|
742
796
|
command: provider && String(provider).toLowerCase() === 'codex' ? 'codex' : 'claude',
|
|
@@ -755,9 +809,15 @@ async function handleRequest(req, res) {
|
|
|
755
809
|
await openLinuxTerminal(workspaceDir, command);
|
|
756
810
|
}
|
|
757
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
|
+
|
|
758
817
|
return sendJSON(res, 200, {
|
|
759
818
|
success: true,
|
|
760
|
-
message: 'Pair programming session started',
|
|
819
|
+
message: interactive ? 'Interactive pair programming session started' : 'Pair programming session started',
|
|
820
|
+
mode,
|
|
761
821
|
workspace: workspaceDir,
|
|
762
822
|
task,
|
|
763
823
|
file: file || null
|
|
@@ -1084,10 +1144,25 @@ function connectRelayWebSocket() {
|
|
|
1084
1144
|
}
|
|
1085
1145
|
}
|
|
1086
1146
|
|
|
1147
|
+
const interactive = msg.interactive === true;
|
|
1148
|
+
|
|
1087
1149
|
if (taskPrompt) {
|
|
1088
1150
|
promptFile = path.join(os.tmpdir(), `knoxis-task-${msg.requestId || Date.now()}.txt`);
|
|
1089
1151
|
fs.writeFileSync(promptFile, taskPrompt, 'utf8');
|
|
1090
|
-
|
|
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
|
+
}
|
|
1091
1166
|
console.log(` 📝 Task written to ${promptFile} (${taskPrompt.length} chars)`);
|
|
1092
1167
|
} else {
|
|
1093
1168
|
console.warn(` ⚠️ No task prompt found — Claude will run with: ${command.substring(0, 80)}`);
|
|
@@ -1099,7 +1174,7 @@ function connectRelayWebSocket() {
|
|
|
1099
1174
|
result = await runHeadlessProcess({
|
|
1100
1175
|
workspace: wsDir,
|
|
1101
1176
|
command,
|
|
1102
|
-
prompt: msg.prompt,
|
|
1177
|
+
prompt: interactive ? undefined : msg.prompt,
|
|
1103
1178
|
sessionLabel: msg.requestId || 'relay'
|
|
1104
1179
|
});
|
|
1105
1180
|
} else {
|
|
@@ -1111,17 +1186,17 @@ function connectRelayWebSocket() {
|
|
|
1111
1186
|
} else {
|
|
1112
1187
|
await openLinuxTerminal(wsDir, command);
|
|
1113
1188
|
}
|
|
1114
|
-
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' };
|
|
1115
1190
|
}
|
|
1116
1191
|
} catch (err) {
|
|
1117
1192
|
result = { success: false, error: err.message };
|
|
1118
1193
|
}
|
|
1119
1194
|
|
|
1120
|
-
// Clean up temp prompt file (
|
|
1121
|
-
if (promptFile) {
|
|
1195
|
+
// Clean up temp prompt file after delay (interactive script reads it at startup)
|
|
1196
|
+
if (promptFile && !interactive) {
|
|
1122
1197
|
setTimeout(() => {
|
|
1123
1198
|
try { fs.unlinkSync(promptFile); } catch (e) {}
|
|
1124
|
-
}, 5000);
|
|
1199
|
+
}, 5000);
|
|
1125
1200
|
}
|
|
1126
1201
|
|
|
1127
1202
|
// Send result back to backend
|
|
@@ -1264,7 +1339,7 @@ server.listen(serverMeta.port, () => {
|
|
|
1264
1339
|
const scheme = serverMeta.secure ? 'https' : 'http';
|
|
1265
1340
|
console.log('');
|
|
1266
1341
|
console.log('╔══════════════════════════════════════════════════════════════╗');
|
|
1267
|
-
console.log('║ 🚀 KNOXIS LOCAL AGENT v2.
|
|
1342
|
+
console.log('║ 🚀 KNOXIS LOCAL AGENT v2.4.0 (Interactive Pair) ║');
|
|
1268
1343
|
console.log('╚══════════════════════════════════════════════════════════════╝');
|
|
1269
1344
|
console.log('');
|
|
1270
1345
|
console.log(`🔒 Mode: ${serverMeta.secure ? 'HTTPS (Secure)' : 'HTTP (Insecure - see warning below)'}`);
|