knoxis-helper 1.8.6 → 1.9.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/lib/knoxis-local-agent.js +88 -236
- package/lib/session-recorder.js +95 -65
- package/package.json +1 -1
|
@@ -46,7 +46,52 @@ const ALLOWED_ORIGINS = (process.env.KNOXIS_ALLOWED_ORIGINS || '')
|
|
|
46
46
|
.map(origin => origin.trim())
|
|
47
47
|
.filter(Boolean);
|
|
48
48
|
// Merge custom origins with trusted ones
|
|
49
|
-
const ALL_ALLOWED_ORIGINS =
|
|
49
|
+
const ALL_ALLOWED_ORIGINS = Array.from(new Set([...TRUSTED_ORIGINS, ...ALLOWED_ORIGINS]));
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Collect Maker-related Markdown documents for persistence and syncing.
|
|
53
|
+
* These include backlog, PRD, glossary, product, and strategy files.
|
|
54
|
+
*/
|
|
55
|
+
function collectMakerDocs(workspaceDir) {
|
|
56
|
+
try {
|
|
57
|
+
if (!workspaceDir || !fs.existsSync(workspaceDir)) return [];
|
|
58
|
+
const makerFiles = [];
|
|
59
|
+
const candidates = [
|
|
60
|
+
'BACKLOG.md',
|
|
61
|
+
'GLOSSARY.md',
|
|
62
|
+
'PRD.md',
|
|
63
|
+
'PRODUCT.md',
|
|
64
|
+
'STRATEGY.md'
|
|
65
|
+
];
|
|
66
|
+
candidates.forEach(name => {
|
|
67
|
+
const full = path.join(workspaceDir, name);
|
|
68
|
+
if (fs.existsSync(full)) makerFiles.push(full);
|
|
69
|
+
});
|
|
70
|
+
const docsDir = path.join(workspaceDir, 'docs');
|
|
71
|
+
if (fs.existsSync(docsDir)) {
|
|
72
|
+
fs.readdirSync(docsDir).forEach(f => {
|
|
73
|
+
if (f.endsWith('.md')) makerFiles.push(path.join(docsDir, f));
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
const mvpDir = path.join(workspaceDir, '.mvp');
|
|
77
|
+
if (fs.existsSync(mvpDir)) {
|
|
78
|
+
const walk = dir => {
|
|
79
|
+
fs.readdirSync(dir, { withFileTypes: true }).forEach(entry => {
|
|
80
|
+
const res = path.join(dir, entry.name);
|
|
81
|
+
if (entry.isDirectory()) walk(res);
|
|
82
|
+
else if (entry.name.endsWith('.md')) makerFiles.push(res);
|
|
83
|
+
});
|
|
84
|
+
};
|
|
85
|
+
walk(mvpDir);
|
|
86
|
+
}
|
|
87
|
+
console.log(`📁 Maker-aware discovery found ${makerFiles.length} planning docs.`);
|
|
88
|
+
return makerFiles;
|
|
89
|
+
} catch (err) {
|
|
90
|
+
console.warn('⚠️ collectMakerDocs failed:', err.message);
|
|
91
|
+
return [];
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
50
95
|
|
|
51
96
|
const serverMeta = { secure: false, port: DEFAULT_PORT };
|
|
52
97
|
|
|
@@ -580,55 +625,6 @@ function resolveInteractiveScript() {
|
|
|
580
625
|
return null;
|
|
581
626
|
}
|
|
582
627
|
|
|
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', 'feature-kickoff', 'resume', 'session-end', 'recovery']);
|
|
598
|
-
// Every kit mode rewrites files on disk — STATUS / HANDOFF / DECISIONS for
|
|
599
|
-
// session-end, the docs/state/* + docs/features/<slug>/* bag for kickoff /
|
|
600
|
-
// feature-kickoff / resume, and recovery's triage notes. The interactive
|
|
601
|
-
// runner invokes `claude -p --session-id`, which gives Claude proper tool
|
|
602
|
-
// access; the older pair-program.js pipes to `claude` without -p and silently
|
|
603
|
-
// skips the file writes. Route every kit mode through the interactive runner
|
|
604
|
-
// so their kit prompts actually do their work.
|
|
605
|
-
const KIT_MODES_VIA_INTERACTIVE = new Set(['kickoff', 'feature-kickoff', 'resume', 'session-end', 'recovery']);
|
|
606
|
-
|
|
607
|
-
function buildPairProgramCommand({ scriptPath, workspace, mode, archetype, pattern, productSlug, projectSlug, taskPrompt, userId, workspaceId, taskIds }) {
|
|
608
|
-
const q = v => `"${escapeForDoubleQuotedShellArg(v)}"`;
|
|
609
|
-
// Mode/archetype/pattern are constrained to known short tokens — no need to quote.
|
|
610
|
-
// Slugs and paths can contain spaces or special chars — always quote.
|
|
611
|
-
const parts = [`node ${q(scriptPath)}`, `--workspace ${q(workspace)}`];
|
|
612
|
-
if (mode) parts.push(`--mode ${mode}`);
|
|
613
|
-
if (archetype) parts.push(`--archetype ${archetype}`);
|
|
614
|
-
if (pattern) parts.push(`--pattern ${pattern}`);
|
|
615
|
-
if (productSlug) parts.push(`--product-slug ${q(productSlug)}`);
|
|
616
|
-
if (projectSlug) parts.push(`--project-slug ${q(projectSlug)}`);
|
|
617
|
-
if (userId) parts.push(`--user-id ${q(userId)}`);
|
|
618
|
-
if (workspaceId) parts.push(`--workspace-id ${q(workspaceId)}`);
|
|
619
|
-
if (Array.isArray(taskIds)) {
|
|
620
|
-
for (const t of taskIds) {
|
|
621
|
-
if (t) parts.push(`--task-id ${q(t)}`);
|
|
622
|
-
}
|
|
623
|
-
}
|
|
624
|
-
if (taskPrompt && taskPrompt.length) {
|
|
625
|
-
// Use base64 to avoid shell-quoting hazards (newlines, quotes, backticks).
|
|
626
|
-
const b64 = Buffer.from(taskPrompt, 'utf8').toString('base64');
|
|
627
|
-
parts.push(`--prompt-base64 ${b64}`);
|
|
628
|
-
}
|
|
629
|
-
return parts.join(' ');
|
|
630
|
-
}
|
|
631
|
-
|
|
632
628
|
// Request handler
|
|
633
629
|
async function handleRequest(req, res) {
|
|
634
630
|
const parsedUrl = url.parse(req.url, true);
|
|
@@ -685,7 +681,14 @@ async function handleRequest(req, res) {
|
|
|
685
681
|
if (body.claudeMdContent && workspace && fs.existsSync(workspace)) {
|
|
686
682
|
try {
|
|
687
683
|
fs.writeFileSync(path.join(workspace, 'CLAUDE.md'), body.claudeMdContent, 'utf8');
|
|
688
|
-
|
|
684
|
+
console.log('📄 Wrote CLAUDE.md (' + body.claudeMdContent.length + ' chars)');
|
|
685
|
+
// Maker-aware discovery integration
|
|
686
|
+
try {
|
|
687
|
+
const makerDocs = collectMakerDocs(workspace);
|
|
688
|
+
console.log(` Maker doc sync: ${makerDocs.length} files found.`);
|
|
689
|
+
} catch (err) {
|
|
690
|
+
console.warn('⚠️ Maker doc sync failed:', err.message);
|
|
691
|
+
}
|
|
689
692
|
} catch (writeErr) {
|
|
690
693
|
console.warn('⚠️ Failed to write CLAUDE.md:', writeErr.message);
|
|
691
694
|
}
|
|
@@ -916,22 +919,8 @@ async function handleRequest(req, res) {
|
|
|
916
919
|
const body = await parseBody(req);
|
|
917
920
|
const { workspace, task, file, provider, headless, sessionId, claudeMdContent } = body;
|
|
918
921
|
const interactive = body.interactive === true || body.interactive === 'true';
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
const pattern = typeof body.pattern === 'string' ? body.pattern : null;
|
|
922
|
-
const productSlug = typeof body.productSlug === 'string' ? body.productSlug : null;
|
|
923
|
-
const projectSlug = typeof body.projectSlug === 'string' ? body.projectSlug : null;
|
|
924
|
-
// QIG portal linkage — let session records get tied back to the user / workspace / tasks.
|
|
925
|
-
const portalUserId = typeof body.userId === 'string' ? body.userId : null;
|
|
926
|
-
const portalWorkspaceId = typeof body.workspaceId === 'string' ? body.workspaceId : null;
|
|
927
|
-
const portalTaskIds = Array.isArray(body.taskIds)
|
|
928
|
-
? body.taskIds.filter(t => typeof t === 'string' && t)
|
|
929
|
-
: (typeof body.taskId === 'string' ? [body.taskId] : []);
|
|
930
|
-
|
|
931
|
-
// Kickoff and session-end modes don't strictly need a task — the kit prompt
|
|
932
|
-
// drives the interaction. For all other paths a task is still required.
|
|
933
|
-
const taskOptionalForMode = kitMode === 'kickoff' || kitMode === 'session-end';
|
|
934
|
-
if (!task && !taskOptionalForMode) {
|
|
922
|
+
|
|
923
|
+
if (!task) {
|
|
935
924
|
return sendJSON(res, 400, { success: false, error: 'Task description required' }, requestOrigin);
|
|
936
925
|
}
|
|
937
926
|
|
|
@@ -953,80 +942,32 @@ async function handleRequest(req, res) {
|
|
|
953
942
|
try {
|
|
954
943
|
fs.writeFileSync(path.join(workspaceDir, 'CLAUDE.md'), effectiveClaudeMd, 'utf8');
|
|
955
944
|
console.log(`📄 Wrote CLAUDE.md to ${workspaceDir} (${effectiveClaudeMd.length} chars)`);
|
|
945
|
+
// Maker-aware discovery integration
|
|
946
|
+
try {
|
|
947
|
+
const makerDocs = collectMakerDocs(workspaceDir);
|
|
948
|
+
console.log(` Maker doc sync: ${makerDocs.length} files found.`);
|
|
949
|
+
} catch (err) {
|
|
950
|
+
console.warn('⚠️ Maker doc sync failed:', err.message);
|
|
951
|
+
}
|
|
956
952
|
} catch (e) {
|
|
957
953
|
console.warn(`⚠️ Failed to write CLAUDE.md: ${e.message}`);
|
|
958
954
|
}
|
|
959
955
|
}
|
|
960
956
|
|
|
961
|
-
// Write the actual task to a temp file
|
|
957
|
+
// Write the actual task to a temp file
|
|
962
958
|
const promptFile = path.join(os.tmpdir(), `knoxis-task-${sessionId || Date.now()}.txt`);
|
|
963
|
-
const promptText =
|
|
964
|
-
|
|
959
|
+
const promptText = file ? `Working on file: ${file}\n\nTask: ${task}` : task;
|
|
960
|
+
fs.writeFileSync(promptFile, promptText, 'utf8');
|
|
965
961
|
|
|
966
|
-
// Determine the command to run
|
|
962
|
+
// Determine the command to run
|
|
967
963
|
let command;
|
|
968
964
|
let mode = 'single-shot';
|
|
969
965
|
|
|
970
|
-
|
|
971
|
-
// every task auto-scaffold, the standards systemIntro, auto-context, and
|
|
972
|
-
// portal-sync. Kit mode adds --mode; default mode runs the standard
|
|
973
|
-
// 4-step pipeline. Interactive (Groq) mode and the legacy claude-pipe
|
|
974
|
-
// fallback remain available when the runner isn't present.
|
|
975
|
-
const ppScript = resolvePairProgramScript();
|
|
976
|
-
const kitViaInteractive = kitMode && KIT_MODES_VIA_INTERACTIVE.has(kitMode);
|
|
977
|
-
if (kitMode && !kitViaInteractive && !ppScript) {
|
|
978
|
-
return sendJSON(res, 500, { success: false, error: 'knoxis-pair-program.js not found — reinstall knoxis-helper.' }, requestOrigin);
|
|
979
|
-
}
|
|
980
|
-
if (kitViaInteractive) {
|
|
981
|
-
const scriptPath = resolveInteractiveScript();
|
|
982
|
-
if (!scriptPath) {
|
|
983
|
-
return sendJSON(res, 500, { success: false, error: 'knoxis-interactive-pair.js not found — reinstall knoxis-helper.' }, requestOrigin);
|
|
984
|
-
}
|
|
985
|
-
const kitEnv = { KNOXIS_WORKSPACE_PATH: workspaceDir, KNOXIS_KIT_MODE: kitMode };
|
|
986
|
-
if (promptText) kitEnv.KNOXIS_TASK_FILE = promptFile;
|
|
987
|
-
if (archetype) kitEnv.KNOXIS_KIT_ARCHETYPE = archetype;
|
|
988
|
-
if (pattern) kitEnv.KNOXIS_KIT_PATTERN = pattern;
|
|
989
|
-
if (productSlug) kitEnv.KNOXIS_PRODUCT_SLUG = productSlug;
|
|
990
|
-
if (projectSlug) kitEnv.KNOXIS_PROJECT_SLUG = projectSlug;
|
|
991
|
-
if (portalUserId) kitEnv.KNOXIS_USER_ID = portalUserId;
|
|
992
|
-
if (portalWorkspaceId) kitEnv.KNOXIS_WORKSPACE_ID = portalWorkspaceId;
|
|
993
|
-
if (portalTaskIds && portalTaskIds.length) kitEnv.KNOXIS_TASK_IDS = portalTaskIds.join(',');
|
|
994
|
-
if (sessionId) kitEnv.KNOXIS_BACKEND_SESSION_ID = sessionId;
|
|
995
|
-
command = buildEnvCommand(kitEnv, `node "${scriptPath}"`);
|
|
996
|
-
mode = `kit:${kitMode}${archetype ? `/${archetype}` : ''}`;
|
|
997
|
-
console.log(`🧰 Kit (interactive runner): ${mode} — ${scriptPath}`);
|
|
998
|
-
} else if (kitMode || (ppScript && !interactive && promptText)) {
|
|
999
|
-
command = buildPairProgramCommand({
|
|
1000
|
-
scriptPath: ppScript,
|
|
1001
|
-
workspace: workspaceDir,
|
|
1002
|
-
mode: kitMode || null,
|
|
1003
|
-
archetype,
|
|
1004
|
-
pattern,
|
|
1005
|
-
productSlug,
|
|
1006
|
-
projectSlug,
|
|
1007
|
-
taskPrompt: promptText,
|
|
1008
|
-
userId: portalUserId,
|
|
1009
|
-
workspaceId: portalWorkspaceId,
|
|
1010
|
-
taskIds: portalTaskIds
|
|
1011
|
-
});
|
|
1012
|
-
mode = kitMode
|
|
1013
|
-
? `kit:${kitMode}${archetype ? `/${archetype}` : ''}${pattern ? `/${pattern}` : ''}`
|
|
1014
|
-
: 'pair-program:default';
|
|
1015
|
-
console.log(`🧰 ${kitMode ? 'Kit' : 'Pair-program (default pipeline)'}: ${mode}`);
|
|
1016
|
-
} else if (interactive) {
|
|
966
|
+
if (interactive) {
|
|
1017
967
|
const scriptPath = resolveInteractiveScript();
|
|
1018
968
|
if (scriptPath) {
|
|
1019
|
-
// Interactive mode: multi-turn with Groq pair programmer
|
|
1020
|
-
|
|
1021
|
-
// session record and POST it via portal-sync.
|
|
1022
|
-
const interactiveEnv = { KNOXIS_TASK_FILE: promptFile, KNOXIS_WORKSPACE_PATH: workspaceDir };
|
|
1023
|
-
if (portalUserId) interactiveEnv.KNOXIS_USER_ID = portalUserId;
|
|
1024
|
-
if (portalWorkspaceId) interactiveEnv.KNOXIS_WORKSPACE_ID = portalWorkspaceId;
|
|
1025
|
-
if (portalTaskIds && portalTaskIds.length) interactiveEnv.KNOXIS_TASK_IDS = portalTaskIds.join(',');
|
|
1026
|
-
if (productSlug) interactiveEnv.KNOXIS_PRODUCT_SLUG = productSlug;
|
|
1027
|
-
if (projectSlug) interactiveEnv.KNOXIS_PROJECT_SLUG = projectSlug;
|
|
1028
|
-
if (sessionId) interactiveEnv.KNOXIS_BACKEND_SESSION_ID = sessionId;
|
|
1029
|
-
command = buildEnvCommand(interactiveEnv, `node "${scriptPath}"`);
|
|
969
|
+
// Interactive mode: multi-turn with Groq pair programmer
|
|
970
|
+
command = buildEnvCommand({ KNOXIS_TASK_FILE: promptFile }, `node "${scriptPath}"`);
|
|
1030
971
|
mode = 'interactive';
|
|
1031
972
|
console.log(`🤝 Interactive mode: ${scriptPath}`);
|
|
1032
973
|
} else {
|
|
@@ -1035,19 +976,17 @@ async function handleRequest(req, res) {
|
|
|
1035
976
|
command = buildCatCommand(promptFile) + ' | claude --dangerously-skip-permissions';
|
|
1036
977
|
}
|
|
1037
978
|
} else {
|
|
1038
|
-
//
|
|
979
|
+
// Standard single-shot mode: pipe task to Claude
|
|
1039
980
|
command = buildCatCommand(promptFile) + ' | claude --dangerously-skip-permissions';
|
|
1040
981
|
}
|
|
1041
982
|
|
|
1042
983
|
if (headless) {
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
const isScriptInvocation = mode === 'interactive' || mode === 'pair-program:default' || (kitMode != null);
|
|
1046
|
-
if (isScriptInvocation) {
|
|
984
|
+
if (interactive && mode === 'interactive') {
|
|
985
|
+
// Headless interactive: run the script directly as a process
|
|
1047
986
|
const result = await runHeadlessProcess({
|
|
1048
987
|
workspace: workspaceDir,
|
|
1049
988
|
command,
|
|
1050
|
-
sessionLabel: sessionId ||
|
|
989
|
+
sessionLabel: sessionId || 'interactive-pair'
|
|
1051
990
|
});
|
|
1052
991
|
return sendJSON(res, result.success ? 200 : 500, { ...result, mode }, requestOrigin);
|
|
1053
992
|
}
|
|
@@ -1076,15 +1015,10 @@ async function handleRequest(req, res) {
|
|
|
1076
1015
|
|
|
1077
1016
|
return sendJSON(res, 200, {
|
|
1078
1017
|
success: true,
|
|
1079
|
-
message:
|
|
1080
|
-
: interactive ? 'Interactive pair programming session started'
|
|
1081
|
-
: 'Pair programming session started',
|
|
1018
|
+
message: interactive ? 'Interactive pair programming session started' : 'Pair programming session started',
|
|
1082
1019
|
mode,
|
|
1083
|
-
kitMode: kitMode || null,
|
|
1084
|
-
archetype: archetype || null,
|
|
1085
|
-
pattern: pattern || null,
|
|
1086
1020
|
workspace: workspaceDir,
|
|
1087
|
-
task
|
|
1021
|
+
task,
|
|
1088
1022
|
file: file || null
|
|
1089
1023
|
}, requestOrigin);
|
|
1090
1024
|
} catch (error) {
|
|
@@ -1437,6 +1371,13 @@ function connectRelayWebSocket() {
|
|
|
1437
1371
|
try {
|
|
1438
1372
|
fs.writeFileSync(path.join(wsDir, 'CLAUDE.md'), msg.claudeMdContent, 'utf8');
|
|
1439
1373
|
console.log(` 📄 Wrote CLAUDE.md (${msg.claudeMdContent.length} chars)`);
|
|
1374
|
+
// Maker-aware discovery integration
|
|
1375
|
+
try {
|
|
1376
|
+
const makerDocs = collectMakerDocs(wsDir);
|
|
1377
|
+
console.log(` Maker doc sync: ${makerDocs.length} files found.`);
|
|
1378
|
+
} catch (err) {
|
|
1379
|
+
console.warn('⚠️ Maker doc sync failed:', err.message);
|
|
1380
|
+
}
|
|
1440
1381
|
} catch (writeErr) {
|
|
1441
1382
|
console.warn(` ⚠️ Failed to write CLAUDE.md: ${writeErr.message}`);
|
|
1442
1383
|
}
|
|
@@ -1463,83 +1404,16 @@ function connectRelayWebSocket() {
|
|
|
1463
1404
|
}
|
|
1464
1405
|
|
|
1465
1406
|
const interactive = msg.interactive === true;
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
const pattern = typeof msg.pattern === 'string' ? msg.pattern : null;
|
|
1469
|
-
const productSlug = typeof msg.productSlug === 'string' ? msg.productSlug : null;
|
|
1470
|
-
const projectSlug = typeof msg.projectSlug === 'string' ? msg.projectSlug : null;
|
|
1471
|
-
const portalUserId = typeof msg.userId === 'string' ? msg.userId : null;
|
|
1472
|
-
const portalWorkspaceId = typeof msg.workspaceId === 'string' ? msg.workspaceId : null;
|
|
1473
|
-
const portalTaskIds = Array.isArray(msg.taskIds)
|
|
1474
|
-
? msg.taskIds.filter(t => typeof t === 'string' && t)
|
|
1475
|
-
: (typeof msg.taskId === 'string' ? [msg.taskId] : []);
|
|
1476
|
-
const ppScript = resolvePairProgramScript();
|
|
1477
|
-
const kitViaInteractive = kitMode && KIT_MODES_VIA_INTERACTIVE.has(kitMode);
|
|
1478
|
-
const routeViaPairProgram = !kitViaInteractive && (kitMode || (ppScript && !interactive && taskPrompt));
|
|
1479
|
-
|
|
1480
|
-
if (kitViaInteractive) {
|
|
1481
|
-
const scriptPath = resolveInteractiveScript();
|
|
1482
|
-
if (scriptPath) {
|
|
1483
|
-
// kickoff/resume need Claude's tool path (claude -p --session-id),
|
|
1484
|
-
// which the interactive runner provides. Pass the kit mode + identity
|
|
1485
|
-
// via env so the runner picks the kit branch instead of the 4-phase
|
|
1486
|
-
// Groq flow.
|
|
1487
|
-
if (taskPrompt) {
|
|
1488
|
-
promptFile = path.join(os.tmpdir(), `knoxis-task-${msg.requestId || Date.now()}.txt`);
|
|
1489
|
-
fs.writeFileSync(promptFile, taskPrompt, 'utf8');
|
|
1490
|
-
}
|
|
1491
|
-
const kitEnv = { KNOXIS_WORKSPACE_PATH: wsDir, KNOXIS_KIT_MODE: kitMode };
|
|
1492
|
-
if (promptFile) kitEnv.KNOXIS_TASK_FILE = promptFile;
|
|
1493
|
-
if (archetype) kitEnv.KNOXIS_KIT_ARCHETYPE = archetype;
|
|
1494
|
-
if (pattern) kitEnv.KNOXIS_KIT_PATTERN = pattern;
|
|
1495
|
-
if (productSlug) kitEnv.KNOXIS_PRODUCT_SLUG = productSlug;
|
|
1496
|
-
if (projectSlug) kitEnv.KNOXIS_PROJECT_SLUG = projectSlug;
|
|
1497
|
-
if (portalUserId) kitEnv.KNOXIS_USER_ID = portalUserId;
|
|
1498
|
-
if (portalWorkspaceId) kitEnv.KNOXIS_WORKSPACE_ID = portalWorkspaceId;
|
|
1499
|
-
if (portalTaskIds && portalTaskIds.length) kitEnv.KNOXIS_TASK_IDS = portalTaskIds.join(',');
|
|
1500
|
-
const relaySessionId = typeof msg.sessionId === 'string' ? msg.sessionId : null;
|
|
1501
|
-
if (relaySessionId) kitEnv.KNOXIS_BACKEND_SESSION_ID = relaySessionId;
|
|
1502
|
-
command = buildEnvCommand(kitEnv, `node "${scriptPath}"`);
|
|
1503
|
-
console.log(` 🧰 Kit (interactive runner): ${kitMode}${archetype ? `/${archetype}` : ''} — ${scriptPath}`);
|
|
1504
|
-
} else {
|
|
1505
|
-
console.warn(` ⚠️ knoxis-interactive-pair.js not found — cannot run kit mode ${kitMode}`);
|
|
1506
|
-
}
|
|
1507
|
-
} else if (routeViaPairProgram && ppScript) {
|
|
1508
|
-
command = buildPairProgramCommand({
|
|
1509
|
-
scriptPath: ppScript,
|
|
1510
|
-
workspace: wsDir,
|
|
1511
|
-
mode: kitMode || null,
|
|
1512
|
-
archetype,
|
|
1513
|
-
pattern,
|
|
1514
|
-
productSlug,
|
|
1515
|
-
projectSlug,
|
|
1516
|
-
taskPrompt: taskPrompt || '',
|
|
1517
|
-
userId: portalUserId,
|
|
1518
|
-
workspaceId: portalWorkspaceId,
|
|
1519
|
-
taskIds: portalTaskIds
|
|
1520
|
-
});
|
|
1521
|
-
console.log(` 🧰 ${kitMode ? `Kit mode: ${kitMode}` : 'Pair-program (default pipeline)'}${archetype ? `/${archetype}` : ''}${pattern ? `/${pattern}` : ''}`);
|
|
1522
|
-
} else if (kitMode) {
|
|
1523
|
-
console.warn(` ⚠️ knoxis-pair-program.js not found — cannot run kit mode`);
|
|
1524
|
-
} else if (taskPrompt) {
|
|
1407
|
+
|
|
1408
|
+
if (taskPrompt) {
|
|
1525
1409
|
promptFile = path.join(os.tmpdir(), `knoxis-task-${msg.requestId || Date.now()}.txt`);
|
|
1526
1410
|
fs.writeFileSync(promptFile, taskPrompt, 'utf8');
|
|
1527
1411
|
|
|
1528
1412
|
if (interactive) {
|
|
1529
|
-
// Interactive mode: use multi-turn pair programming script
|
|
1530
|
-
// Pass identity env vars so the script can build a schema-aligned
|
|
1531
|
-
// session record and POST it via portal-sync.
|
|
1413
|
+
// Interactive mode: use multi-turn pair programming script
|
|
1532
1414
|
const scriptPath = resolveInteractiveScript();
|
|
1533
1415
|
if (scriptPath) {
|
|
1534
|
-
|
|
1535
|
-
if (portalUserId) interactiveEnv.KNOXIS_USER_ID = portalUserId;
|
|
1536
|
-
if (portalWorkspaceId) interactiveEnv.KNOXIS_WORKSPACE_ID = portalWorkspaceId;
|
|
1537
|
-
if (portalTaskIds && portalTaskIds.length) interactiveEnv.KNOXIS_TASK_IDS = portalTaskIds.join(',');
|
|
1538
|
-
if (productSlug) interactiveEnv.KNOXIS_PRODUCT_SLUG = productSlug;
|
|
1539
|
-
if (projectSlug) interactiveEnv.KNOXIS_PROJECT_SLUG = projectSlug;
|
|
1540
|
-
const relaySessionId = typeof msg.sessionId === 'string' ? msg.sessionId : null;
|
|
1541
|
-
if (relaySessionId) interactiveEnv.KNOXIS_BACKEND_SESSION_ID = relaySessionId;
|
|
1542
|
-
command = buildEnvCommand(interactiveEnv, `node "${scriptPath}"`);
|
|
1416
|
+
command = buildEnvCommand({ KNOXIS_TASK_FILE: promptFile }, `node "${scriptPath}"`);
|
|
1543
1417
|
console.log(` 🤝 Interactive mode: ${scriptPath}`);
|
|
1544
1418
|
} else {
|
|
1545
1419
|
command = buildCatCommand(promptFile) + ' | claude --dangerously-skip-permissions';
|
|
@@ -1598,28 +1472,6 @@ function connectRelayWebSocket() {
|
|
|
1598
1472
|
}
|
|
1599
1473
|
} else if (msg.type === 'connected') {
|
|
1600
1474
|
console.log(`🤝 Backend acknowledged: ${msg.message}`);
|
|
1601
|
-
} else if (msg.type === 'portal_config') {
|
|
1602
|
-
// Backend is auto-distributing the portal-sync token + URL so devs
|
|
1603
|
-
// don't paste them by hand. Merge into ~/.knoxis/config.json so
|
|
1604
|
-
// portal-sync.js picks them up on the next session save.
|
|
1605
|
-
try {
|
|
1606
|
-
ensureKnoxisDir();
|
|
1607
|
-
const cfgPath = path.join(KNOXIS_DIR, 'config.json');
|
|
1608
|
-
let existing = {};
|
|
1609
|
-
try {
|
|
1610
|
-
if (fs.existsSync(cfgPath)) existing = JSON.parse(fs.readFileSync(cfgPath, 'utf8')) || {};
|
|
1611
|
-
} catch (_) {}
|
|
1612
|
-
if (typeof msg.portalToken === 'string' && msg.portalToken) existing.portalToken = msg.portalToken;
|
|
1613
|
-
if (typeof msg.portalUrl === 'string' && msg.portalUrl) existing.portalUrl = msg.portalUrl;
|
|
1614
|
-
if (typeof msg.portalSessionsPath === 'string' && msg.portalSessionsPath) {
|
|
1615
|
-
existing.portalSessionsPath = msg.portalSessionsPath;
|
|
1616
|
-
}
|
|
1617
|
-
fs.writeFileSync(cfgPath, JSON.stringify(existing, null, 2));
|
|
1618
|
-
// Don't log the token itself.
|
|
1619
|
-
console.log(`🔐 Portal-sync config received (token len ${(msg.portalToken || '').length})`);
|
|
1620
|
-
} catch (cfgErr) {
|
|
1621
|
-
console.warn(`⚠️ Failed to persist portal_config: ${cfgErr.message}`);
|
|
1622
|
-
}
|
|
1623
1475
|
}
|
|
1624
1476
|
} catch (e) {
|
|
1625
1477
|
console.error('❌ Relay message handling error:', e.message);
|
|
@@ -1803,4 +1655,4 @@ server.listen(serverMeta.port, () => {
|
|
|
1803
1655
|
console.log('✅ HTTPS enabled - ready for secure connections from deployed frontends');
|
|
1804
1656
|
console.log('');
|
|
1805
1657
|
}
|
|
1806
|
-
});
|
|
1658
|
+
});
|
package/lib/session-recorder.js
CHANGED
|
@@ -34,89 +34,119 @@ function extractFilesTouched(diff) {
|
|
|
34
34
|
return Array.from(seen);
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
-
// Snapshot the
|
|
38
|
-
// latest STATUS / HANDOFF /
|
|
39
|
-
// on qig.ai.
|
|
40
|
-
// path; the frontend distinguishes
|
|
37
|
+
// Snapshot the project's maker-framework markdown docs so the session record
|
|
38
|
+
// carries the latest STATUS / HANDOFF / ARCHITECTURE / PRD / etc. up to the
|
|
39
|
+
// portal for display on qig.ai. Captured as a single bag keyed by relative
|
|
40
|
+
// path; the frontend distinguishes by path prefix.
|
|
41
41
|
//
|
|
42
|
-
//
|
|
43
|
-
//
|
|
44
|
-
//
|
|
45
|
-
//
|
|
42
|
+
// All 11 dirs under `docs/` listed in the maker framework are walked
|
|
43
|
+
// recursively for `.md` files in priority order, until a total-bag budget
|
|
44
|
+
// is hit. The Java PairProgrammerSessionService persists the bag to the
|
|
45
|
+
// `state_files` JSONB column.
|
|
46
46
|
//
|
|
47
|
-
//
|
|
48
|
-
//
|
|
49
|
-
//
|
|
50
|
-
const
|
|
51
|
-
'
|
|
52
|
-
'
|
|
53
|
-
'
|
|
54
|
-
'
|
|
55
|
-
'
|
|
56
|
-
'
|
|
57
|
-
|
|
58
|
-
'
|
|
47
|
+
// Priority order matters: when the budget is exhausted, the high-value docs
|
|
48
|
+
// must already be in. State first, then architecture, then product / planning,
|
|
49
|
+
// then features, then the rest alphabetical.
|
|
50
|
+
const STATE_DOC_DIRS = [
|
|
51
|
+
'state',
|
|
52
|
+
'architecture',
|
|
53
|
+
'product',
|
|
54
|
+
'planning',
|
|
55
|
+
'features',
|
|
56
|
+
'compliance',
|
|
57
|
+
'design',
|
|
58
|
+
'operations',
|
|
59
|
+
'reports',
|
|
60
|
+
'requirements',
|
|
61
|
+
'standards'
|
|
59
62
|
];
|
|
60
|
-
const
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
'OPEN_QUESTIONS.md',
|
|
65
|
-
'manifest.json'
|
|
66
|
-
];
|
|
67
|
-
const MAX_STATE_FILE_BYTES = 256 * 1024; // 256KB per file
|
|
68
|
-
|
|
69
|
-
function readSafe(absPath, relPath, out) {
|
|
70
|
-
try {
|
|
71
|
-
const stat = fs.statSync(absPath);
|
|
72
|
-
if (!stat.isFile()) return;
|
|
73
|
-
if (stat.size > MAX_STATE_FILE_BYTES) {
|
|
74
|
-
out[relPath] = `[File omitted: ${stat.size} bytes exceeds ${MAX_STATE_FILE_BYTES}-byte cap]`;
|
|
75
|
-
return;
|
|
76
|
-
}
|
|
77
|
-
out[relPath] = fs.readFileSync(absPath, 'utf8');
|
|
78
|
-
} catch (e) {
|
|
79
|
-
// Missing file is normal — skip silently.
|
|
80
|
-
}
|
|
81
|
-
}
|
|
63
|
+
const MAX_STATE_FILE_BYTES = 256 * 1024; // 256KB per file
|
|
64
|
+
const MAX_BAG_BYTES = 2 * 1024 * 1024; // 2MB total — fits Postgres JSONB row comfortably + keeps frontend fast.
|
|
65
|
+
const MAX_FILES_PER_DIR = 50; // Bounds runaway dirs (e.g. archive of old reports).
|
|
66
|
+
const MAX_WALK_DEPTH = 5;
|
|
82
67
|
|
|
83
68
|
function readStateFiles(workspace) {
|
|
84
69
|
const out = {};
|
|
70
|
+
const skipped = [];
|
|
71
|
+
const state = { bytes: 0, halted: false };
|
|
85
72
|
if (!workspace) return out;
|
|
86
73
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
74
|
+
for (const dir of STATE_DOC_DIRS) {
|
|
75
|
+
walkMdFiles(path.join(workspace, 'docs', dir), `docs/${dir}`, out, 0, state, skipped);
|
|
76
|
+
if (state.halted) break;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (skipped.length > 0) {
|
|
80
|
+
out['__truncated__'] = JSON.stringify({
|
|
81
|
+
reason: `state_files bag hit ${MAX_BAG_BYTES}-byte budget`,
|
|
82
|
+
capturedBytes: state.bytes,
|
|
83
|
+
skippedFiles: skipped.slice(0, 200)
|
|
84
|
+
}, null, 2);
|
|
90
85
|
}
|
|
91
86
|
|
|
92
|
-
|
|
93
|
-
|
|
87
|
+
return out;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function walkMdFiles(absDir, relDir, out, depth, state, skipped) {
|
|
91
|
+
if (state.halted || depth > MAX_WALK_DEPTH) return;
|
|
92
|
+
let entries;
|
|
94
93
|
try {
|
|
95
|
-
|
|
96
|
-
if (!entry.isFile() || !entry.name.endsWith('.md')) continue;
|
|
97
|
-
const rel = `docs/architecture/adr/${entry.name}`;
|
|
98
|
-
readSafe(path.join(adrDir, entry.name), rel, out);
|
|
99
|
-
}
|
|
94
|
+
entries = fs.readdirSync(absDir, { withFileTypes: true });
|
|
100
95
|
} catch (e) {
|
|
101
|
-
//
|
|
96
|
+
return; // Missing or unreadable dir — skip silently.
|
|
102
97
|
}
|
|
103
98
|
|
|
104
|
-
//
|
|
105
|
-
//
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
99
|
+
// Sort: directories last, files alphabetically. Gives stable, predictable
|
|
100
|
+
// capture order so truncation skips the same low-priority files each run.
|
|
101
|
+
entries.sort((a, b) => {
|
|
102
|
+
if (a.isDirectory() !== b.isDirectory()) return a.isDirectory() ? 1 : -1;
|
|
103
|
+
return a.name.localeCompare(b.name);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
let filesInDir = 0;
|
|
107
|
+
for (const entry of entries) {
|
|
108
|
+
if (state.halted) return;
|
|
109
|
+
if (entry.name.startsWith('.')) continue; // Skip hidden entries.
|
|
110
|
+
const absPath = path.join(absDir, entry.name);
|
|
111
|
+
const relPath = `${relDir}/${entry.name}`;
|
|
112
|
+
if (entry.isDirectory()) {
|
|
113
|
+
walkMdFiles(absPath, relPath, out, depth + 1, state, skipped);
|
|
114
|
+
} else if (entry.isFile()) {
|
|
115
|
+
if (!entry.name.endsWith('.md')) continue;
|
|
116
|
+
if (entry.name.includes('.bak-')) continue; // Skip *.bak-<timestamp> backups.
|
|
117
|
+
if (filesInDir >= MAX_FILES_PER_DIR) {
|
|
118
|
+
skipped.push(`${relPath} (per-dir cap)`);
|
|
119
|
+
continue;
|
|
113
120
|
}
|
|
121
|
+
filesInDir++;
|
|
122
|
+
readSafe(absPath, relPath, out, state, skipped);
|
|
114
123
|
}
|
|
115
|
-
} catch (e) {
|
|
116
|
-
// No features dir is normal.
|
|
117
124
|
}
|
|
125
|
+
}
|
|
118
126
|
|
|
119
|
-
|
|
127
|
+
function readSafe(absPath, relPath, out, state, skipped) {
|
|
128
|
+
let stat;
|
|
129
|
+
try {
|
|
130
|
+
stat = fs.statSync(absPath);
|
|
131
|
+
} catch (e) {
|
|
132
|
+
return; // Missing file is normal.
|
|
133
|
+
}
|
|
134
|
+
if (!stat.isFile()) return;
|
|
135
|
+
if (stat.size > MAX_STATE_FILE_BYTES) {
|
|
136
|
+
out[relPath] = `[File omitted: ${stat.size} bytes exceeds ${MAX_STATE_FILE_BYTES}-byte cap]`;
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
if (state.bytes + stat.size > MAX_BAG_BYTES) {
|
|
140
|
+
skipped.push(`${relPath} (bag budget)`);
|
|
141
|
+
state.halted = true;
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
try {
|
|
145
|
+
out[relPath] = fs.readFileSync(absPath, 'utf8');
|
|
146
|
+
state.bytes += stat.size;
|
|
147
|
+
} catch (e) {
|
|
148
|
+
// Read failure (perms etc.) — skip silently.
|
|
149
|
+
}
|
|
120
150
|
}
|
|
121
151
|
|
|
122
152
|
// Enumerate feature slugs from the workspace so the frontend can list features
|