knoxis-helper 1.9.0 → 1.10.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 +15 -1
- package/lib/session-recorder.js +46 -20
- package/package.json +1 -1
|
@@ -1413,8 +1413,22 @@ function connectRelayWebSocket() {
|
|
|
1413
1413
|
// Interactive mode: use multi-turn pair programming script
|
|
1414
1414
|
const scriptPath = resolveInteractiveScript();
|
|
1415
1415
|
if (scriptPath) {
|
|
1416
|
-
|
|
1416
|
+
// Forward QIG attribution from relay message → env vars so
|
|
1417
|
+
// SessionRecorder writes non-null workspace_id / product_slug /
|
|
1418
|
+
// project_slug / task_ids to pair_programmer_sessions.
|
|
1419
|
+
const envVars = { KNOXIS_TASK_FILE: promptFile, KNOXIS_WORKSPACE_PATH: wsDir };
|
|
1420
|
+
if (msg.userId) envVars.KNOXIS_USER_ID = msg.userId;
|
|
1421
|
+
if (msg.workspaceId) envVars.KNOXIS_WORKSPACE_ID = msg.workspaceId;
|
|
1422
|
+
if (msg.productSlug) envVars.KNOXIS_PRODUCT_SLUG = msg.productSlug;
|
|
1423
|
+
if (msg.projectSlug) envVars.KNOXIS_PROJECT_SLUG = msg.projectSlug;
|
|
1424
|
+
if (msg.kitMode) envVars.KNOXIS_KIT_MODE = msg.kitMode;
|
|
1425
|
+
if (Array.isArray(msg.taskIds) && msg.taskIds.length > 0) {
|
|
1426
|
+
envVars.KNOXIS_TASK_IDS = msg.taskIds.filter(Boolean).join(',');
|
|
1427
|
+
}
|
|
1428
|
+
command = buildEnvCommand(envVars, `node "${scriptPath}"`);
|
|
1417
1429
|
console.log(` 🤝 Interactive mode: ${scriptPath}`);
|
|
1430
|
+
const forwarded = Object.keys(envVars).filter(k => k !== 'KNOXIS_TASK_FILE' && k !== 'KNOXIS_WORKSPACE_PATH');
|
|
1431
|
+
if (forwarded.length > 0) console.log(` 🏷️ Identity vars: ${forwarded.join(', ')}`);
|
|
1418
1432
|
} else {
|
|
1419
1433
|
command = buildCatCommand(promptFile) + ' | claude --dangerously-skip-permissions';
|
|
1420
1434
|
console.warn(` ⚠️ Interactive script not found, falling back to single-shot`);
|
package/lib/session-recorder.js
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
const fs = require('fs');
|
|
4
4
|
const path = require('path');
|
|
5
5
|
const os = require('os');
|
|
6
|
+
const crypto = require('crypto');
|
|
6
7
|
const { execSync } = require('child_process');
|
|
7
8
|
const util = require('util');
|
|
8
9
|
const { exec } = require('child_process');
|
|
@@ -65,29 +66,39 @@ const MAX_BAG_BYTES = 2 * 1024 * 1024; // 2MB total — fits Postgres JSO
|
|
|
65
66
|
const MAX_FILES_PER_DIR = 50; // Bounds runaway dirs (e.g. archive of old reports).
|
|
66
67
|
const MAX_WALK_DEPTH = 5;
|
|
67
68
|
|
|
69
|
+
/**
|
|
70
|
+
* Walk the workspace's framework markdown docs and return a SPLIT representation:
|
|
71
|
+
* manifest[relPath] = { sha256, size } // small — goes into the JSONB column
|
|
72
|
+
* contents[sha256] = base64(utf8 content) // large — uploaded to Supabase Storage
|
|
73
|
+
*
|
|
74
|
+
* Legacy placeholders (oversized-file notes, truncation marker) stay as string
|
|
75
|
+
* values in the manifest — they're not real files, just metadata the frontend
|
|
76
|
+
* already knows how to render.
|
|
77
|
+
*/
|
|
68
78
|
function readStateFiles(workspace) {
|
|
69
|
-
const
|
|
79
|
+
const manifest = {};
|
|
80
|
+
const contents = {};
|
|
70
81
|
const skipped = [];
|
|
71
82
|
const state = { bytes: 0, halted: false };
|
|
72
|
-
if (!workspace) return
|
|
83
|
+
if (!workspace) return { manifest, contents };
|
|
73
84
|
|
|
74
85
|
for (const dir of STATE_DOC_DIRS) {
|
|
75
|
-
walkMdFiles(path.join(workspace, 'docs', dir), `docs/${dir}`,
|
|
86
|
+
walkMdFiles(path.join(workspace, 'docs', dir), `docs/${dir}`, manifest, contents, 0, state, skipped);
|
|
76
87
|
if (state.halted) break;
|
|
77
88
|
}
|
|
78
89
|
|
|
79
90
|
if (skipped.length > 0) {
|
|
80
|
-
|
|
91
|
+
manifest['__truncated__'] = JSON.stringify({
|
|
81
92
|
reason: `state_files bag hit ${MAX_BAG_BYTES}-byte budget`,
|
|
82
93
|
capturedBytes: state.bytes,
|
|
83
94
|
skippedFiles: skipped.slice(0, 200)
|
|
84
95
|
}, null, 2);
|
|
85
96
|
}
|
|
86
97
|
|
|
87
|
-
return
|
|
98
|
+
return { manifest, contents };
|
|
88
99
|
}
|
|
89
100
|
|
|
90
|
-
function walkMdFiles(absDir, relDir,
|
|
101
|
+
function walkMdFiles(absDir, relDir, manifest, contents, depth, state, skipped) {
|
|
91
102
|
if (state.halted || depth > MAX_WALK_DEPTH) return;
|
|
92
103
|
let entries;
|
|
93
104
|
try {
|
|
@@ -110,7 +121,7 @@ function walkMdFiles(absDir, relDir, out, depth, state, skipped) {
|
|
|
110
121
|
const absPath = path.join(absDir, entry.name);
|
|
111
122
|
const relPath = `${relDir}/${entry.name}`;
|
|
112
123
|
if (entry.isDirectory()) {
|
|
113
|
-
walkMdFiles(absPath, relPath,
|
|
124
|
+
walkMdFiles(absPath, relPath, manifest, contents, depth + 1, state, skipped);
|
|
114
125
|
} else if (entry.isFile()) {
|
|
115
126
|
if (!entry.name.endsWith('.md')) continue;
|
|
116
127
|
if (entry.name.includes('.bak-')) continue; // Skip *.bak-<timestamp> backups.
|
|
@@ -119,12 +130,12 @@ function walkMdFiles(absDir, relDir, out, depth, state, skipped) {
|
|
|
119
130
|
continue;
|
|
120
131
|
}
|
|
121
132
|
filesInDir++;
|
|
122
|
-
readSafe(absPath, relPath,
|
|
133
|
+
readSafe(absPath, relPath, manifest, contents, state, skipped);
|
|
123
134
|
}
|
|
124
135
|
}
|
|
125
136
|
}
|
|
126
137
|
|
|
127
|
-
function readSafe(absPath, relPath,
|
|
138
|
+
function readSafe(absPath, relPath, manifest, contents, state, skipped) {
|
|
128
139
|
let stat;
|
|
129
140
|
try {
|
|
130
141
|
stat = fs.statSync(absPath);
|
|
@@ -133,7 +144,9 @@ function readSafe(absPath, relPath, out, state, skipped) {
|
|
|
133
144
|
}
|
|
134
145
|
if (!stat.isFile()) return;
|
|
135
146
|
if (stat.size > MAX_STATE_FILE_BYTES) {
|
|
136
|
-
|
|
147
|
+
// Oversized files stay as inline placeholder strings — the frontend treats
|
|
148
|
+
// these as "metadata, not file content" and shows them as-is.
|
|
149
|
+
manifest[relPath] = `[File omitted: ${stat.size} bytes exceeds ${MAX_STATE_FILE_BYTES}-byte cap]`;
|
|
137
150
|
return;
|
|
138
151
|
}
|
|
139
152
|
if (state.bytes + stat.size > MAX_BAG_BYTES) {
|
|
@@ -141,12 +154,20 @@ function readSafe(absPath, relPath, out, state, skipped) {
|
|
|
141
154
|
state.halted = true;
|
|
142
155
|
return;
|
|
143
156
|
}
|
|
157
|
+
let buf;
|
|
144
158
|
try {
|
|
145
|
-
|
|
146
|
-
state.bytes += stat.size;
|
|
159
|
+
buf = fs.readFileSync(absPath);
|
|
147
160
|
} catch (e) {
|
|
148
|
-
// Read failure (perms etc.) — skip silently.
|
|
161
|
+
return; // Read failure (perms etc.) — skip silently.
|
|
162
|
+
}
|
|
163
|
+
const sha256 = crypto.createHash('sha256').update(buf).digest('hex');
|
|
164
|
+
manifest[relPath] = { sha256, size: buf.length };
|
|
165
|
+
// Dedupe: only one base64 payload per unique sha. Multiple paths with the
|
|
166
|
+
// same content reference the same Storage object.
|
|
167
|
+
if (!Object.prototype.hasOwnProperty.call(contents, sha256)) {
|
|
168
|
+
contents[sha256] = buf.toString('base64');
|
|
149
169
|
}
|
|
170
|
+
state.bytes += buf.length;
|
|
150
171
|
}
|
|
151
172
|
|
|
152
173
|
// Enumerate feature slugs from the workspace so the frontend can list features
|
|
@@ -344,6 +365,10 @@ class SessionRecorder {
|
|
|
344
365
|
_buildRecord(finalCommit, totalDiff) {
|
|
345
366
|
const completedSteps = this.steps.filter(s => s.completedAt && !s.error).length;
|
|
346
367
|
const closedCleanly = this.steps.length > 0 && completedSteps === this.steps.length;
|
|
368
|
+
// Split the state-files snapshot into a manifest (small, goes to JSONB) and
|
|
369
|
+
// a base64 contents map (large, backend offloads to Supabase Storage and
|
|
370
|
+
// strips before persisting the row).
|
|
371
|
+
const { manifest: stateFilesManifest, contents: stateFileContents } = readStateFiles(this.workspace);
|
|
347
372
|
return {
|
|
348
373
|
sessionId: this.sessionId,
|
|
349
374
|
version: '1.1.0',
|
|
@@ -367,13 +392,14 @@ class SessionRecorder {
|
|
|
367
392
|
completedSteps,
|
|
368
393
|
closedCleanly,
|
|
369
394
|
filesTouched: extractFilesTouched(totalDiff),
|
|
370
|
-
//
|
|
371
|
-
//
|
|
372
|
-
//
|
|
373
|
-
//
|
|
374
|
-
|
|
375
|
-
//
|
|
376
|
-
|
|
395
|
+
// Manifest keyed by relative path. Each entry is either a `{sha256, size}`
|
|
396
|
+
// pointer (Supabase Storage), an inline placeholder string (oversized
|
|
397
|
+
// file / truncation marker), or — in older agents — the raw content
|
|
398
|
+
// string. The frontend renders all three shapes; see useStateFileBody.
|
|
399
|
+
stateFiles: stateFilesManifest,
|
|
400
|
+
// sha → base64(utf8). Drained server-side into pair-programmer-files
|
|
401
|
+
// bucket; never persisted to the JSONB column.
|
|
402
|
+
stateFileContents,
|
|
377
403
|
// Quick index of feature slugs found in docs/features/. Lets the
|
|
378
404
|
// frontend enumerate features without parsing the bag.
|
|
379
405
|
featureSlugs: listFeatureSlugs(this.workspace),
|