knoxis-helper 1.4.3 → 1.4.5

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.
@@ -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 failed - running in HTTP mode');
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 = 'cat "' + promptFile + '" | claude --dangerously-skip-permissions';
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 = `KNOXIS_TASK_FILE="${promptFile}" node "${scriptPath}"`;
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 = `cat "${promptFile}" | claude --dangerously-skip-permissions`;
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 = `cat "${promptFile}" | claude --dangerously-skip-permissions`;
921
+ command = buildCatCommand(promptFile) + ' | claude --dangerously-skip-permissions';
782
922
  }
783
923
 
784
924
  if (headless) {
@@ -876,7 +1016,15 @@ end tell
876
1016
  }
877
1017
 
878
1018
  /**
879
- * Open terminal on Windows
1019
+ * Open terminal on Windows.
1020
+ *
1021
+ * If the agent is running elevated (admin), Windows won't let us spawn an
1022
+ * unelevated child directly — `start cmd` inherits the parent's integrity
1023
+ * level. We detect that case and route through a one-shot scheduled task
1024
+ * at /rl LIMITED, which runs as the interactive user at medium integrity.
1025
+ * That matters because claude's credentials live under the user's real
1026
+ * %USERPROFILE%\.claude\, not the admin profile, so an elevated cmd can't
1027
+ * find them and hangs on launch.
880
1028
  */
