knoxis-helper 1.4.7 → 1.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/knoxis-helper.js +33 -3
- package/lib/knoxis-interactive-pair.js +24 -9
- package/lib/knoxis-local-agent.js +156 -15
- package/lib/knoxis-pair-program.js +227 -40
- package/lib/portal-sync.js +149 -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,25 @@ 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 sourceTemplatesDir = path.join(libDir, 'templates');
|
|
88
103
|
const sourcePackage = path.join(__dirname, '..', 'package.json');
|
|
89
104
|
|
|
90
105
|
if (!fs.existsSync(sourceAgent)) {
|
|
@@ -125,6 +140,21 @@ function installAgentLocally(force) {
|
|
|
125
140
|
console.log(' Installed: knoxis-interactive-pair.js');
|
|
126
141
|
}
|
|
127
142
|
|
|
143
|
+
if (fs.existsSync(sourceStateScaffold)) {
|
|
144
|
+
fs.copyFileSync(sourceStateScaffold, path.join(AGENT_DIR, 'state-scaffold.js'));
|
|
145
|
+
console.log(' Installed: state-scaffold.js');
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
if (fs.existsSync(sourcePortalSync)) {
|
|
149
|
+
fs.copyFileSync(sourcePortalSync, path.join(AGENT_DIR, 'portal-sync.js'));
|
|
150
|
+
console.log(' Installed: portal-sync.js');
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
if (fs.existsSync(sourceTemplatesDir)) {
|
|
154
|
+
copyDirRecursive(sourceTemplatesDir, path.join(AGENT_DIR, 'templates'));
|
|
155
|
+
console.log(' Installed: templates/');
|
|
156
|
+
}
|
|
157
|
+
|
|
128
158
|
if (fs.existsSync(sourcePackage)) {
|
|
129
159
|
fs.copyFileSync(sourcePackage, path.join(AGENT_DIR, 'package.json'));
|
|
130
160
|
}
|
|
@@ -193,15 +193,30 @@ function runClaudeTurn(message, isResume) {
|
|
|
193
193
|
args.push('--session-id', SESSION_ID);
|
|
194
194
|
}
|
|
195
195
|
|
|
196
|
-
//
|
|
197
|
-
//
|
|
198
|
-
//
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
196
|
+
// Windows: claude is usually claude.cmd (npm shim) or claude.exe.
|
|
197
|
+
// .cmd/.bat shims require going through cmd.exe (Node CVE-2024-27980).
|
|
198
|
+
// We can't just pass `shell: true` with an args array — that triggers
|
|
199
|
+
// DEP0190 in Node 22+ (args get concatenated into the shell command
|
|
200
|
+
// line without escaping). Instead, build the command line ourselves
|
|
201
|
+
// and pass it as a single string. Our args are static flags + a UUID
|
|
202
|
+
// (no whitespace, no quotes) so trivial space-quoting is sufficient.
|
|
203
|
+
let proc;
|
|
204
|
+
if (isWindows) {
|
|
205
|
+
const quote = a => /\s/.test(a) ? `"${a}"` : a;
|
|
206
|
+
const cmdLine = ['claude', ...args].map(quote).join(' ');
|
|
207
|
+
proc = spawn(cmdLine, [], {
|
|
208
|
+
cwd: process.cwd(),
|
|
209
|
+
env: process.env,
|
|
210
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
211
|
+
shell: true
|
|
212
|
+
});
|
|
213
|
+
} else {
|
|
214
|
+
proc = spawn('claude', args, {
|
|
215
|
+
cwd: process.cwd(),
|
|
216
|
+
env: process.env,
|
|
217
|
+
stdio: ['pipe', 'pipe', 'pipe']
|
|
218
|
+
});
|
|
219
|
+
}
|
|
205
220
|
|
|
206
221
|
let stdout = '';
|
|
207
222
|
let stderr = '';
|
|
@@ -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,16 +950,44 @@ 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
993
|
// Interactive mode: multi-turn with Groq pair programmer
|
|
@@ -917,17 +1000,19 @@ async function handleRequest(req, res) {
|
|
|
917
1000
|
command = buildCatCommand(promptFile) + ' | claude --dangerously-skip-permissions';
|
|
918
1001
|
}
|
|
919
1002
|
} else {
|
|
920
|
-
//
|
|
1003
|
+
// Legacy fallback when neither pair-program nor interactive scripts are installed.
|
|
921
1004
|
command = buildCatCommand(promptFile) + ' | claude --dangerously-skip-permissions';
|
|
922
1005
|
}
|
|
923
1006
|
|
|
924
1007
|
if (headless) {
|
|
925
|
-
|
|
926
|
-
|
|
1008
|
+
// Anything routed through pair-program.js or knoxis-interactive-pair.js
|
|
1009
|
+
// is a script invocation — run the assembled command verbatim.
|
|
1010
|
+
const isScriptInvocation = mode === 'interactive' || mode === 'pair-program:default' || (kitMode != null);
|
|
1011
|
+
if (isScriptInvocation) {
|
|
927
1012
|
const result = await runHeadlessProcess({
|
|
928
1013
|
workspace: workspaceDir,
|
|
929
1014
|
command,
|
|
930
|
-
sessionLabel: sessionId || 'interactive-pair'
|
|
1015
|
+
sessionLabel: sessionId || (kitMode ? `kit-${kitMode}` : (mode === 'interactive' ? 'interactive-pair' : 'pair-program'))
|
|
931
1016
|
});
|
|
932
1017
|
return sendJSON(res, result.success ? 200 : 500, { ...result, mode }, requestOrigin);
|
|
933
1018
|
}
|
|
@@ -956,10 +1041,15 @@ async function handleRequest(req, res) {
|
|
|
956
1041
|
|
|
957
1042
|
return sendJSON(res, 200, {
|
|
958
1043
|
success: true,
|
|
959
|
-
message:
|
|
1044
|
+
message: kitMode ? `Pair programming session started (kit mode: ${kitMode})`
|
|
1045
|
+
: interactive ? 'Interactive pair programming session started'
|
|
1046
|
+
: 'Pair programming session started',
|
|
960
1047
|
mode,
|
|
1048
|
+
kitMode: kitMode || null,
|
|
1049
|
+
archetype: archetype || null,
|
|
1050
|
+
pattern: pattern || null,
|
|
961
1051
|
workspace: workspaceDir,
|
|
962
|
-
task,
|
|
1052
|
+
task: task || null,
|
|
963
1053
|
file: file || null
|
|
964
1054
|
}, requestOrigin);
|
|
965
1055
|
} catch (error) {
|
|
@@ -1338,8 +1428,37 @@ function connectRelayWebSocket() {
|
|
|
1338
1428
|
}
|
|
1339
1429
|
|
|
1340
1430
|
const interactive = msg.interactive === true;
|
|
1341
|
-
|
|
1342
|
-
|
|
1431
|
+
const kitMode = typeof msg.mode === 'string' && KIT_MODES.has(msg.mode) ? msg.mode : null;
|
|
1432
|
+
const archetype = typeof msg.archetype === 'string' ? msg.archetype : null;
|
|
1433
|
+
const pattern = typeof msg.pattern === 'string' ? msg.pattern : null;
|
|
1434
|
+
const productSlug = typeof msg.productSlug === 'string' ? msg.productSlug : null;
|
|
1435
|
+
const projectSlug = typeof msg.projectSlug === 'string' ? msg.projectSlug : null;
|
|
1436
|
+
const portalUserId = typeof msg.userId === 'string' ? msg.userId : null;
|
|
1437
|
+
const portalWorkspaceId = typeof msg.workspaceId === 'string' ? msg.workspaceId : null;
|
|
1438
|
+
const portalTaskIds = Array.isArray(msg.taskIds)
|
|
1439
|
+
? msg.taskIds.filter(t => typeof t === 'string' && t)
|
|
1440
|
+
: (typeof msg.taskId === 'string' ? [msg.taskId] : []);
|
|
1441
|
+
const ppScript = resolvePairProgramScript();
|
|
1442
|
+
const routeViaPairProgram = (kitMode || (ppScript && !interactive && taskPrompt));
|
|
1443
|
+
|
|
1444
|
+
if (routeViaPairProgram && ppScript) {
|
|
1445
|
+
command = buildPairProgramCommand({
|
|
1446
|
+
scriptPath: ppScript,
|
|
1447
|
+
workspace: wsDir,
|
|
1448
|
+
mode: kitMode || null,
|
|
1449
|
+
archetype,
|
|
1450
|
+
pattern,
|
|
1451
|
+
productSlug,
|
|
1452
|
+
projectSlug,
|
|
1453
|
+
taskPrompt: taskPrompt || '',
|
|
1454
|
+
userId: portalUserId,
|
|
1455
|
+
workspaceId: portalWorkspaceId,
|
|
1456
|
+
taskIds: portalTaskIds
|
|
1457
|
+
});
|
|
1458
|
+
console.log(` 🧰 ${kitMode ? `Kit mode: ${kitMode}` : 'Pair-program (default pipeline)'}${archetype ? `/${archetype}` : ''}${pattern ? `/${pattern}` : ''}`);
|
|
1459
|
+
} else if (kitMode) {
|
|
1460
|
+
console.warn(` ⚠️ knoxis-pair-program.js not found — cannot run kit mode`);
|
|
1461
|
+
} else if (taskPrompt) {
|
|
1343
1462
|
promptFile = path.join(os.tmpdir(), `knoxis-task-${msg.requestId || Date.now()}.txt`);
|
|
1344
1463
|
fs.writeFileSync(promptFile, taskPrompt, 'utf8');
|
|
1345
1464
|
|
|
@@ -1406,6 +1525,28 @@ function connectRelayWebSocket() {
|
|
|
1406
1525
|
}
|
|
1407
1526
|
} else if (msg.type === 'connected') {
|
|
1408
1527
|
console.log(`🤝 Backend acknowledged: ${msg.message}`);
|
|
1528
|
+
} else if (msg.type === 'portal_config') {
|
|
1529
|
+
// Backend is auto-distributing the portal-sync token + URL so devs
|
|
1530
|
+
// don't paste them by hand. Merge into ~/.knoxis/config.json so
|
|
1531
|
+
// portal-sync.js picks them up on the next session save.
|
|
1532
|
+
try {
|
|
1533
|
+
ensureKnoxisDir();
|
|
1534
|
+
const cfgPath = path.join(KNOXIS_DIR, 'config.json');
|
|
1535
|
+
let existing = {};
|
|
1536
|
+
try {
|
|
1537
|
+
if (fs.existsSync(cfgPath)) existing = JSON.parse(fs.readFileSync(cfgPath, 'utf8')) || {};
|
|
1538
|
+
} catch (_) {}
|
|
1539
|
+
if (typeof msg.portalToken === 'string' && msg.portalToken) existing.portalToken = msg.portalToken;
|
|
1540
|
+
if (typeof msg.portalUrl === 'string' && msg.portalUrl) existing.portalUrl = msg.portalUrl;
|
|
1541
|
+
if (typeof msg.portalSessionsPath === 'string' && msg.portalSessionsPath) {
|
|
1542
|
+
existing.portalSessionsPath = msg.portalSessionsPath;
|
|
1543
|
+
}
|
|
1544
|
+
fs.writeFileSync(cfgPath, JSON.stringify(existing, null, 2));
|
|
1545
|
+
// Don't log the token itself.
|
|
1546
|
+
console.log(`🔐 Portal-sync config received (token len ${(msg.portalToken || '').length})`);
|
|
1547
|
+
} catch (cfgErr) {
|
|
1548
|
+
console.warn(`⚠️ Failed to persist portal_config: ${cfgErr.message}`);
|
|
1549
|
+
}
|
|
1409
1550
|
}
|
|
1410
1551
|
} catch (e) {
|
|
1411
1552
|
console.error('❌ Relay message handling error:', e.message);
|