knoxis-helper 1.4.8 → 1.5.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 +39 -3
- package/lib/knoxis-interactive-pair.js +110 -22
- package/lib/knoxis-local-agent.js +176 -19
- package/lib/knoxis-pair-program.js +153 -111
- package/lib/portal-sync.js +149 -0
- package/lib/session-recorder.js +228 -0
- package/lib/state-scaffold.js +125 -0
- package/lib/templates/coding-ruleset.js +139 -0
- package/lib/templates/index.js +44 -0
- package/lib/templates/kickoff.js +175 -0
- package/lib/templates/recovery.js +130 -0
- package/lib/templates/resume.js +205 -0
- package/lib/templates/session-end.js +171 -0
- package/package.json +1 -1
package/bin/knoxis-helper.js
CHANGED
|
@@ -81,10 +81,26 @@ function ask(rl, question) {
|
|
|
81
81
|
* Copy agent files to ~/.knoxis/agent/ so they persist across npx cache clears,
|
|
82
82
|
* VPN changes, and reboots. Returns the stable path.
|
|
83
83
|
*/
|
|
84
|
+
function copyDirRecursive(src, dest) {
|
|
85
|
+
if (!fs.existsSync(src)) return;
|
|
86
|
+
if (!fs.existsSync(dest)) fs.mkdirSync(dest, { recursive: true });
|
|
87
|
+
for (const entry of fs.readdirSync(src, { withFileTypes: true })) {
|
|
88
|
+
const s = path.join(src, entry.name);
|
|
89
|
+
const d = path.join(dest, entry.name);
|
|
90
|
+
if (entry.isDirectory()) copyDirRecursive(s, d);
|
|
91
|
+
else if (entry.isFile()) fs.copyFileSync(s, d);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
84
95
|
function installAgentLocally(force) {
|
|
85
|
-
const
|
|
86
|
-
const
|
|
87
|
-
const
|
|
96
|
+
const libDir = path.join(__dirname, '..', 'lib');
|
|
97
|
+
const sourceAgent = path.join(libDir, 'knoxis-local-agent.js');
|
|
98
|
+
const sourcePairProgram = path.join(libDir, 'knoxis-pair-program.js');
|
|
99
|
+
const sourceInteractivePair = path.join(libDir, 'knoxis-interactive-pair.js');
|
|
100
|
+
const sourceStateScaffold = path.join(libDir, 'state-scaffold.js');
|
|
101
|
+
const sourcePortalSync = path.join(libDir, 'portal-sync.js');
|
|
102
|
+
const sourceSessionRecorder = path.join(libDir, 'session-recorder.js');
|
|
103
|
+
const sourceTemplatesDir = path.join(libDir, 'templates');
|
|
88
104
|
const sourcePackage = path.join(__dirname, '..', 'package.json');
|
|
89
105
|
|
|
90
106
|
if (!fs.existsSync(sourceAgent)) {
|
|
@@ -125,6 +141,26 @@ function installAgentLocally(force) {
|
|
|
125
141
|
console.log(' Installed: knoxis-interactive-pair.js');
|
|
126
142
|
}
|
|
127
143
|
|
|
144
|
+
if (fs.existsSync(sourceStateScaffold)) {
|
|
145
|
+
fs.copyFileSync(sourceStateScaffold, path.join(AGENT_DIR, 'state-scaffold.js'));
|
|
146
|
+
console.log(' Installed: state-scaffold.js');
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
if (fs.existsSync(sourcePortalSync)) {
|
|
150
|
+
fs.copyFileSync(sourcePortalSync, path.join(AGENT_DIR, 'portal-sync.js'));
|
|
151
|
+
console.log(' Installed: portal-sync.js');
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
if (fs.existsSync(sourceSessionRecorder)) {
|
|
155
|
+
fs.copyFileSync(sourceSessionRecorder, path.join(AGENT_DIR, 'session-recorder.js'));
|
|
156
|
+
console.log(' Installed: session-recorder.js');
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
if (fs.existsSync(sourceTemplatesDir)) {
|
|
160
|
+
copyDirRecursive(sourceTemplatesDir, path.join(AGENT_DIR, 'templates'));
|
|
161
|
+
console.log(' Installed: templates/');
|
|
162
|
+
}
|
|
163
|
+
|
|
128
164
|
if (fs.existsSync(sourcePackage)) {
|
|
129
165
|
fs.copyFileSync(sourcePackage, path.join(AGENT_DIR, 'package.json'));
|
|
130
166
|
}
|
|
@@ -33,6 +33,9 @@ const https = require('https');
|
|
|
33
33
|
const fs = require('fs');
|
|
34
34
|
const path = require('path');
|
|
35
35
|
const os = require('os');
|
|
36
|
+
const { SessionRecorder } = require('./session-recorder');
|
|
37
|
+
const { scaffoldStateLayout } = require('./state-scaffold');
|
|
38
|
+
const { syncSessionToPortal } = require('./portal-sync');
|
|
36
39
|
|
|
37
40
|
// === CONFIG ===
|
|
38
41
|
const CONFIG_PATH = path.join(os.homedir(), '.knoxis', 'config.json');
|
|
@@ -269,6 +272,42 @@ async function runSingleShot(task) {
|
|
|
269
272
|
return result.code || 0;
|
|
270
273
|
}
|
|
271
274
|
|
|
275
|
+
// === IDENTITY (passed by knoxis-local-agent via env vars) ===
|
|
276
|
+
function readIdentityFromEnv() {
|
|
277
|
+
const taskIds = (process.env.KNOXIS_TASK_IDS || '')
|
|
278
|
+
.split(',')
|
|
279
|
+
.map(s => s.trim())
|
|
280
|
+
.filter(Boolean);
|
|
281
|
+
return {
|
|
282
|
+
workspace: process.env.KNOXIS_WORKSPACE_PATH || process.cwd(),
|
|
283
|
+
userId: process.env.KNOXIS_USER_ID || (config && config.userId) || null,
|
|
284
|
+
workspaceId: process.env.KNOXIS_WORKSPACE_ID || null,
|
|
285
|
+
taskIds,
|
|
286
|
+
productSlug: process.env.KNOXIS_PRODUCT_SLUG || null,
|
|
287
|
+
projectSlug: process.env.KNOXIS_PROJECT_SLUG || null,
|
|
288
|
+
engineerId: process.env.KNOXIS_USER_ID || (config && config.userId) || null
|
|
289
|
+
};
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// === RECORDER + PORTAL FINALIZATION ===
|
|
293
|
+
// Saves the session JSON locally and POSTs to the portal stub. Called from
|
|
294
|
+
// every exit path so partial / fallback sessions still surface in the UI.
|
|
295
|
+
async function finalizeSession(recorder) {
|
|
296
|
+
if (!recorder) return;
|
|
297
|
+
try {
|
|
298
|
+
const filepath = await recorder.saveAsync();
|
|
299
|
+
console.log(' Session JSON: ' + filepath);
|
|
300
|
+
} catch (e) {
|
|
301
|
+
console.warn(' Could not save session JSON: ' + e.message);
|
|
302
|
+
}
|
|
303
|
+
try {
|
|
304
|
+
const record = recorder.buildFinalRecord();
|
|
305
|
+
await syncSessionToPortal(record);
|
|
306
|
+
} catch (e) {
|
|
307
|
+
console.warn(' Portal-sync errored: ' + e.message);
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
272
311
|
// === MAIN ===
|
|
273
312
|
async function main() {
|
|
274
313
|
const task = loadTask();
|
|
@@ -282,15 +321,40 @@ async function main() {
|
|
|
282
321
|
|
|
283
322
|
initSessionLog();
|
|
284
323
|
|
|
324
|
+
// Auto-scaffold + recorder. Idempotent — pre-existing files preserved.
|
|
325
|
+
const identity = readIdentityFromEnv();
|
|
326
|
+
let scaffoldResult = null;
|
|
327
|
+
try {
|
|
328
|
+
scaffoldResult = scaffoldStateLayout(identity.workspace);
|
|
329
|
+
} catch (e) {
|
|
330
|
+
console.warn(' Scaffold failed: ' + e.message);
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
const recorder = new SessionRecorder(task, identity.workspace, 'Claude Code + Knoxis (Groq)', {
|
|
334
|
+
mode: null, // interactive flow isn't a kit mode
|
|
335
|
+
archetype: null,
|
|
336
|
+
productSlug: identity.productSlug,
|
|
337
|
+
projectSlug: identity.projectSlug,
|
|
338
|
+
engineerId: identity.engineerId,
|
|
339
|
+
userId: identity.userId,
|
|
340
|
+
workspaceId: identity.workspaceId,
|
|
341
|
+
taskIds: identity.taskIds
|
|
342
|
+
});
|
|
343
|
+
|
|
285
344
|
console.log('');
|
|
286
345
|
console.log('╔══════════════════════════════════════════════════════════════╗');
|
|
287
346
|
console.log('║ KNOXIS INTERACTIVE PAIR PROGRAMMING ║');
|
|
288
347
|
console.log('╚══════════════════════════════════════════════════════════════╝');
|
|
289
348
|
console.log('');
|
|
290
|
-
console.log(' Task:
|
|
291
|
-
console.log(' Session:
|
|
292
|
-
console.log('
|
|
293
|
-
console.log('
|
|
349
|
+
console.log(' Task: ' + task.substring(0, 100) + (task.length > 100 ? '...' : ''));
|
|
350
|
+
console.log(' Session: ' + SESSION_ID);
|
|
351
|
+
console.log(' Recorded: ' + recorder.sessionId);
|
|
352
|
+
console.log(' Workspace: ' + identity.workspace);
|
|
353
|
+
console.log(' Pair: ' + (hasGroq ? 'Groq (' + GROQ_MODEL + ')' : 'Disabled (no GROQ_API_KEY)'));
|
|
354
|
+
console.log(' Timeout: ' + (MAX_PHASE_MS / 60000).toFixed(0) + ' min per phase');
|
|
355
|
+
if (scaffoldResult && (scaffoldResult.dirs.length || scaffoldResult.files.length)) {
|
|
356
|
+
console.log(' Scaffolded: ' + (scaffoldResult.dirs.length + scaffoldResult.files.length) + ' new entries (CODING_RULES + docs/state/)');
|
|
357
|
+
}
|
|
294
358
|
console.log('');
|
|
295
359
|
|
|
296
360
|
appendLog('# Knoxis Interactive Pair Programming Session');
|
|
@@ -301,7 +365,11 @@ async function main() {
|
|
|
301
365
|
|
|
302
366
|
// If no Groq, fall back to enhanced single-shot
|
|
303
367
|
if (!hasGroq) {
|
|
368
|
+
const idx = recorder.startStep('single-shot', 'Claude Code', task);
|
|
369
|
+
recorder.setStepPrompt(idx, task);
|
|
304
370
|
const code = await runSingleShot(task);
|
|
371
|
+
recorder.completeStep(idx, '(single-shot mode; output streamed live)', code !== 0 ? `exit ${code}` : null);
|
|
372
|
+
await finalizeSession(recorder);
|
|
305
373
|
process.exit(code);
|
|
306
374
|
}
|
|
307
375
|
|
|
@@ -352,8 +420,11 @@ async function main() {
|
|
|
352
420
|
'Share your plan and then STOP. Do not implement yet. I will review it first.',
|
|
353
421
|
].join('\n');
|
|
354
422
|
|
|
423
|
+
const phase1Idx = recorder.startStep('understand-plan', 'Claude Code', planPrompt);
|
|
424
|
+
recorder.setStepPrompt(phase1Idx, planPrompt);
|
|
355
425
|
const phase1 = await runClaudeTurn(planPrompt, false);
|
|
356
426
|
appendLog(phase1.stdout + '\n');
|
|
427
|
+
recorder.completeStep(phase1Idx, phase1.stdout, phase1.code !== 0 && !phase1.stdout ? `exit ${phase1.code}: ${phase1.stderr || '(empty)'}` : null);
|
|
357
428
|
|
|
358
429
|
if (phase1.code !== 0 && !phase1.stdout) {
|
|
359
430
|
console.log('');
|
|
@@ -362,9 +433,13 @@ async function main() {
|
|
|
362
433
|
console.log(' stderr: ' + phase1.stderr.split('\n').slice(0, 5).join('\n '));
|
|
363
434
|
}
|
|
364
435
|
appendLog('Phase 1 failed (exit ' + phase1.code + '). stderr: ' + (phase1.stderr || '(empty)') + '\n');
|
|
436
|
+
const fallbackIdx = recorder.startStep('single-shot-fallback', 'Claude Code', task);
|
|
437
|
+
recorder.setStepPrompt(fallbackIdx, task);
|
|
365
438
|
const code = await runSingleShot(task);
|
|
439
|
+
recorder.completeStep(fallbackIdx, '(single-shot fallback; output streamed live)', code !== 0 ? `exit ${code}` : null);
|
|
366
440
|
const logFile = saveSessionLog();
|
|
367
441
|
if (logFile) console.log(' Log: ' + logFile);
|
|
442
|
+
await finalizeSession(recorder);
|
|
368
443
|
process.exit(code);
|
|
369
444
|
}
|
|
370
445
|
|
|
@@ -377,12 +452,13 @@ async function main() {
|
|
|
377
452
|
console.log(' KNOXIS: Reviewing plan...');
|
|
378
453
|
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
|
379
454
|
|
|
380
|
-
const
|
|
381
|
-
|
|
382
|
-
'Claude
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
);
|
|
455
|
+
const planReviewPrompt = 'Claude produced the following plan:\n\n'
|
|
456
|
+
+ phase1.stdout.substring(0, 8000)
|
|
457
|
+
+ '\n\nReview this plan. Answer any questions Claude asked. Approve or suggest specific changes. Then tell Claude to proceed with implementation.';
|
|
458
|
+
const planReviewIdx = recorder.startStep('knoxis-plan-review', 'Knoxis (Groq)', planReviewPrompt);
|
|
459
|
+
recorder.setStepPrompt(planReviewIdx, planReviewPrompt);
|
|
460
|
+
const planReview = await callGroq(groqSystem, planReviewPrompt);
|
|
461
|
+
recorder.completeStep(planReviewIdx, planReview, null);
|
|
386
462
|
|
|
387
463
|
console.log('');
|
|
388
464
|
console.log(' Knoxis: ' + planReview);
|
|
@@ -399,8 +475,11 @@ async function main() {
|
|
|
399
475
|
console.log('');
|
|
400
476
|
appendLog('## Phase 2: Implementation\n');
|
|
401
477
|
|
|
478
|
+
const phase2Idx = recorder.startStep('implement', 'Claude Code', planReview);
|
|
479
|
+
recorder.setStepPrompt(phase2Idx, planReview);
|
|
402
480
|
const phase2 = await runClaudeTurn(planReview, true);
|
|
403
481
|
appendLog(phase2.stdout.substring(0, 10000) + '\n');
|
|
482
|
+
recorder.completeStep(phase2Idx, phase2.stdout, phase2.code !== 0 ? `exit ${phase2.code}: ${(phase2.stderr || '').slice(0, 200)}` : null);
|
|
404
483
|
|
|
405
484
|
// If resume failed (session not found), try context accumulation fallback
|
|
406
485
|
if (phase2.code !== 0 && phase2.stderr && phase2.stderr.includes('session')) {
|
|
@@ -418,9 +497,11 @@ async function main() {
|
|
|
418
497
|
'Now implement the solution. Follow existing patterns in the codebase.',
|
|
419
498
|
].join('\n');
|
|
420
499
|
|
|
500
|
+
const phase2bIdx = recorder.startStep('implement-fallback', 'Claude Code', fallbackPrompt);
|
|
501
|
+
recorder.setStepPrompt(phase2bIdx, fallbackPrompt);
|
|
421
502
|
const phase2b = await runClaudeTurn(fallbackPrompt, false);
|
|
422
|
-
// Use new session for subsequent turns
|
|
423
503
|
appendLog(phase2b.stdout.substring(0, 10000) + '\n');
|
|
504
|
+
recorder.completeStep(phase2bIdx, phase2b.stdout, phase2b.code !== 0 ? `exit ${phase2b.code}` : null);
|
|
424
505
|
|
|
425
506
|
// Skip to verification with this result
|
|
426
507
|
const verifyPrompt = 'Verify the changes compile/build correctly. Run the most relevant test or build command. Give a brief summary of what was done.';
|
|
@@ -430,8 +511,11 @@ async function main() {
|
|
|
430
511
|
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
|
431
512
|
console.log('');
|
|
432
513
|
|
|
514
|
+
const phase3bIdx = recorder.startStep('verify', 'Claude Code', verifyPrompt);
|
|
515
|
+
recorder.setStepPrompt(phase3bIdx, verifyPrompt);
|
|
433
516
|
const phase3b = await runClaudeTurn(verifyPrompt, true);
|
|
434
517
|
appendLog('## Phase 3: Verification\n' + phase3b.stdout.substring(0, 5000) + '\n');
|
|
518
|
+
recorder.completeStep(phase3bIdx, phase3b.stdout, phase3b.code !== 0 ? `exit ${phase3b.code}` : null);
|
|
435
519
|
|
|
436
520
|
console.log('');
|
|
437
521
|
console.log('╔══════════════════════════════════════════════════════════════╗');
|
|
@@ -441,6 +525,7 @@ async function main() {
|
|
|
441
525
|
if (logFile) console.log(' Log: ' + logFile);
|
|
442
526
|
console.log(' Resume: claude --resume ' + SESSION_ID);
|
|
443
527
|
console.log('');
|
|
528
|
+
await finalizeSession(recorder);
|
|
444
529
|
process.exit(phase3b.code || 0);
|
|
445
530
|
}
|
|
446
531
|
|
|
@@ -453,12 +538,13 @@ async function main() {
|
|
|
453
538
|
console.log(' KNOXIS: Reviewing implementation...');
|
|
454
539
|
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
|
455
540
|
|
|
456
|
-
const
|
|
457
|
-
|
|
458
|
-
'Claude
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
);
|
|
541
|
+
const implReviewPrompt = 'Claude implemented the following:\n\n'
|
|
542
|
+
+ phase2.stdout.substring(0, 8000)
|
|
543
|
+
+ '\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.';
|
|
544
|
+
const implReviewIdx = recorder.startStep('knoxis-impl-review', 'Knoxis (Groq)', implReviewPrompt);
|
|
545
|
+
recorder.setStepPrompt(implReviewIdx, implReviewPrompt);
|
|
546
|
+
const implReview = await callGroq(groqSystem, implReviewPrompt);
|
|
547
|
+
recorder.completeStep(implReviewIdx, implReview, null);
|
|
462
548
|
|
|
463
549
|
console.log('');
|
|
464
550
|
console.log(' Knoxis: ' + implReview);
|
|
@@ -475,12 +561,13 @@ async function main() {
|
|
|
475
561
|
console.log('');
|
|
476
562
|
appendLog('## Phase 3: Verification\n');
|
|
477
563
|
|
|
478
|
-
const
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
);
|
|
564
|
+
const phase3Prompt = implReview
|
|
565
|
+
+ '\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.';
|
|
566
|
+
const phase3Idx = recorder.startStep('verify', 'Claude Code', phase3Prompt);
|
|
567
|
+
recorder.setStepPrompt(phase3Idx, phase3Prompt);
|
|
568
|
+
const phase3 = await runClaudeTurn(phase3Prompt, true);
|
|
483
569
|
appendLog(phase3.stdout.substring(0, 5000) + '\n');
|
|
570
|
+
recorder.completeStep(phase3Idx, phase3.stdout, phase3.code !== 0 ? `exit ${phase3.code}` : null);
|
|
484
571
|
|
|
485
572
|
|
|
486
573
|
// ═══════════════════════════════════════════
|
|
@@ -497,6 +584,7 @@ async function main() {
|
|
|
497
584
|
if (logFile) console.log(' Log: ' + logFile);
|
|
498
585
|
console.log('');
|
|
499
586
|
|
|
587
|
+
await finalizeSession(recorder);
|
|
500
588
|
process.exit(phase3.code || 0);
|
|
501
589
|
}
|
|
502
590
|
|
|
@@ -580,6 +580,47 @@ function resolveInteractiveScript() {
|
|
|
580
580
|
return null;
|
|
581
581
|
}
|
|
582
582
|
|
|
583
|
+
// Resolve the kit-aware pair-program runner (used when caller specifies --mode)
|
|
584
|
+
function resolvePairProgramScript() {
|
|
585
|
+
const candidates = [
|
|
586
|
+
path.join(__dirname, 'knoxis-pair-program.js'),
|
|
587
|
+
path.join(__dirname, '..', 'knoxis-pair-program.js'),
|
|
588
|
+
path.join(os.homedir(), '.knoxis', 'agent', 'knoxis-pair-program.js'),
|
|
589
|
+
path.join(__dirname, '..', '..', 'knoxis-pair-program.js'),
|
|
590
|
+
];
|
|
591
|
+
for (const candidate of candidates) {
|
|
592
|
+
if (fs.existsSync(candidate)) return candidate;
|
|
593
|
+
}
|
|
594
|
+
return null;
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
const KIT_MODES = new Set(['kickoff', 'resume', 'session-end', 'recovery']);
|
|
598
|
+
|
|
599
|
+
function buildPairProgramCommand({ scriptPath, workspace, mode, archetype, pattern, productSlug, projectSlug, taskPrompt, userId, workspaceId, taskIds }) {
|
|
600
|
+
const q = v => `"${escapeForDoubleQuotedShellArg(v)}"`;
|
|
601
|
+
// Mode/archetype/pattern are constrained to known short tokens — no need to quote.
|
|
602
|
+
// Slugs and paths can contain spaces or special chars — always quote.
|
|
603
|
+
const parts = [`node ${q(scriptPath)}`, `--workspace ${q(workspace)}`];
|
|
604
|
+
if (mode) parts.push(`--mode ${mode}`);
|
|
605
|
+
if (archetype) parts.push(`--archetype ${archetype}`);
|
|
606
|
+
if (pattern) parts.push(`--pattern ${pattern}`);
|
|
607
|
+
if (productSlug) parts.push(`--product-slug ${q(productSlug)}`);
|
|
608
|
+
if (projectSlug) parts.push(`--project-slug ${q(projectSlug)}`);
|
|
609
|
+
if (userId) parts.push(`--user-id ${q(userId)}`);
|
|
610
|
+
if (workspaceId) parts.push(`--workspace-id ${q(workspaceId)}`);
|
|
611
|
+
if (Array.isArray(taskIds)) {
|
|
612
|
+
for (const t of taskIds) {
|
|
613
|
+
if (t) parts.push(`--task-id ${q(t)}`);
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
if (taskPrompt && taskPrompt.length) {
|
|
617
|
+
// Use base64 to avoid shell-quoting hazards (newlines, quotes, backticks).
|
|
618
|
+
const b64 = Buffer.from(taskPrompt, 'utf8').toString('base64');
|
|
619
|
+
parts.push(`--prompt-base64 ${b64}`);
|
|
620
|
+
}
|
|
621
|
+
return parts.join(' ');
|
|
622
|
+
}
|
|
623
|
+
|
|
583
624
|
// Request handler
|
|
584
625
|
async function handleRequest(req, res) {
|
|
585
626
|
const parsedUrl = url.parse(req.url, true);
|
|
@@ -867,8 +908,22 @@ async function handleRequest(req, res) {
|
|
|
867
908
|
const body = await parseBody(req);
|
|
868
909
|
const { workspace, task, file, provider, headless, sessionId, claudeMdContent } = body;
|
|
869
910
|
const interactive = body.interactive === true || body.interactive === 'true';
|
|
870
|
-
|
|
871
|
-
|
|
911
|
+
const kitMode = typeof body.mode === 'string' && KIT_MODES.has(body.mode) ? body.mode : null;
|
|
912
|
+
const archetype = typeof body.archetype === 'string' ? body.archetype : null;
|
|
913
|
+
const pattern = typeof body.pattern === 'string' ? body.pattern : null;
|
|
914
|
+
const productSlug = typeof body.productSlug === 'string' ? body.productSlug : null;
|
|
915
|
+
const projectSlug = typeof body.projectSlug === 'string' ? body.projectSlug : null;
|
|
916
|
+
// QIG portal linkage — let session records get tied back to the user / workspace / tasks.
|
|
917
|
+
const portalUserId = typeof body.userId === 'string' ? body.userId : null;
|
|
918
|
+
const portalWorkspaceId = typeof body.workspaceId === 'string' ? body.workspaceId : null;
|
|
919
|
+
const portalTaskIds = Array.isArray(body.taskIds)
|
|
920
|
+
? body.taskIds.filter(t => typeof t === 'string' && t)
|
|
921
|
+
: (typeof body.taskId === 'string' ? [body.taskId] : []);
|
|
922
|
+
|
|
923
|
+
// Kickoff and session-end modes don't strictly need a task — the kit prompt
|
|
924
|
+
// drives the interaction. For all other paths a task is still required.
|
|
925
|
+
const taskOptionalForMode = kitMode === 'kickoff' || kitMode === 'session-end';
|
|
926
|
+
if (!task && !taskOptionalForMode) {
|
|
872
927
|
return sendJSON(res, 400, { success: false, error: 'Task description required' }, requestOrigin);
|
|
873
928
|
}
|
|
874
929
|
|
|
@@ -895,20 +950,56 @@ async function handleRequest(req, res) {
|
|
|
895
950
|
}
|
|
896
951
|
}
|
|
897
952
|
|
|
898
|
-
// Write the actual task to a temp file
|
|
953
|
+
// Write the actual task to a temp file (used by single-shot and interactive paths)
|
|
899
954
|
const promptFile = path.join(os.tmpdir(), `knoxis-task-${sessionId || Date.now()}.txt`);
|
|
900
|
-
const promptText = file ? `Working on file: ${file}\n\nTask: ${task}` : task;
|
|
901
|
-
fs.writeFileSync(promptFile, promptText, 'utf8');
|
|
955
|
+
const promptText = task ? (file ? `Working on file: ${file}\n\nTask: ${task}` : task) : '';
|
|
956
|
+
if (promptText) fs.writeFileSync(promptFile, promptText, 'utf8');
|
|
902
957
|
|
|
903
|
-
// Determine the command to run
|
|
958
|
+
// Determine the command to run.
|
|
904
959
|
let command;
|
|
905
960
|
let mode = 'single-shot';
|
|
906
961
|
|
|
907
|
-
|
|
962
|
+
// Route through knoxis-pair-program.js whenever it's installed — gives
|
|
963
|
+
// every task auto-scaffold, the standards systemIntro, auto-context, and
|
|
964
|
+
// portal-sync. Kit mode adds --mode; default mode runs the standard
|
|
965
|
+
// 4-step pipeline. Interactive (Groq) mode and the legacy claude-pipe
|
|
966
|
+
// fallback remain available when the runner isn't present.
|
|
967
|
+
const ppScript = resolvePairProgramScript();
|
|
968
|
+
if (kitMode && !ppScript) {
|
|
969
|
+
return sendJSON(res, 500, { success: false, error: 'knoxis-pair-program.js not found — reinstall knoxis-helper.' }, requestOrigin);
|
|
970
|
+
}
|
|
971
|
+
|
|
972
|
+
if (kitMode || (ppScript && !interactive && promptText)) {
|
|
973
|
+
command = buildPairProgramCommand({
|
|
974
|
+
scriptPath: ppScript,
|
|
975
|
+
workspace: workspaceDir,
|
|
976
|
+
mode: kitMode || null,
|
|
977
|
+
archetype,
|
|
978
|
+
pattern,
|
|
979
|
+
productSlug,
|
|
980
|
+
projectSlug,
|
|
981
|
+
taskPrompt: promptText,
|
|
982
|
+
userId: portalUserId,
|
|
983
|
+
workspaceId: portalWorkspaceId,
|
|
984
|
+
taskIds: portalTaskIds
|
|
985
|
+
});
|
|
986
|
+
mode = kitMode
|
|
987
|
+
? `kit:${kitMode}${archetype ? `/${archetype}` : ''}${pattern ? `/${pattern}` : ''}`
|
|
988
|
+
: 'pair-program:default';
|
|
989
|
+
console.log(`🧰 ${kitMode ? 'Kit' : 'Pair-program (default pipeline)'}: ${mode}`);
|
|
990
|
+
} else if (interactive) {
|
|
908
991
|
const scriptPath = resolveInteractiveScript();
|
|
909
992
|
if (scriptPath) {
|
|
910
|
-
// Interactive mode: multi-turn with Groq pair programmer
|
|
911
|
-
|
|
993
|
+
// Interactive mode: multi-turn with Groq pair programmer.
|
|
994
|
+
// Pass identity env vars so the script can build a schema-aligned
|
|
995
|
+
// session record and POST it via portal-sync.
|
|
996
|
+
const interactiveEnv = { KNOXIS_TASK_FILE: promptFile, KNOXIS_WORKSPACE_PATH: workspaceDir };
|
|
997
|
+
if (portalUserId) interactiveEnv.KNOXIS_USER_ID = portalUserId;
|
|
998
|
+
if (portalWorkspaceId) interactiveEnv.KNOXIS_WORKSPACE_ID = portalWorkspaceId;
|
|
999
|
+
if (portalTaskIds && portalTaskIds.length) interactiveEnv.KNOXIS_TASK_IDS = portalTaskIds.join(',');
|
|
1000
|
+
if (productSlug) interactiveEnv.KNOXIS_PRODUCT_SLUG = productSlug;
|
|
1001
|
+
if (projectSlug) interactiveEnv.KNOXIS_PROJECT_SLUG = projectSlug;
|
|
1002
|
+
command = buildEnvCommand(interactiveEnv, `node "${scriptPath}"`);
|
|
912
1003
|
mode = 'interactive';
|
|
913
1004
|
console.log(`🤝 Interactive mode: ${scriptPath}`);
|
|
914
1005
|
} else {
|
|
@@ -917,17 +1008,19 @@ async function handleRequest(req, res) {
|
|
|
917
1008
|
command = buildCatCommand(promptFile) + ' | claude --dangerously-skip-permissions';
|
|
918
1009
|
}
|
|
919
1010
|
} else {
|
|
920
|
-
//
|
|
1011
|
+
// Legacy fallback when neither pair-program nor interactive scripts are installed.
|
|
921
1012
|
command = buildCatCommand(promptFile) + ' | claude --dangerously-skip-permissions';
|
|
922
1013
|
}
|
|
923
1014
|
|
|
924
1015
|
if (headless) {
|
|
925
|
-
|
|
926
|
-
|
|
1016
|
+
// Anything routed through pair-program.js or knoxis-interactive-pair.js
|
|
1017
|
+
// is a script invocation — run the assembled command verbatim.
|
|
1018
|
+
const isScriptInvocation = mode === 'interactive' || mode === 'pair-program:default' || (kitMode != null);
|
|
1019
|
+
if (isScriptInvocation) {
|
|
927
1020
|
const result = await runHeadlessProcess({
|
|
928
1021
|
workspace: workspaceDir,
|
|
929
1022
|
command,
|
|
930
|
-
sessionLabel: sessionId || 'interactive-pair'
|
|
1023
|
+
sessionLabel: sessionId || (kitMode ? `kit-${kitMode}` : (mode === 'interactive' ? 'interactive-pair' : 'pair-program'))
|
|
931
1024
|
});
|
|
932
1025
|
return sendJSON(res, result.success ? 200 : 500, { ...result, mode }, requestOrigin);
|
|
933
1026
|
}
|
|
@@ -956,10 +1049,15 @@ async function handleRequest(req, res) {
|
|
|
956
1049
|
|
|
957
1050
|
return sendJSON(res, 200, {
|
|
958
1051
|
success: true,
|
|
959
|
-
message:
|
|
1052
|
+
message: kitMode ? `Pair programming session started (kit mode: ${kitMode})`
|
|
1053
|
+
: interactive ? 'Interactive pair programming session started'
|
|
1054
|
+
: 'Pair programming session started',
|
|
960
1055
|
mode,
|
|
1056
|
+
kitMode: kitMode || null,
|
|
1057
|
+
archetype: archetype || null,
|
|
1058
|
+
pattern: pattern || null,
|
|
961
1059
|
workspace: workspaceDir,
|
|
962
|
-
task,
|
|
1060
|
+
task: task || null,
|
|
963
1061
|
file: file || null
|
|
964
1062
|
}, requestOrigin);
|
|
965
1063
|
} catch (error) {
|
|
@@ -1338,16 +1436,53 @@ function connectRelayWebSocket() {
|
|
|
1338
1436
|
}
|
|
1339
1437
|
|
|
1340
1438
|
const interactive = msg.interactive === true;
|
|
1341
|
-
|
|
1342
|
-
|
|
1439
|
+
const kitMode = typeof msg.mode === 'string' && KIT_MODES.has(msg.mode) ? msg.mode : null;
|
|
1440
|
+
const archetype = typeof msg.archetype === 'string' ? msg.archetype : null;
|
|
1441
|
+
const pattern = typeof msg.pattern === 'string' ? msg.pattern : null;
|
|
1442
|
+
const productSlug = typeof msg.productSlug === 'string' ? msg.productSlug : null;
|
|
1443
|
+
const projectSlug = typeof msg.projectSlug === 'string' ? msg.projectSlug : null;
|
|
1444
|
+
const portalUserId = typeof msg.userId === 'string' ? msg.userId : null;
|
|
1445
|
+
const portalWorkspaceId = typeof msg.workspaceId === 'string' ? msg.workspaceId : null;
|
|
1446
|
+
const portalTaskIds = Array.isArray(msg.taskIds)
|
|
1447
|
+
? msg.taskIds.filter(t => typeof t === 'string' && t)
|
|
1448
|
+
: (typeof msg.taskId === 'string' ? [msg.taskId] : []);
|
|
1449
|
+
const ppScript = resolvePairProgramScript();
|
|
1450
|
+
const routeViaPairProgram = (kitMode || (ppScript && !interactive && taskPrompt));
|
|
1451
|
+
|
|
1452
|
+
if (routeViaPairProgram && ppScript) {
|
|
1453
|
+
command = buildPairProgramCommand({
|
|
1454
|
+
scriptPath: ppScript,
|
|
1455
|
+
workspace: wsDir,
|
|
1456
|
+
mode: kitMode || null,
|
|
1457
|
+
archetype,
|
|
1458
|
+
pattern,
|
|
1459
|
+
productSlug,
|
|
1460
|
+
projectSlug,
|
|
1461
|
+
taskPrompt: taskPrompt || '',
|
|
1462
|
+
userId: portalUserId,
|
|
1463
|
+
workspaceId: portalWorkspaceId,
|
|
1464
|
+
taskIds: portalTaskIds
|
|
1465
|
+
});
|
|
1466
|
+
console.log(` 🧰 ${kitMode ? `Kit mode: ${kitMode}` : 'Pair-program (default pipeline)'}${archetype ? `/${archetype}` : ''}${pattern ? `/${pattern}` : ''}`);
|
|
1467
|
+
} else if (kitMode) {
|
|
1468
|
+
console.warn(` ⚠️ knoxis-pair-program.js not found — cannot run kit mode`);
|
|
1469
|
+
} else if (taskPrompt) {
|
|
1343
1470
|
promptFile = path.join(os.tmpdir(), `knoxis-task-${msg.requestId || Date.now()}.txt`);
|
|
1344
1471
|
fs.writeFileSync(promptFile, taskPrompt, 'utf8');
|
|
1345
1472
|
|
|
1346
1473
|
if (interactive) {
|
|
1347
|
-
// Interactive mode: use multi-turn pair programming script
|
|
1474
|
+
// Interactive mode: use multi-turn pair programming script.
|
|
1475
|
+
// Pass identity env vars so the script can build a schema-aligned
|
|
1476
|
+
// session record and POST it via portal-sync.
|
|
1348
1477
|
const scriptPath = resolveInteractiveScript();
|
|
1349
1478
|
if (scriptPath) {
|
|
1350
|
-
|
|
1479
|
+
const interactiveEnv = { KNOXIS_TASK_FILE: promptFile, KNOXIS_WORKSPACE_PATH: wsDir };
|
|
1480
|
+
if (portalUserId) interactiveEnv.KNOXIS_USER_ID = portalUserId;
|
|
1481
|
+
if (portalWorkspaceId) interactiveEnv.KNOXIS_WORKSPACE_ID = portalWorkspaceId;
|
|
1482
|
+
if (portalTaskIds && portalTaskIds.length) interactiveEnv.KNOXIS_TASK_IDS = portalTaskIds.join(',');
|
|
1483
|
+
if (productSlug) interactiveEnv.KNOXIS_PRODUCT_SLUG = productSlug;
|
|
1484
|
+
if (projectSlug) interactiveEnv.KNOXIS_PROJECT_SLUG = projectSlug;
|
|
1485
|
+
command = buildEnvCommand(interactiveEnv, `node "${scriptPath}"`);
|
|
1351
1486
|
console.log(` 🤝 Interactive mode: ${scriptPath}`);
|
|
1352
1487
|
} else {
|
|
1353
1488
|
command = buildCatCommand(promptFile) + ' | claude --dangerously-skip-permissions';
|
|
@@ -1406,6 +1541,28 @@ function connectRelayWebSocket() {
|
|
|
1406
1541
|
}
|
|
1407
1542
|
} else if (msg.type === 'connected') {
|
|
1408
1543
|
console.log(`🤝 Backend acknowledged: ${msg.message}`);
|
|
1544
|
+
} else if (msg.type === 'portal_config') {
|
|
1545
|
+
// Backend is auto-distributing the portal-sync token + URL so devs
|
|
1546
|
+
// don't paste them by hand. Merge into ~/.knoxis/config.json so
|
|
1547
|
+
// portal-sync.js picks them up on the next session save.
|
|
1548
|
+
try {
|
|
1549
|
+
ensureKnoxisDir();
|
|
1550
|
+
const cfgPath = path.join(KNOXIS_DIR, 'config.json');
|
|
1551
|
+
let existing = {};
|
|
1552
|
+
try {
|
|
1553
|
+
if (fs.existsSync(cfgPath)) existing = JSON.parse(fs.readFileSync(cfgPath, 'utf8')) || {};
|
|
1554
|
+
} catch (_) {}
|
|
1555
|
+
if (typeof msg.portalToken === 'string' && msg.portalToken) existing.portalToken = msg.portalToken;
|
|
1556
|
+
if (typeof msg.portalUrl === 'string' && msg.portalUrl) existing.portalUrl = msg.portalUrl;
|
|
1557
|
+
if (typeof msg.portalSessionsPath === 'string' && msg.portalSessionsPath) {
|
|
1558
|
+
existing.portalSessionsPath = msg.portalSessionsPath;
|
|
1559
|
+
}
|
|
1560
|
+
fs.writeFileSync(cfgPath, JSON.stringify(existing, null, 2));
|
|
1561
|
+
// Don't log the token itself.
|
|
1562
|
+
console.log(`🔐 Portal-sync config received (token len ${(msg.portalToken || '').length})`);
|
|
1563
|
+
} catch (cfgErr) {
|
|
1564
|
+
console.warn(`⚠️ Failed to persist portal_config: ${cfgErr.message}`);
|
|
1565
|
+
}
|
|
1409
1566
|
}
|
|
1410
1567
|
} catch (e) {
|
|
1411
1568
|
console.error('❌ Relay message handling error:', e.message);
|