881
1029
  function openWindowsTerminal(workspaceDir, command) {
882
1030
  return new Promise((resolve, reject) => {
@@ -884,16 +1032,57 @@ function openWindowsTerminal(workspaceDir, command) {
884
1032
  fs.mkdirSync(workspaceDir, { recursive: true });
885
1033
  }
886
1034
 
887
- const fullCommand = `start cmd /K "cd /d "${workspaceDir}" && ${command}"`;
1035
+ // `net session` exits 0 only when elevated.
1036
+ exec('net session >nul 2>&1', (elevErr) => {
1037
+ const isElevated = !elevErr;
888
1038
 
889
- exec(fullCommand, (error) => {
890
- if (error) {
891
- console.error('❌ Windows terminal error:', error);
892
- reject(error);
893
- } else {
894
- console.log('✅ Terminal opened on Windows');
895
- resolve();
1039
+ if (!isElevated) {
1040
+ const fullCommand = `start cmd /K "cd /d "${workspaceDir}" && ${command}"`;
1041
+ exec(fullCommand, (error) => {
1042
+ if (error) {
1043
+ console.error('❌ Windows terminal error:', error);
1044
+ reject(error);
1045
+ } else {
1046
+ console.log('✅ Terminal opened on Windows');
1047
+ resolve();
1048
+ }
1049
+ });
1050
+ return;
896
1051
  }
1052
+
1053
+ const tempBat = path.join(
1054
+ os.tmpdir(),
1055
+ `knoxis-launch-${process.pid}-${Date.now()}.bat`
1056
+ );
1057
+ const batContent = `@echo off\r\ncd /d "${workspaceDir}"\r\n${command}\r\ncmd /K\r\n`;
1058
+ fs.writeFileSync(tempBat, batContent);
1059
+
1060
+ const taskName = `KnoxisSpawn_${process.pid}_${Date.now()}`;
1061
+ const createCmd = `schtasks /create /tn "${taskName}" /tr "${tempBat}" /sc once /st 00:00 /it /rl LIMITED /f`;
1062
+
1063
+ exec(createCmd, (cErr) => {
1064
+ if (cErr) {
1065
+ try { fs.unlinkSync(tempBat); } catch (_) {}
1066
+ console.error('❌ Windows terminal error (schtasks create):', cErr);
1067
+ return reject(cErr);
1068
+ }
1069
+
1070
+ exec(`schtasks /run /tn "${taskName}"`, (rErr) => {
1071
+ // Task registration cleanup. Leave the .bat in %TEMP% — cmd may
1072
+ // still have an open handle to it, and the OS cleans %TEMP%.
1073
+ setTimeout(() => {
1074
+ exec(`schtasks /delete /tn "${taskName}" /f`, () => {});
1075
+ }, 5000);
1076
+
1077
+ if (rErr) {
1078
+ console.error('❌ Windows terminal error (schtasks run):', rErr);
1079
+ reject(rErr);
1080
+ } else {
1081
+ console.log('✅ Terminal opened on Windows (de-elevated via scheduled task)');
1082
+ resolve();
1083
+ }
1084
+ });
1085
+ });
897
1086
  });
898
1087
  });
899
1088
  }
@@ -930,34 +1119,38 @@ function openLinuxTerminal(workspaceDir, command) {
930
1119
  }
931
1120
 
932
1121
  function createServer() {
933
- // Try to load existing certs
934
- try {
935
- if (fs.existsSync(KEY_FILE) && fs.existsSync(CERT_FILE)) {
936
- const key = fs.readFileSync(KEY_FILE);
937
- 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 {
938
1126
  serverMeta.secure = true;
939
- return https.createServer({ key, cert }, handleRequest);
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);
940
1131
  }
941
- } catch (err) {
942
- console.warn('⚠️ Failed to load TLS certificates:', err.message);
943
1132
  }
944
1133
 
945
- // No certs exist - try to generate them
946
- if (!fs.existsSync(CERT_FILE) || !fs.existsSync(KEY_FILE)) {
947
- const generated = generateSelfSignedCert();
948
- if (generated && fs.existsSync(KEY_FILE) && fs.existsSync(CERT_FILE)) {
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) {
949
1143
  try {
950
- const key = fs.readFileSync(KEY_FILE);
951
- const cert = fs.readFileSync(CERT_FILE);
952
1144
  serverMeta.secure = true;
953
- return https.createServer({ key, cert }, handleRequest);
1145
+ console.log('🔒 Using freshly generated ' + fresh.source + ' certificate');
1146
+ return https.createServer(fresh.opts, handleRequest);
954
1147
  } catch (err) {
955
- console.warn('⚠️ Failed to load generated certificates:', err.message);
1148
+ console.warn('⚠️ Failed to create HTTPS server from generated certs:', err.message);
956
1149
  }
957
1150
  }
958
1151
  }
959
1152
 
960
- // Fallback to HTTP (will cause mixed content issues from HTTPS frontends)
1153
+ // 3. Fallback to HTTP (will cause mixed content issues from HTTPS frontends)
961
1154
  serverMeta.secure = false;
962
1155
  console.warn('');
963
1156
  console.warn('⚠️ RUNNING IN HTTP MODE - This will cause 405/CORS errors from HTTPS frontends!');
@@ -1154,14 +1347,14 @@ function connectRelayWebSocket() {
1154
1347
  // Interactive mode: use multi-turn pair programming script
1155
1348
  const scriptPath = resolveInteractiveScript();
1156
1349
  if (scriptPath) {
1157
- command = `KNOXIS_TASK_FILE="${promptFile}" node "${scriptPath}"`;
1350
+ command = buildEnvCommand({ KNOXIS_TASK_FILE: promptFile }, `node "${scriptPath}"`);
1158
1351
  console.log(` 🤝 Interactive mode: ${scriptPath}`);
1159
1352
  } else {
1160
- command = `cat "${promptFile}" | claude --dangerously-skip-permissions`;
1353
+ command = buildCatCommand(promptFile) + ' | claude --dangerously-skip-permissions';
1161
1354
  console.warn(` ⚠️ Interactive script not found, falling back to single-shot`);
1162
1355
  }
1163
1356
  } else {
1164
- command = `cat "${promptFile}" | claude --dangerously-skip-permissions`;
1357
+ command = buildCatCommand(promptFile) + ' | claude --dangerously-skip-permissions';
1165
1358
  }
1166
1359
  console.log(` 📝 Task written to ${promptFile} (${taskPrompt.length} chars)`);
1167
1360
  } else {
@@ -1380,9 +1573,17 @@ server.listen(serverMeta.port, () => {
1380
1573
  console.warn('║ Browsers will block requests from HTTPS sites (like qig.ai) ║');
1381
1574
  console.warn('╚══════════════════════════════════════════════════════════════╝');
1382
1575
  console.warn('');
1383
- console.warn('To fix this, either:');
1384
- console.warn(` 1. Install OpenSSL and restart (auto-generates certs)`);
1385
- console.warn(` 2. Manually place certs in: ${CERT_DIR}`);
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
+ }
1386
1587
  console.warn('');
1387
1588
  } else {
1388
1589
  console.log('✅ HTTPS enabled - ready for secure connections from deployed frontends');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "knoxis-helper",
3
- "version": "1.4.3",
3
+ "version": "1.4.5",
4
4
  "description": "Local helper for Knoxis pair programming - connects your machine to Knoxis on qig.ai",
5
5
  "bin": {
6
6
  "knoxis-helper": "./bin/knoxis-helper.js"