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.
- package/bin/knoxis-helper.js +6 -0
- package/lib/knoxis-interactive-pair.js +484 -0
- package/lib/knoxis-local-agent.js +123 -24
- 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',
|
|
@@ -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,
|
|
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,
|
|
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,
|
|
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
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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 (
|
|
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);
|
|
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.
|
|
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)'}`);
|