knoxis-helper 1.4.4 → 1.4.6
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 +180 -28
- package/lib/knoxis-pair-program.js +355 -22
- package/package.json +1 -1
|
@@ -28,6 +28,8 @@ const DEFAULT_PORT = parseInt(process.env.KNOXIS_AGENT_PORT || '3456', 10);
|
|
|
28
28
|
const CERT_DIR = process.env.KNOXIS_CERT_DIR || path.join(os.homedir(), '.knoxis', 'certs');
|
|
29
29
|
const CERT_FILE = process.env.KNOXIS_CERT_FILE || path.join(CERT_DIR, 'localhost.pem');
|
|
30
30
|
const KEY_FILE = process.env.KNOXIS_CERT_KEY || path.join(CERT_DIR, 'localhost-key.pem');
|
|
31
|
+
const PFX_FILE = process.env.KNOXIS_CERT_PFX || path.join(CERT_DIR, 'localhost.pfx');
|
|
32
|
+
const PFX_PASS = 'knoxis-local'; // Not a real secret — self-signed cert for localhost only
|
|
31
33
|
|
|
32
34
|
// Trusted origins for CORS (deployed frontends)
|
|
33
35
|
const TRUSTED_ORIGINS = [
|
|
@@ -77,10 +79,116 @@ function generateSelfSignedCert() {
|
|
|
77
79
|
return true;
|
|
78
80
|
}
|
|
79
81
|
|
|
80
|
-
console.warn('⚠️ OpenSSL
|
|
82
|
+
console.warn('⚠️ OpenSSL not available');
|
|
81
83
|
return false;
|
|
82
84
|
}
|
|
83
85
|
|
|
86
|
+
/**
|
|
87
|
+
* Windows fallback: generate a self-signed certificate via PowerShell.
|
|
88
|
+
* Uses New-SelfSignedCertificate (built into Windows 10+ / Server 2016+).
|
|
89
|
+
* Always produces a PFX file. Also attempts PEM export when .NET 5+ is present.
|
|
90
|
+
* Cert is created in the user's personal store (no admin required) and removed
|
|
91
|
+
* from the store after export.
|
|
92
|
+
*/
|
|
93
|
+
function generateSelfSignedCertWindows() {
|
|
94
|
+
console.log('🔐 Generating self-signed certificate via PowerShell...');
|
|
95
|
+
|
|
96
|
+
if (!fs.existsSync(CERT_DIR)) {
|
|
97
|
+
fs.mkdirSync(CERT_DIR, { recursive: true });
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Write a PS1 script to a temp file to avoid all shell-escaping issues.
|
|
101
|
+
const psLines = [
|
|
102
|
+
'$ErrorActionPreference = "Stop"',
|
|
103
|
+
'$cert = New-SelfSignedCertificate -Subject "CN=localhost" -DnsName "localhost","127.0.0.1" `',
|
|
104
|
+
' -CertStoreLocation "Cert:\\CurrentUser\\My" -NotAfter (Get-Date).AddDays(365) `',
|
|
105
|
+
' -KeyExportPolicy Exportable -KeySpec KeyExchange',
|
|
106
|
+
'$thumb = $cert.Thumbprint',
|
|
107
|
+
'',
|
|
108
|
+
'# Export PFX (works on all Windows versions with this cmdlet)',
|
|
109
|
+
'$pwd = ConvertTo-SecureString -String "' + PFX_PASS + '" -Force -AsPlainText',
|
|
110
|
+
'Export-PfxCertificate -Cert "Cert:\\CurrentUser\\My\\$thumb" -FilePath "' + PFX_FILE + '" -Password $pwd | Out-Null',
|
|
111
|
+
'',
|
|
112
|
+
'# Attempt PEM export — only works on .NET 5+ (PowerShell 7+)',
|
|
113
|
+
'try {',
|
|
114
|
+
' $certDer = $cert.Export([System.Security.Cryptography.X509Certificates.X509ContentType]::Cert)',
|
|
115
|
+
' $certB64 = [Convert]::ToBase64String($certDer, "InsertLineBreaks")',
|
|
116
|
+
' $certPem = "-----BEGIN CERTIFICATE-----`r`n$certB64`r`n-----END CERTIFICATE-----"',
|
|
117
|
+
' [System.IO.File]::WriteAllText("' + CERT_FILE + '", $certPem)',
|
|
118
|
+
'',
|
|
119
|
+
' $rsa = [System.Security.Cryptography.X509Certificates.RSACertificateExtensions]::GetRSAPrivateKey($cert)',
|
|
120
|
+
' $keyBytes = $rsa.ExportPkcs8PrivateKey()',
|
|
121
|
+
' $keyB64 = [Convert]::ToBase64String($keyBytes, "InsertLineBreaks")',
|
|
122
|
+
' $keyPem = "-----BEGIN PRIVATE KEY-----`r`n$keyB64`r`n-----END PRIVATE KEY-----"',
|
|
123
|
+
' [System.IO.File]::WriteAllText("' + KEY_FILE + '", $keyPem)',
|
|
124
|
+
'} catch {',
|
|
125
|
+
' # PEM export not available on this .NET version — PFX is sufficient',
|
|
126
|
+
'}',
|
|
127
|
+
'',
|
|
128
|
+
'# Remove cert from user store (the exported files are all we need)',
|
|
129
|
+
'Remove-Item "Cert:\\CurrentUser\\My\\$thumb" -Force -ErrorAction SilentlyContinue',
|
|
130
|
+
];
|
|
131
|
+
|
|
132
|
+
const tmpScript = path.join(os.tmpdir(), 'knoxis-cert-' + Date.now() + '.ps1');
|
|
133
|
+
fs.writeFileSync(tmpScript, psLines.join('\r\n'), 'utf8');
|
|
134
|
+
|
|
135
|
+
try {
|
|
136
|
+
const result = spawnSync('powershell', [
|
|
137
|
+
'-NoProfile', '-NonInteractive', '-ExecutionPolicy', 'Bypass',
|
|
138
|
+
'-File', tmpScript
|
|
139
|
+
], { stdio: 'pipe', timeout: 30000 });
|
|
140
|
+
|
|
141
|
+
if (result.status === 0 && fs.existsSync(PFX_FILE)) {
|
|
142
|
+
console.log('✅ Certificate generated via PowerShell');
|
|
143
|
+
if (fs.existsSync(CERT_FILE) && fs.existsSync(KEY_FILE)) {
|
|
144
|
+
console.log(' PEM files exported (PFX + PEM)');
|
|
145
|
+
} else {
|
|
146
|
+
console.log(' PFX exported (PEM not available on this .NET version)');
|
|
147
|
+
}
|
|
148
|
+
return true;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
console.warn('⚠️ PowerShell certificate generation failed');
|
|
152
|
+
if (result.stderr) {
|
|
153
|
+
const firstLine = result.stderr.toString().trim().split('\n')[0];
|
|
154
|
+
if (firstLine) console.warn(' ' + firstLine);
|
|
155
|
+
}
|
|
156
|
+
return false;
|
|
157
|
+
} finally {
|
|
158
|
+
try { fs.unlinkSync(tmpScript); } catch (e) {}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Try to create an HTTPS server from the available cert material.
|
|
164
|
+
* Returns { opts, source } on success, null on failure.
|
|
165
|
+
* Checks PEM first (cross-platform), then PFX (Windows PowerShell fallback).
|
|
166
|
+
*/
|
|
167
|
+
function tryLoadCerts() {
|
|
168
|
+
// Prefer PEM files (generated by openssl or PS .NET 5+)
|
|
169
|
+
try {
|
|
170
|
+
if (fs.existsSync(KEY_FILE) && fs.existsSync(CERT_FILE)) {
|
|
171
|
+
const key = fs.readFileSync(KEY_FILE);
|
|
172
|
+
const cert = fs.readFileSync(CERT_FILE);
|
|
173
|
+
return { opts: { key, cert }, source: 'PEM' };
|
|
174
|
+
}
|
|
175
|
+
} catch (err) {
|
|
176
|
+
console.warn('⚠️ Failed to load PEM certificates:', err.message);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// Fallback: PFX file (Windows PS 5.1 where PEM export isn't available)
|
|
180
|
+
try {
|
|
181
|
+
if (fs.existsSync(PFX_FILE)) {
|
|
182
|
+
const pfx = fs.readFileSync(PFX_FILE);
|
|
183
|
+
return { opts: { pfx, passphrase: PFX_PASS }, source: 'PFX' };
|
|
184
|
+
}
|
|
185
|
+
} catch (err) {
|
|
186
|
+
console.warn('⚠️ Failed to load PFX certificate:', err.message);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
return null;
|
|
190
|
+
}
|
|
191
|
+
|
|
84
192
|
/**
|
|
85
193
|
* Get CORS headers for the given request origin
|
|
86
194
|
*/
|
|
@@ -132,6 +240,38 @@ function buildShellCommand(command) {
|
|
|
132
240
|
return { cmd: 'bash', args: ['-lc', command] };
|
|
133
241
|
}
|
|
134
242
|
|
|
243
|
+
/**
|
|
244
|
+
* Build a cross-platform command to pipe a file's contents to stdout.
|
|
245
|
+
* Uses `type` on Windows (cmd.exe) and `cat` on Unix.
|
|
246
|
+
*/
|
|
247
|
+
function buildCatCommand(filePath) {
|
|
248
|
+
if (os.platform() === 'win32') {
|
|
249
|
+
return `type "${filePath}"`;
|
|
250
|
+
}
|
|
251
|
+
return `cat "${filePath}"`;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* Build a cross-platform command string that sets environment variables
|
|
256
|
+
* before running a child command.
|
|
257
|
+
* Unix: VAR="val" command
|
|
258
|
+
* Windows: set "VAR=val" && command
|
|
259
|
+
* @param {Record<string,string>} envVars
|
|
260
|
+
* @param {string} command
|
|
261
|
+
*/
|
|
262
|
+
function buildEnvCommand(envVars, command) {
|
|
263
|
+
if (os.platform() === 'win32') {
|
|
264
|
+
const sets = Object.entries(envVars)
|
|
265
|
+
.map(([k, v]) => `set "${k}=${v}"`)
|
|
266
|
+
.join(' && ');
|
|
267
|
+
return `${sets} && ${command}`;
|
|
268
|
+
}
|
|
269
|
+
const prefix = Object.entries(envVars)
|
|
270
|
+
.map(([k, v]) => `${k}="${v}"`)
|
|
271
|
+
.join(' ');
|
|
272
|
+
return `${prefix} ${command}`;
|
|
273
|
+
}
|
|
274
|
+
|
|
135
275
|
function commandExists(cmd) {
|
|
136
276
|
const detector = os.platform() === 'win32' ? 'where' : 'which';
|
|
137
277
|
const result = spawnSync(detector, [cmd], { stdio: 'ignore' });
|
|
@@ -509,7 +649,7 @@ async function handleRequest(req, res) {
|
|
|
509
649
|
if (prompt && prompt.trim().length > 0) {
|
|
510
650
|
promptFile = path.join(os.tmpdir(), 'knoxis-task-' + (sessionId || Date.now()) + '.txt');
|
|
511
651
|
fs.writeFileSync(promptFile, prompt, 'utf8');
|
|
512
|
-
finalCommand =
|
|
652
|
+
finalCommand = buildCatCommand(promptFile) + ' | claude --dangerously-skip-permissions';
|
|
513
653
|
console.log('📝 Task written to ' + promptFile + ' (' + prompt.length + ' chars)');
|
|
514
654
|
}
|
|
515
655
|
|
|
@@ -768,17 +908,17 @@ async function handleRequest(req, res) {
|
|
|
768
908
|
const scriptPath = resolveInteractiveScript();
|
|
769
909
|
if (scriptPath) {
|
|
770
910
|
// Interactive mode: multi-turn with Groq pair programmer
|
|
771
|
-
command =
|
|
911
|
+
command = buildEnvCommand({ KNOXIS_TASK_FILE: promptFile }, `node "${scriptPath}"`);
|
|
772
912
|
mode = 'interactive';
|
|
773
913
|
console.log(`🤝 Interactive mode: ${scriptPath}`);
|
|
774
914
|
} else {
|
|
775
915
|
// Interactive requested but script not found - fall back to single-shot
|
|
776
916
|
console.warn('⚠️ Interactive script not found, falling back to single-shot');
|
|
777
|
-
command =
|
|
917
|
+
command = buildCatCommand(promptFile) + ' | claude --dangerously-skip-permissions';
|
|
778
918
|
}
|
|
779
919
|
} else {
|
|
780
920
|
// Standard single-shot mode: pipe task to Claude
|
|
781
|
-
command =
|
|
921
|
+
command = buildCatCommand(promptFile) + ' | claude --dangerously-skip-permissions';
|
|
782
922
|
}
|
|
783
923
|
|
|
784
924
|
if (headless) {
|
|
@@ -979,34 +1119,38 @@ function openLinuxTerminal(workspaceDir, command) {
|
|
|
979
1119
|
}
|
|
980
1120
|
|
|
981
1121
|
function createServer() {
|
|
982
|
-
// Try to load existing certs
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
const cert = fs.readFileSync(CERT_FILE);
|
|
1122
|
+
// 1. Try to load existing certs (PEM or PFX)
|
|
1123
|
+
const existing = tryLoadCerts();
|
|
1124
|
+
if (existing) {
|
|
1125
|
+
try {
|
|
987
1126
|
serverMeta.secure = true;
|
|
988
|
-
|
|
1127
|
+
console.log('🔒 Loaded existing ' + existing.source + ' certificate');
|
|
1128
|
+
return https.createServer(existing.opts, handleRequest);
|
|
1129
|
+
} catch (err) {
|
|
1130
|
+
console.warn('⚠️ Failed to create HTTPS server from existing certs:', err.message);
|
|
989
1131
|
}
|
|
990
|
-
} catch (err) {
|
|
991
|
-
console.warn('⚠️ Failed to load TLS certificates:', err.message);
|
|
992
1132
|
}
|
|
993
1133
|
|
|
994
|
-
//
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
1134
|
+
// 2. Generate new certs — try openssl first, then Windows PowerShell
|
|
1135
|
+
let generated = generateSelfSignedCert();
|
|
1136
|
+
if (!generated && os.platform() === 'win32') {
|
|
1137
|
+
generated = generateSelfSignedCertWindows();
|
|
1138
|
+
}
|
|
1139
|
+
|
|
1140
|
+
if (generated) {
|
|
1141
|
+
const fresh = tryLoadCerts();
|
|
1142
|
+
if (fresh) {
|
|
998
1143
|
try {
|
|
999
|
-
const key = fs.readFileSync(KEY_FILE);
|
|
1000
|
-
const cert = fs.readFileSync(CERT_FILE);
|
|
1001
1144
|
serverMeta.secure = true;
|
|
1002
|
-
|
|
1145
|
+
console.log('🔒 Using freshly generated ' + fresh.source + ' certificate');
|
|
1146
|
+
return https.createServer(fresh.opts, handleRequest);
|
|
1003
1147
|
} catch (err) {
|
|
1004
|
-
console.warn('⚠️ Failed to
|
|
1148
|
+
console.warn('⚠️ Failed to create HTTPS server from generated certs:', err.message);
|
|
1005
1149
|
}
|
|
1006
1150
|
}
|
|
1007
1151
|
}
|
|
1008
1152
|
|
|
1009
|
-
// Fallback to HTTP (will cause mixed content issues from HTTPS frontends)
|
|
1153
|
+
// 3. Fallback to HTTP (will cause mixed content issues from HTTPS frontends)
|
|
1010
1154
|
serverMeta.secure = false;
|
|
1011
1155
|
console.warn('');
|
|
1012
1156
|
console.warn('⚠️ RUNNING IN HTTP MODE - This will cause 405/CORS errors from HTTPS frontends!');
|
|
@@ -1203,14 +1347,14 @@ function connectRelayWebSocket() {
|
|
|
1203
1347
|
// Interactive mode: use multi-turn pair programming script
|
|
1204
1348
|
const scriptPath = resolveInteractiveScript();
|
|
1205
1349
|
if (scriptPath) {
|
|
1206
|
-
command =
|
|
1350
|
+
command = buildEnvCommand({ KNOXIS_TASK_FILE: promptFile }, `node "${scriptPath}"`);
|
|
1207
1351
|
console.log(` 🤝 Interactive mode: ${scriptPath}`);
|
|
1208
1352
|
} else {
|
|
1209
|
-
command =
|
|
1353
|
+
command = buildCatCommand(promptFile) + ' | claude --dangerously-skip-permissions';
|
|
1210
1354
|
console.warn(` ⚠️ Interactive script not found, falling back to single-shot`);
|
|
1211
1355
|
}
|
|
1212
1356
|
} else {
|
|
1213
|
-
command =
|
|
1357
|
+
command = buildCatCommand(promptFile) + ' | claude --dangerously-skip-permissions';
|
|
1214
1358
|
}
|
|
1215
1359
|
console.log(` 📝 Task written to ${promptFile} (${taskPrompt.length} chars)`);
|
|
1216
1360
|
} else {
|
|
@@ -1429,9 +1573,17 @@ server.listen(serverMeta.port, () => {
|
|
|
1429
1573
|
console.warn('║ Browsers will block requests from HTTPS sites (like qig.ai) ║');
|
|
1430
1574
|
console.warn('╚══════════════════════════════════════════════════════════════╝');
|
|
1431
1575
|
console.warn('');
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1576
|
+
if (os.platform() === 'win32') {
|
|
1577
|
+
console.warn('To fix this on Windows, try one of:');
|
|
1578
|
+
console.warn(' 1. winget install ShiningLight.OpenSSL.Light (then restart)');
|
|
1579
|
+
console.warn(' 2. choco install openssl (then restart)');
|
|
1580
|
+
console.warn(' 3. Install Git for Windows (ships with OpenSSL)');
|
|
1581
|
+
console.warn(` 4. Manually place PEM or PFX certs in: ${CERT_DIR}`);
|
|
1582
|
+
} else {
|
|
1583
|
+
console.warn('To fix this, either:');
|
|
1584
|
+
console.warn(' 1. Install OpenSSL and restart (auto-generates certs)');
|
|
1585
|
+
console.warn(` 2. Manually place certs in: ${CERT_DIR}`);
|
|
1586
|
+
}
|
|
1435
1587
|
console.warn('');
|
|
1436
1588
|
} else {
|
|
1437
1589
|
console.log('✅ HTTPS enabled - ready for secure connections from deployed frontends');
|
|
@@ -5,6 +5,24 @@ const path = require('path');
|
|
|
5
5
|
const os = require('os');
|
|
6
6
|
const { spawn, spawnSync, execSync } = require('child_process');
|
|
7
7
|
|
|
8
|
+
// ===== RETRY CONFIGURATION =====
|
|
9
|
+
// Can be overridden via environment variables
|
|
10
|
+
const RETRY_CONFIG = {
|
|
11
|
+
maxRetries: parseInt(process.env.KNOXIS_MAX_RETRIES || '3'),
|
|
12
|
+
retryDelay: parseInt(process.env.KNOXIS_RETRY_DELAY || '30000'), // 30 seconds
|
|
13
|
+
gitTimeout: parseInt(process.env.KNOXIS_GIT_TIMEOUT || '30000'), // 30 seconds
|
|
14
|
+
aiCallTimeout: parseInt(process.env.KNOXIS_AI_TIMEOUT || '300000'), // 5 minutes
|
|
15
|
+
enableRetryLogging: process.env.KNOXIS_RETRY_LOG !== 'false'
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
// Global retry statistics
|
|
19
|
+
let retryStats = {
|
|
20
|
+
totalRetries: 0,
|
|
21
|
+
successfulRetries: 0,
|
|
22
|
+
failedCommands: [],
|
|
23
|
+
startTime: Date.now()
|
|
24
|
+
};
|
|
25
|
+
|
|
8
26
|
function parseArgs(argv) {
|
|
9
27
|
const args = {};
|
|
10
28
|
const multi = {};
|
|
@@ -59,7 +77,45 @@ function commandExists(cmd) {
|
|
|
59
77
|
return result.status === 0;
|
|
60
78
|
}
|
|
61
79
|
|
|
62
|
-
// Resolve workspace name to path using knoxis registry
|
|
80
|
+
// Resolve workspace name to path using knoxis registry (async version)
|
|
81
|
+
async function resolveWorkspacePathAsync(nameOrPath) {
|
|
82
|
+
const os = require('os');
|
|
83
|
+
|
|
84
|
+
// Direct path - check if exists
|
|
85
|
+
try {
|
|
86
|
+
await fs.promises.stat(nameOrPath);
|
|
87
|
+
return path.resolve(nameOrPath);
|
|
88
|
+
} catch (e) {
|
|
89
|
+
// Not a direct path, continue
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Try knoxis workspace registry
|
|
93
|
+
const workspacesFile = path.join(os.homedir(), '.knoxis', 'workspaces.json');
|
|
94
|
+
try {
|
|
95
|
+
const data = await fs.promises.readFile(workspacesFile, 'utf8');
|
|
96
|
+
const workspaces = JSON.parse(data);
|
|
97
|
+
|
|
98
|
+
// Exact match
|
|
99
|
+
if (workspaces[nameOrPath]) {
|
|
100
|
+
return workspaces[nameOrPath];
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Fuzzy match
|
|
104
|
+
const lower = nameOrPath.toLowerCase();
|
|
105
|
+
for (const [name, wsPath] of Object.entries(workspaces)) {
|
|
106
|
+
if (name.toLowerCase().includes(lower)) {
|
|
107
|
+
console.log(`Matched workspace: ${name} -> ${wsPath}`);
|
|
108
|
+
return wsPath;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
} catch (e) {
|
|
112
|
+
// Registry file not found or parse error
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return null;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Sync version kept for backward compatibility
|
|
63
119
|
function resolveWorkspacePath(nameOrPath) {
|
|
64
120
|
const os = require('os');
|
|
65
121
|
|
|
@@ -132,32 +188,62 @@ function toArray(value) {
|
|
|
132
188
|
return [value];
|
|
133
189
|
}
|
|
134
190
|
|
|
135
|
-
function gatherContext(workspace, inputs) {
|
|
191
|
+
async function gatherContext(workspace, inputs) {
|
|
136
192
|
const sections = [];
|
|
137
193
|
const labels = [];
|
|
138
194
|
const seen = new Set();
|
|
195
|
+
const MAX_FILE_SIZE = 10 * 1024 * 1024; // 10MB limit
|
|
196
|
+
const ALLOWED_EXTENSIONS = /\.(js|jsx|ts|tsx|java|py|json|md|txt|yml|yaml|xml|html|css|scss|sql|sh|bash|env|config|conf)$/i;
|
|
139
197
|
|
|
140
|
-
toArray(inputs)
|
|
198
|
+
for (const entry of toArray(inputs)) {
|
|
141
199
|
if (typeof entry !== 'string') {
|
|
142
|
-
|
|
200
|
+
continue;
|
|
143
201
|
}
|
|
144
202
|
const trimmed = entry.trim();
|
|
145
203
|
if (!trimmed) {
|
|
146
|
-
|
|
204
|
+
continue;
|
|
147
205
|
}
|
|
148
206
|
const absolute = path.isAbsolute(trimmed) ? trimmed : path.join(workspace, trimmed);
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
207
|
+
|
|
208
|
+
try {
|
|
209
|
+
const stats = await fs.promises.stat(absolute);
|
|
210
|
+
|
|
211
|
+
// Skip directories
|
|
212
|
+
if (stats.isDirectory()) {
|
|
213
|
+
console.warn(`[Context] Skipping directory: ${absolute}`);
|
|
214
|
+
continue;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// Skip if already processed
|
|
218
|
+
if (seen.has(absolute)) {
|
|
219
|
+
continue;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// Check file size
|
|
223
|
+
if (stats.size > MAX_FILE_SIZE) {
|
|
224
|
+
console.warn(`[Context] Skipping large file (${(stats.size/1048576).toFixed(2)}MB): ${absolute}`);
|
|
225
|
+
labels.push(path.basename(absolute) + ' [too large]');
|
|
226
|
+
sections.push(formatSection(path.basename(absolute), `[File too large: ${(stats.size/1048576).toFixed(2)}MB]`));
|
|
227
|
+
continue;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// Check file extension
|
|
231
|
+
if (!ALLOWED_EXTENSIONS.test(absolute)) {
|
|
232
|
+
console.warn(`[Context] Skipping non-text file: ${absolute}`);
|
|
233
|
+
continue;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
seen.add(absolute);
|
|
237
|
+
const content = await fs.promises.readFile(absolute, 'utf8');
|
|
238
|
+
const title = path.relative(workspace, absolute) || path.basename(absolute);
|
|
239
|
+
labels.push(title);
|
|
240
|
+
sections.push(formatSection(title, content));
|
|
241
|
+
} catch (err) {
|
|
242
|
+
if (err.code !== 'ENOENT') {
|
|
243
|
+
console.error(`[Context] Error reading ${absolute}: ${err.message}`);
|
|
244
|
+
}
|
|
154
245
|
}
|
|
155
|
-
|
|
156
|
-
const content = fs.readFileSync(absolute, 'utf8');
|
|
157
|
-
const title = path.relative(workspace, absolute) || path.basename(absolute);
|
|
158
|
-
labels.push(title);
|
|
159
|
-
sections.push(formatSection(title, content));
|
|
160
|
-
});
|
|
246
|
+
}
|
|
161
247
|
|
|
162
248
|
return { sections, labels };
|
|
163
249
|
}
|
|
@@ -309,7 +395,7 @@ function buildPrompt(options) {
|
|
|
309
395
|
return sections.join('\n\n');
|
|
310
396
|
}
|
|
311
397
|
|
|
312
|
-
async function
|
|
398
|
+
async function callAiBase(aiConfig, prompt, livePrinter) {
|
|
313
399
|
return new Promise((resolve, reject) => {
|
|
314
400
|
const proc = spawn(aiConfig.cmd, aiConfig.args, { stdio: ['pipe', 'pipe', 'pipe'] });
|
|
315
401
|
let stdout = '';
|
|
@@ -350,6 +436,60 @@ async function callAi(aiConfig, prompt, livePrinter) {
|
|
|
350
436
|
});
|
|
351
437
|
}
|
|
352
438
|
|
|
439
|
+
async function callAi(aiConfig, prompt, livePrinter, options = {}) {
|
|
440
|
+
const maxRetries = options.maxRetries || RETRY_CONFIG.maxRetries;
|
|
441
|
+
const retryDelay = options.retryDelay || RETRY_CONFIG.retryDelay;
|
|
442
|
+
const enableLogging = options.silent === false || RETRY_CONFIG.enableRetryLogging;
|
|
443
|
+
|
|
444
|
+
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
|
445
|
+
try {
|
|
446
|
+
const timestamp = new Date().toISOString();
|
|
447
|
+
if (enableLogging) {
|
|
448
|
+
console.log(`[${timestamp}] [AI Call - Attempt ${attempt}/${maxRetries}] Invoking ${aiConfig.label}...`);
|
|
449
|
+
}
|
|
450
|
+
if (attempt > 1) {
|
|
451
|
+
retryStats.totalRetries++;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
const result = await callAiBase(aiConfig, prompt, livePrinter);
|
|
455
|
+
|
|
456
|
+
if (attempt > 1) {
|
|
457
|
+
retryStats.successfulRetries++;
|
|
458
|
+
if (enableLogging) {
|
|
459
|
+
console.log(`[AI Call - Success] Recovered after ${attempt} attempts`);
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
return result;
|
|
463
|
+
} catch (error) {
|
|
464
|
+
const isLastAttempt = attempt === maxRetries;
|
|
465
|
+
const timestamp = new Date().toISOString();
|
|
466
|
+
|
|
467
|
+
if (enableLogging) {
|
|
468
|
+
console.error(`[${timestamp}] [AI Call - Attempt ${attempt}/${maxRetries}] Failed`);
|
|
469
|
+
console.error(` Provider: ${aiConfig.label}`);
|
|
470
|
+
console.error(` Error: ${error.message}`);
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
if (!isLastAttempt) {
|
|
474
|
+
if (enableLogging) {
|
|
475
|
+
console.log(`[AI Call - Retry] Waiting ${retryDelay/1000} seconds before retry...`);
|
|
476
|
+
}
|
|
477
|
+
await new Promise(resolve => setTimeout(resolve, retryDelay));
|
|
478
|
+
} else {
|
|
479
|
+
if (enableLogging) {
|
|
480
|
+
console.error(`[AI Call - Failed] Max retries reached. AI call failed permanently.`);
|
|
481
|
+
}
|
|
482
|
+
retryStats.failedCommands.push({
|
|
483
|
+
command: `AI call to ${aiConfig.label}`,
|
|
484
|
+
error: error.message,
|
|
485
|
+
timestamp: timestamp
|
|
486
|
+
});
|
|
487
|
+
throw error;
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
|
|
353
493
|
// ===== SESSION RECORDING =====
|
|
354
494
|
// Records full prompts, responses, git diffs, and timing for model training
|
|
355
495
|
|
|
@@ -359,8 +499,134 @@ function ensureSessionDir() {
|
|
|
359
499
|
if (!fs.existsSync(SESSIONS_DIR)) fs.mkdirSync(SESSIONS_DIR, { recursive: true });
|
|
360
500
|
}
|
|
361
501
|
|
|
362
|
-
|
|
363
|
-
|
|
502
|
+
// Async version with proper setTimeout
|
|
503
|
+
async function safeExecAsync(cmd, cwd, options = {}) {
|
|
504
|
+
const { exec } = require('child_process');
|
|
505
|
+
const util = require('util');
|
|
506
|
+
const execAsync = util.promisify(exec);
|
|
507
|
+
|
|
508
|
+
const maxRetries = options.maxRetries || RETRY_CONFIG.maxRetries;
|
|
509
|
+
const retryDelay = options.retryDelay || RETRY_CONFIG.retryDelay;
|
|
510
|
+
const timeout = options.timeout || RETRY_CONFIG.gitTimeout;
|
|
511
|
+
const silent = options.silent || !RETRY_CONFIG.enableRetryLogging;
|
|
512
|
+
|
|
513
|
+
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
|
514
|
+
try {
|
|
515
|
+
if (!silent && attempt > 1) {
|
|
516
|
+
console.log(`[Git Retry ${attempt}/${maxRetries}] Executing: ${cmd.substring(0, 50)}...`);
|
|
517
|
+
retryStats.totalRetries++;
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
const { stdout } = await execAsync(cmd, {
|
|
521
|
+
cwd,
|
|
522
|
+
encoding: 'utf8',
|
|
523
|
+
timeout,
|
|
524
|
+
maxBuffer: 10 * 1024 * 1024 // 10MB buffer
|
|
525
|
+
});
|
|
526
|
+
|
|
527
|
+
const result = stdout.trim();
|
|
528
|
+
|
|
529
|
+
if (attempt > 1) {
|
|
530
|
+
retryStats.successfulRetries++;
|
|
531
|
+
if (!silent) {
|
|
532
|
+
console.log(`[Git Success] Command recovered after ${attempt} attempts`);
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
return result;
|
|
536
|
+
} catch (e) {
|
|
537
|
+
const isLastAttempt = attempt === maxRetries;
|
|
538
|
+
if (!silent) {
|
|
539
|
+
const timestamp = new Date().toISOString();
|
|
540
|
+
console.error(`[${timestamp}] [Git Attempt ${attempt}/${maxRetries}] Command failed: ${cmd.substring(0, 50)}...`);
|
|
541
|
+
console.error(` Error: ${e.message || 'Unknown error'}`);
|
|
542
|
+
if (e.code === 'ETIMEDOUT') {
|
|
543
|
+
console.error(` Timeout after ${timeout/1000} seconds`);
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
if (!isLastAttempt) {
|
|
548
|
+
if (!silent) {
|
|
549
|
+
console.log(` Waiting ${retryDelay/1000} seconds before retry...`);
|
|
550
|
+
}
|
|
551
|
+
// Proper async delay
|
|
552
|
+
await new Promise(resolve => setTimeout(resolve, retryDelay));
|
|
553
|
+
} else {
|
|
554
|
+
if (!silent) {
|
|
555
|
+
console.error(` Max retries reached. Command failed permanently.`);
|
|
556
|
+
}
|
|
557
|
+
retryStats.failedCommands.push({
|
|
558
|
+
command: cmd.substring(0, 100),
|
|
559
|
+
error: e.message,
|
|
560
|
+
timestamp: new Date().toISOString()
|
|
561
|
+
});
|
|
562
|
+
return null;
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
return null;
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
// Sync version (kept for backward compatibility)
|
|
570
|
+
function safeExec(cmd, cwd, options = {}) {
|
|
571
|
+
const maxRetries = options.maxRetries || RETRY_CONFIG.maxRetries;
|
|
572
|
+
const retryDelay = options.retryDelay || RETRY_CONFIG.retryDelay;
|
|
573
|
+
const timeout = options.timeout || RETRY_CONFIG.gitTimeout;
|
|
574
|
+
const silent = options.silent || !RETRY_CONFIG.enableRetryLogging;
|
|
575
|
+
|
|
576
|
+
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
|
577
|
+
try {
|
|
578
|
+
if (!silent && attempt > 1) {
|
|
579
|
+
console.log(`[Git Retry ${attempt}/${maxRetries}] Executing: ${cmd.substring(0, 50)}...`);
|
|
580
|
+
retryStats.totalRetries++;
|
|
581
|
+
}
|
|
582
|
+
const result = execSync(cmd, { cwd, encoding: 'utf8', timeout }).trim();
|
|
583
|
+
if (attempt > 1) {
|
|
584
|
+
retryStats.successfulRetries++;
|
|
585
|
+
if (!silent) {
|
|
586
|
+
console.log(`[Git Success] Command recovered after ${attempt} attempts`);
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
return result;
|
|
590
|
+
} catch (e) {
|
|
591
|
+
const isLastAttempt = attempt === maxRetries;
|
|
592
|
+
if (!silent) {
|
|
593
|
+
const timestamp = new Date().toISOString();
|
|
594
|
+
console.error(`[${timestamp}] [Git Attempt ${attempt}/${maxRetries}] Command failed: ${cmd.substring(0, 50)}...`);
|
|
595
|
+
console.error(` Error: ${e.message || 'Unknown error'}`);
|
|
596
|
+
if (e.code === 'ETIMEDOUT') {
|
|
597
|
+
console.error(` Timeout after ${timeout/1000} seconds`);
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
if (!isLastAttempt) {
|
|
602
|
+
if (!silent) {
|
|
603
|
+
console.log(` Waiting ${retryDelay/1000} seconds before retry...`);
|
|
604
|
+
}
|
|
605
|
+
// Cross-platform sleep
|
|
606
|
+
const sleepCmd = process.platform === 'win32'
|
|
607
|
+
? `powershell -Command "Start-Sleep -Seconds ${retryDelay/1000}"`
|
|
608
|
+
: `sleep ${retryDelay/1000}`;
|
|
609
|
+
try {
|
|
610
|
+
execSync(sleepCmd, { stdio: 'ignore' });
|
|
611
|
+
} catch (sleepError) {
|
|
612
|
+
// If sleep command fails, we have to skip the delay
|
|
613
|
+
// Busy-wait is too CPU intensive and blocks the event loop
|
|
614
|
+
console.warn(`[Warning] Unable to sleep between retries, continuing immediately`);
|
|
615
|
+
}
|
|
616
|
+
} else {
|
|
617
|
+
if (!silent) {
|
|
618
|
+
console.error(` Max retries reached. Command failed permanently.`);
|
|
619
|
+
}
|
|
620
|
+
retryStats.failedCommands.push({
|
|
621
|
+
command: cmd.substring(0, 100),
|
|
622
|
+
error: e.message,
|
|
623
|
+
timestamp: new Date().toISOString()
|
|
624
|
+
});
|
|
625
|
+
return null;
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
return null;
|
|
364
630
|
}
|
|
365
631
|
|
|
366
632
|
function slugify(text) {
|
|
@@ -404,6 +670,34 @@ class SessionRecorder {
|
|
|
404
670
|
s.error = error || null;
|
|
405
671
|
}
|
|
406
672
|
|
|
673
|
+
async saveAsync() {
|
|
674
|
+
const record = {
|
|
675
|
+
sessionId: this.sessionId, version: '1.0.0',
|
|
676
|
+
task: this.task, workspace: this.workspace, aiProvider: this.aiProvider,
|
|
677
|
+
startedAt: this.startedAt, completedAt: new Date().toISOString(),
|
|
678
|
+
totalDurationMs: Date.now() - new Date(this.startedAt).getTime(),
|
|
679
|
+
steps: this.steps,
|
|
680
|
+
totalSteps: this.steps.length,
|
|
681
|
+
completedSteps: this.steps.filter(s => s.completedAt && !s.error).length,
|
|
682
|
+
git: {
|
|
683
|
+
initialCommit: this.initialCommit,
|
|
684
|
+
finalCommit: await safeExecAsync('git rev-parse --short HEAD', this.workspace) || '',
|
|
685
|
+
totalDiff: await safeExecAsync('git diff', this.workspace) || ''
|
|
686
|
+
},
|
|
687
|
+
environment: { platform: os.platform(), nodeVersion: process.version }
|
|
688
|
+
};
|
|
689
|
+
const filename = `${this.sessionId}-${slugify(this.task)}.json`;
|
|
690
|
+
const filepath = path.join(SESSIONS_DIR, filename);
|
|
691
|
+
await fs.promises.writeFile(filepath, JSON.stringify(record, null, 2), 'utf8');
|
|
692
|
+
// Append to index
|
|
693
|
+
try {
|
|
694
|
+
await fs.promises.appendFile(path.join(SESSIONS_DIR, 'index.jsonl'),
|
|
695
|
+
JSON.stringify({ sessionId: record.sessionId, task: record.task, startedAt: record.startedAt, totalDurationMs: record.totalDurationMs, file: filename }) + '\n');
|
|
696
|
+
} catch (e) {}
|
|
697
|
+
return filepath;
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
// Sync version kept for backward compatibility
|
|
407
701
|
save() {
|
|
408
702
|
const record = {
|
|
409
703
|
sessionId: this.sessionId, version: '1.0.0',
|
|
@@ -484,7 +778,7 @@ async function run() {
|
|
|
484
778
|
globalContextInputs.push(timeline.sharedContext);
|
|
485
779
|
}
|
|
486
780
|
|
|
487
|
-
const globalContext = gatherContext(workspace, globalContextInputs);
|
|
781
|
+
const globalContext = await gatherContext(workspace, globalContextInputs);
|
|
488
782
|
const globalContextBlock = globalContext.sections.join('\n\n');
|
|
489
783
|
|
|
490
784
|
let scheduledSteps;
|
|
@@ -557,7 +851,7 @@ IMPORTANT: Work autonomously. Do not ask questions or wait for confirmation. Mak
|
|
|
557
851
|
Only work inside the provided workspace and preserve user data.`;
|
|
558
852
|
|
|
559
853
|
for (const step of scheduledSteps) {
|
|
560
|
-
const stepContext = gatherContext(workspace, step.contextPaths);
|
|
854
|
+
const stepContext = await gatherContext(workspace, step.contextPaths);
|
|
561
855
|
if (stepContext.labels.length) {
|
|
562
856
|
console.log(`Step context for ${step.displayName}: ${stepContext.labels.join(', ')}`);
|
|
563
857
|
console.log('');
|
|
@@ -614,10 +908,49 @@ Only work inside the provided workspace and preserve user data.`;
|
|
|
614
908
|
console.log('');
|
|
615
909
|
}
|
|
616
910
|
|
|
911
|
+
// Print retry statistics if any retries occurred
|
|
912
|
+
printRetryStatistics();
|
|
913
|
+
|
|
617
914
|
console.log('Session complete. Knoxis and the AI partner are standing by for further instructions.');
|
|
618
915
|
}
|
|
619
916
|
|
|
917
|
+
function printRetryStatistics() {
|
|
918
|
+
if (retryStats.totalRetries === 0 && retryStats.failedCommands.length === 0) {
|
|
919
|
+
return; // No retries needed, don't print stats
|
|
920
|
+
}
|
|
921
|
+
|
|
922
|
+
console.log('');
|
|
923
|
+
console.log('===== RETRY STATISTICS =====');
|
|
924
|
+
console.log(`Total Retries: ${retryStats.totalRetries}`);
|
|
925
|
+
console.log(`Successful Recoveries: ${retryStats.successfulRetries}`);
|
|
926
|
+
console.log(`Failed After Max Retries: ${retryStats.failedCommands.length}`);
|
|
927
|
+
|
|
928
|
+
if (retryStats.failedCommands.length > 0) {
|
|
929
|
+
console.log('\nFailed Commands:');
|
|
930
|
+
retryStats.failedCommands.forEach((failure, index) => {
|
|
931
|
+
console.log(` ${index + 1}. [${failure.timestamp}]`);
|
|
932
|
+
console.log(` Command: ${failure.command}`);
|
|
933
|
+
console.log(` Error: ${failure.error}`);
|
|
934
|
+
});
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
const sessionDuration = Date.now() - retryStats.startTime;
|
|
938
|
+
const minutes = Math.floor(sessionDuration / 60000);
|
|
939
|
+
const seconds = Math.floor((sessionDuration % 60000) / 1000);
|
|
940
|
+
console.log(`\nSession Duration: ${minutes}m ${seconds}s`);
|
|
941
|
+
|
|
942
|
+
if (retryStats.totalRetries > 0) {
|
|
943
|
+
const retryOverhead = retryStats.totalRetries * RETRY_CONFIG.retryDelay;
|
|
944
|
+
const overheadMinutes = Math.floor(retryOverhead / 60000);
|
|
945
|
+
const overheadSeconds = Math.floor((retryOverhead % 60000) / 1000);
|
|
946
|
+
console.log(`Estimated Retry Overhead: ${overheadMinutes}m ${overheadSeconds}s`);
|
|
947
|
+
}
|
|
948
|
+
console.log('============================');
|
|
949
|
+
console.log('');
|
|
950
|
+
}
|
|
951
|
+
|
|
620
952
|
run().catch(err => {
|
|
621
|
-
console.error(err.message);
|
|
953
|
+
console.error('Fatal error:', err.message);
|
|
954
|
+
printRetryStatistics();
|
|
622
955
|
process.exit(1);
|
|
623
956
|
});
|