fraim-framework 2.0.197 → 2.0.198

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.
@@ -37,6 +37,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
37
37
  };
38
38
  Object.defineProperty(exports, "__esModule", { value: true });
39
39
  exports.AiHubServer = exports.HostConfigStore = exports.DeploymentStore = void 0;
40
+ exports.configureFraimForHubAgent = configureFraimForHubAgent;
40
41
  exports.findAvailablePort = findAvailablePort;
41
42
  exports.findAvailablePortExcluding = findAvailablePortExcluding;
42
43
  const express_1 = __importDefault(require("express"));
@@ -862,6 +863,37 @@ function hubAgentOption(hubId) {
862
863
  const frId = HUB_TO_FIRST_RUN_ID[hubId];
863
864
  return frId ? types_1.FIRST_RUN_AGENT_OPTIONS.find((o) => o.id === frId) : undefined;
864
865
  }
866
+ /**
867
+ * Issue #747: after the Hub installs an agent CLI, run the `add-ide` command for that agent so the
868
+ * FRAIM MCP (plus slash commands / rules) is wired into its config and its first run works.
869
+ * Previously the install only ran `npm install -g` + a version probe, so the agent launched with
870
+ * no `fraim` MCP server. This invokes the same `runAddIDE` the `fraim add-ide` CLI runs — scoped to
871
+ * the just-installed agent — rather than re-implementing the MCP-config write.
872
+ *
873
+ * `runAddIDE` reads the FRAIM key from ~/.fraim/config.json (written by setup/first-run) and
874
+ * `process.exit(1)`s when it is missing. We guard on that here via the exported `loadGlobalConfig`
875
+ * so a missing key degrades to a logged no-op instead of terminating the long-running Hub server.
876
+ * `skipTokenPrompts` keeps the run fully non-interactive.
877
+ */
878
+ async function configureFraimForHubAgent(hubId) {
879
+ const frId = HUB_TO_FIRST_RUN_ID[hubId];
880
+ if (!frId)
881
+ return { configured: false, error: `Unknown hub agent: ${hubId}` };
882
+ try {
883
+ const { runAddIDE, loadGlobalConfig } = await Promise.resolve().then(() => __importStar(require('../cli/commands/add-ide')));
884
+ const config = await loadGlobalConfig();
885
+ if (!config?.fraimKey) {
886
+ return { configured: false, error: 'No FRAIM key in ~/.fraim/config.json; run `fraim setup` first.' };
887
+ }
888
+ // `frId` (e.g. 'claude-code', 'codex', 'gemini-cli', 'copilot-cli') is a valid add-ide
889
+ // `--ide` name/alias; runAddIDE resolves and configures it even when not yet detected.
890
+ await runAddIDE({ ide: frId, skipTokenPrompts: true });
891
+ return { configured: true, ideName: frId };
892
+ }
893
+ catch (e) {
894
+ return { configured: false, error: e instanceof Error ? e.message : String(e) };
895
+ }
896
+ }
865
897
  function hubCommandVersion(command, extraBinDirs) {
866
898
  const executable = process.platform === 'win32' ? 'cmd.exe' : command;
867
899
  const args = process.platform === 'win32'
@@ -2529,12 +2561,19 @@ class AiHubServer {
2529
2561
  if (!ver) {
2530
2562
  throw new Error(`${option.label} install completed, but the CLI is not runnable from FRAIM's managed PATH.`);
2531
2563
  }
2564
+ // Issue #747: run `add-ide` for the newly installed agent so the FRAIM MCP is wired in
2565
+ // and its first run works (previously the agent launched with no `fraim` MCP server).
2566
+ const mcp = await configureFraimForHubAgent(hubId);
2567
+ if (!mcp.configured) {
2568
+ console.warn(`[ai-hub] install-agent: FRAIM add-ide did not run for ${option.label}: ${mcp.error || 'unknown reason'}`);
2569
+ }
2532
2570
  return res.json({
2533
2571
  ok: true,
2534
2572
  message: `${option.label} installed successfully.`,
2535
2573
  needsLogin: true,
2536
2574
  loginCommand: option.loginCommand,
2537
2575
  loginHint: `Sign in to ${option.label} to activate it. A terminal window will open — complete sign-in there, then click "Check if Ready".`,
2576
+ fraimConfigured: mcp.configured,
2538
2577
  });
2539
2578
  }
2540
2579
  catch (error) {
@@ -0,0 +1,283 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.FRAIM_PROXY_LOG_FILE = exports.DEFAULT_FRAIM_REMOTE_URL = void 0;
7
+ exports.fingerprintSecret = fingerprintSecret;
8
+ exports.appendLimited = appendLimited;
9
+ exports.sanitizeDiagnosticText = sanitizeDiagnosticText;
10
+ exports.readFileTailUtf8 = readFileTailUtf8;
11
+ exports.buildAgentDebugSteps = buildAgentDebugSteps;
12
+ exports.interpretationForClassification = interpretationForClassification;
13
+ exports.analyzeFraimProxyLog = analyzeFraimProxyLog;
14
+ exports.diagnoseFraimServerConfigConsistency = diagnoseFraimServerConfigConsistency;
15
+ const crypto_1 = __importDefault(require("crypto"));
16
+ const fs_1 = __importDefault(require("fs"));
17
+ exports.DEFAULT_FRAIM_REMOTE_URL = 'https://fraim.wellnessatwork.me';
18
+ exports.FRAIM_PROXY_LOG_FILE = 'fraim-mcp-proxy.log';
19
+ function fingerprintSecret(secret) {
20
+ return {
21
+ prefix: secret.slice(0, 10),
22
+ length: secret.length,
23
+ sha256: crypto_1.default.createHash('sha256').update(secret).digest('hex').slice(0, 16)
24
+ };
25
+ }
26
+ function normalizeUrl(url) {
27
+ return url.replace(/\/+$/, '');
28
+ }
29
+ function appendLimited(current, chunk, maxChars = 8000) {
30
+ const next = current + chunk;
31
+ return next.length > maxChars ? next.slice(-maxChars) : next;
32
+ }
33
+ function sanitizeDiagnosticText(text, knownSecrets = []) {
34
+ let sanitized = text;
35
+ for (const secret of knownSecrets.filter(Boolean)) {
36
+ sanitized = sanitized.split(secret).join(`${secret.slice(0, 10)}...`);
37
+ }
38
+ return sanitized.replace(/fraim_[A-Za-z0-9_-]+/g, (match) => `${match.slice(0, 10)}...`);
39
+ }
40
+ function readFileTailUtf8(filePath, maxBytes = 256 * 1024) {
41
+ const stat = fs_1.default.statSync(filePath);
42
+ const fileSize = stat.size;
43
+ const bytesToRead = Math.min(fileSize, maxBytes);
44
+ const buffer = Buffer.alloc(bytesToRead);
45
+ const fd = fs_1.default.openSync(filePath, 'r');
46
+ try {
47
+ fs_1.default.readSync(fd, buffer, 0, bytesToRead, Math.max(0, fileSize - bytesToRead));
48
+ }
49
+ finally {
50
+ fs_1.default.closeSync(fd);
51
+ }
52
+ return {
53
+ content: buffer.toString('utf8'),
54
+ bytesRead: bytesToRead,
55
+ fileSize
56
+ };
57
+ }
58
+ function buildAgentDebugSteps(classification) {
59
+ switch (classification) {
60
+ case 'no-log-file':
61
+ return [
62
+ 'Run the MCP client once, then inspect the FRAIM proxy log path from this diagnostic.',
63
+ 'If the log file is still absent, the IDE is not launching the FRAIM stdio proxy.'
64
+ ];
65
+ case 'no-recent-proxy-lines':
66
+ return [
67
+ 'No recent FRAIM proxy lifecycle lines were found. Restart the IDE and trigger one FRAIM tool call.',
68
+ 'Then rerun: fraim doctor --test-mcp --json.'
69
+ ];
70
+ case 'no-log-lines-for-expected-key':
71
+ return [
72
+ 'Recent proxy log lines belong to a different API key than the key doctor resolved.',
73
+ 'Check the IDE MCP config env.FRAIM_API_KEY and regenerate it with: fraim add-ide.'
74
+ ];
75
+ case 'local-proxy-started-not-ready':
76
+ return [
77
+ 'The FRAIM stdio proxy started but did not reach the ready state.',
78
+ 'Inspect stderr and the proxy log for startup errors before changing server settings.'
79
+ ];
80
+ case 'local-proxy-ready-no-initialize':
81
+ return [
82
+ 'The FRAIM stdio proxy was ready, but the IDE MCP client did not send initialize/tools traffic.',
83
+ 'Check whether the IDE loaded the correct MCP settings file, then fully restart the IDE.'
84
+ ];
85
+ case 'stdio-closed-before-request':
86
+ return [
87
+ 'The IDE launched the FRAIM stdio proxy and closed stdin before sending an MCP request.',
88
+ 'Check the MCP server command/args shape and whether the IDE rejected the stdio process during startup.'
89
+ ];
90
+ case 'message-processing-failed':
91
+ return [
92
+ 'The local proxy received malformed or unsupported stdio input.',
93
+ 'Check whether the MCP client is using newline-delimited JSON-RPC stdio, not HTTP/SSE configuration.'
94
+ ];
95
+ case 'remote-request-failed':
96
+ return [
97
+ 'The local proxy did send a request, but the remote server rejected or failed it.',
98
+ 'Use the request id and remote status in details to correlate with production logs.'
99
+ ];
100
+ case 'local-proxy-forwarded-to-remote':
101
+ return [
102
+ 'The local proxy sent MCP traffic to the remote server.',
103
+ 'If the IDE still reports disconnected, inspect the IDE-side MCP/tool-discovery logs after the remote response.'
104
+ ];
105
+ default:
106
+ return [
107
+ 'The proxy log did not match a known failure pattern.',
108
+ 'Collect the last 200 proxy log lines and rerun doctor with --json for structured details.'
109
+ ];
110
+ }
111
+ }
112
+ function interpretationForClassification(classification) {
113
+ switch (classification) {
114
+ case 'no-log-file':
115
+ return 'No FRAIM proxy log exists, so no local proxy startup has been observed on this machine.';
116
+ case 'no-recent-proxy-lines':
117
+ return 'A proxy log exists, but it does not contain usable recent FRAIM lifecycle lines.';
118
+ case 'no-log-lines-for-expected-key':
119
+ return 'The local proxy log has recent activity, but not for the API key doctor resolved for this run.';
120
+ case 'local-proxy-started-not-ready':
121
+ return 'The local proxy appears to have started but did not finish startup.';
122
+ case 'local-proxy-ready-no-initialize':
123
+ return 'The local proxy started successfully, but no MCP initialize/tools request reached it.';
124
+ case 'stdio-closed-before-request':
125
+ return 'The MCP client opened the local proxy process but closed stdio before any request was processed.';
126
+ case 'message-processing-failed':
127
+ return 'The local proxy received input but could not parse/process it as MCP JSON-RPC.';
128
+ case 'remote-request-failed':
129
+ return 'The local proxy reached the remote MCP endpoint, and the failure is now remote/auth/network-side.';
130
+ case 'local-proxy-forwarded-to-remote':
131
+ return 'The local proxy has forwarded MCP traffic to the remote endpoint.';
132
+ default:
133
+ return 'The proxy log did not match a known FRAIM MCP startup pattern.';
134
+ }
135
+ }
136
+ function analyzeFraimProxyLog(logContent, options = {}) {
137
+ const maxLines = options.maxLines ?? 300;
138
+ const allLines = logContent.split(/\r?\n/).map(line => line.trim()).filter(Boolean).slice(-maxLines);
139
+ if (allLines.length === 0) {
140
+ const classification = 'no-recent-proxy-lines';
141
+ return {
142
+ classification,
143
+ interpretation: interpretationForClassification(classification),
144
+ agentDebugSteps: buildAgentDebugSteps(classification),
145
+ matchingLineCount: 0,
146
+ startingSeen: false,
147
+ readySeen: false,
148
+ stdinClosedSeen: false,
149
+ parseErrorCount: 0,
150
+ proxyingCount: 0,
151
+ remoteFailureCount: 0
152
+ };
153
+ }
154
+ const keyLineRegex = /^\S+\s+\[([^\]]+)\]\s+(.*)$/;
155
+ const matchingLines = options.expectedKey
156
+ ? allLines.filter(line => keyLineRegex.exec(line)?.[1] === options.expectedKey)
157
+ : allLines;
158
+ if (options.expectedKey && matchingLines.length === 0) {
159
+ const classification = 'no-log-lines-for-expected-key';
160
+ return {
161
+ classification,
162
+ interpretation: interpretationForClassification(classification),
163
+ agentDebugSteps: buildAgentDebugSteps(classification),
164
+ matchingLineCount: 0,
165
+ startingSeen: false,
166
+ readySeen: false,
167
+ stdinClosedSeen: false,
168
+ parseErrorCount: 0,
169
+ proxyingCount: 0,
170
+ remoteFailureCount: 0
171
+ };
172
+ }
173
+ let startingSeen = false;
174
+ let readySeen = false;
175
+ let stdinClosedSeen = false;
176
+ let parseErrorCount = 0;
177
+ let proxyingCount = 0;
178
+ let remoteFailureCount = 0;
179
+ let lastTimestamp;
180
+ let lastMethod;
181
+ let lastRemoteUrl;
182
+ let lastRemoteStatus;
183
+ for (const line of matchingLines) {
184
+ const timestamp = line.split(/\s+/, 1)[0];
185
+ if (timestamp)
186
+ lastTimestamp = timestamp;
187
+ if (line.includes('FRAIM Local MCP Server starting'))
188
+ startingSeen = true;
189
+ if (line.includes('FRAIM Local MCP Server ready'))
190
+ readySeen = true;
191
+ if (line.includes('Stdin closed'))
192
+ stdinClosedSeen = true;
193
+ if (line.includes('Message processing failed'))
194
+ parseErrorCount += 1;
195
+ const proxyingMatch = line.match(/\[req:([^\]]+)\]\s+Proxying\s+([^\s]+)\s+to\s+([^\s]+)/);
196
+ if (proxyingMatch) {
197
+ proxyingCount += 1;
198
+ lastMethod = proxyingMatch[2];
199
+ lastRemoteUrl = proxyingMatch[3];
200
+ }
201
+ const remoteFailureMatch = line.match(/\[req:[^\]]+\]\s+Remote request failed\s+\(([^)]*)\):/);
202
+ if (remoteFailureMatch) {
203
+ remoteFailureCount += 1;
204
+ lastRemoteStatus = remoteFailureMatch[1];
205
+ }
206
+ }
207
+ let classification = 'unknown';
208
+ if (parseErrorCount > 0) {
209
+ classification = 'message-processing-failed';
210
+ }
211
+ else if (remoteFailureCount > 0) {
212
+ classification = 'remote-request-failed';
213
+ }
214
+ else if (proxyingCount > 0) {
215
+ classification = 'local-proxy-forwarded-to-remote';
216
+ }
217
+ else if (stdinClosedSeen) {
218
+ classification = 'stdio-closed-before-request';
219
+ }
220
+ else if (readySeen) {
221
+ classification = 'local-proxy-ready-no-initialize';
222
+ }
223
+ else if (startingSeen) {
224
+ classification = 'local-proxy-started-not-ready';
225
+ }
226
+ return {
227
+ classification,
228
+ interpretation: interpretationForClassification(classification),
229
+ agentDebugSteps: buildAgentDebugSteps(classification),
230
+ matchingLineCount: matchingLines.length,
231
+ startingSeen,
232
+ readySeen,
233
+ stdinClosedSeen,
234
+ parseErrorCount,
235
+ proxyingCount,
236
+ remoteFailureCount,
237
+ lastTimestamp,
238
+ lastMethod,
239
+ lastRemoteUrl,
240
+ lastRemoteStatus
241
+ };
242
+ }
243
+ function diagnoseFraimServerConfigConsistency(fraimServer, options = {}) {
244
+ const configuredKey = fraimServer?.env?.FRAIM_API_KEY ? String(fraimServer.env.FRAIM_API_KEY) : '';
245
+ const configuredRemoteUrl = String(fraimServer?.env?.FRAIM_REMOTE_URL || exports.DEFAULT_FRAIM_REMOTE_URL);
246
+ const expectedRemoteUrl = options.expectedRemoteUrl || process.env.FRAIM_REMOTE_URL || exports.DEFAULT_FRAIM_REMOTE_URL;
247
+ const apiKeyMatchesExpected = options.expectedApiKey && configuredKey
248
+ ? configuredKey === options.expectedApiKey
249
+ : null;
250
+ const remoteUrlMatchesExpected = normalizeUrl(configuredRemoteUrl) === normalizeUrl(expectedRemoteUrl);
251
+ const isLocalRemote = configuredRemoteUrl.includes('localhost') || configuredRemoteUrl.includes('127.0.0.1');
252
+ const details = {
253
+ apiKeyMatchesExpected,
254
+ remoteUrlMatchesExpected,
255
+ configuredRemoteUrl,
256
+ expectedRemoteUrl,
257
+ configuredKeyFingerprint: configuredKey ? fingerprintSecret(configuredKey) : null,
258
+ expectedKeyFingerprint: options.expectedApiKey ? fingerprintSecret(options.expectedApiKey) : null
259
+ };
260
+ const issues = [];
261
+ if (apiKeyMatchesExpected === false) {
262
+ issues.push('configured FRAIM_API_KEY differs from the key resolved by doctor');
263
+ }
264
+ if (!remoteUrlMatchesExpected) {
265
+ issues.push('configured FRAIM_REMOTE_URL differs from the expected remote URL');
266
+ }
267
+ if (isLocalRemote) {
268
+ issues.push('configured FRAIM_REMOTE_URL points at a local server');
269
+ }
270
+ if (issues.length === 0) {
271
+ return {
272
+ status: 'passed',
273
+ message: 'FRAIM MCP config key and remote URL are consistent',
274
+ details
275
+ };
276
+ }
277
+ return {
278
+ status: 'warning',
279
+ message: `FRAIM MCP config consistency warning: ${issues.join('; ')}`,
280
+ suggestion: 'Regenerate IDE MCP settings with: fraim add-ide',
281
+ details
282
+ };
283
+ }
@@ -56,11 +56,51 @@ const toml = __importStar(require("toml"));
56
56
  const ide_detector_1 = require("../../setup/ide-detector");
57
57
  const fraim_mcp_latest_launcher_1 = require("../../mcp/fraim-mcp-latest-launcher");
58
58
  const command_resolution_1 = require("../../mcp/command-resolution");
59
+ const fraim_mcp_diagnostics_1 = require("./fraim-mcp-diagnostics");
59
60
  // Cache the npm major version so execFileSync is called at most once per process.
60
61
  // Without caching, each IDE config check calls diagnoseFraimMcpLaunchPlan which calls
61
62
  // getNpmMajorVersion, and execFileSync blocks the event loop for ~2s on Windows.
62
63
  // With 8+ IDE checks running via Promise.all the blocking stacks sequentially.
63
64
  let _npmMajorVersionCache = undefined;
65
+ function getGlobalFraimConfigPath() {
66
+ return path_1.default.join(os_1.default.homedir(), '.fraim', 'config.json');
67
+ }
68
+ function resolveFraimApiKeyForDiagnostics() {
69
+ const configPath = getGlobalFraimConfigPath();
70
+ if (process.env.FRAIM_API_KEY) {
71
+ return {
72
+ present: true,
73
+ source: 'env',
74
+ configPath,
75
+ apiKey: process.env.FRAIM_API_KEY,
76
+ fingerprint: (0, fraim_mcp_diagnostics_1.fingerprintSecret)(process.env.FRAIM_API_KEY)
77
+ };
78
+ }
79
+ try {
80
+ if (!fs_1.default.existsSync(configPath)) {
81
+ return { present: false, source: 'missing', configPath };
82
+ }
83
+ const config = JSON.parse(fs_1.default.readFileSync(configPath, 'utf8'));
84
+ if (config.apiKey) {
85
+ return {
86
+ present: true,
87
+ source: 'global-config',
88
+ configPath,
89
+ apiKey: String(config.apiKey),
90
+ fingerprint: (0, fraim_mcp_diagnostics_1.fingerprintSecret)(String(config.apiKey))
91
+ };
92
+ }
93
+ return { present: false, source: 'missing', configPath };
94
+ }
95
+ catch (error) {
96
+ return {
97
+ present: false,
98
+ source: 'config-error',
99
+ configPath,
100
+ error: error.message
101
+ };
102
+ }
103
+ }
64
104
  function getNpmMajorVersion() {
65
105
  if (_npmMajorVersionCache !== undefined) {
66
106
  return _npmMajorVersionCache;
@@ -130,7 +170,7 @@ async function testFraimConnectivity() {
130
170
  }
131
171
  };
132
172
  }
133
- const globalConfigPath = path_1.default.join(os_1.default.homedir(), '.fraim', 'config.json');
173
+ const globalConfigPath = getGlobalFraimConfigPath();
134
174
  let apiKey;
135
175
  try {
136
176
  const config = JSON.parse(fs_1.default.readFileSync(globalConfigPath, 'utf8'));
@@ -164,7 +204,7 @@ async function testFraimConnectivity() {
164
204
  }
165
205
  // Test connectivity by making a request to the FRAIM server
166
206
  const startTime = Date.now();
167
- const remoteUrl = process.env.FRAIM_REMOTE_URL || 'https://fraim.wellnessatwork.me';
207
+ const remoteUrl = process.env.FRAIM_REMOTE_URL || fraim_mcp_diagnostics_1.DEFAULT_FRAIM_REMOTE_URL;
168
208
  // Debug: Log what we're testing (helpful for troubleshooting)
169
209
  if (process.env.DEBUG_DOCTOR) {
170
210
  console.log('[DOCTOR DEBUG] Testing FRAIM connectivity:');
@@ -401,6 +441,24 @@ async function validateIDEMCPConfig(ide) {
401
441
  }
402
442
  };
403
443
  }
444
+ if (fraimServer.command) {
445
+ const keyDiagnostic = resolveFraimApiKeyForDiagnostics();
446
+ const consistencyDiagnostic = (0, fraim_mcp_diagnostics_1.diagnoseFraimServerConfigConsistency)(fraimServer, {
447
+ expectedApiKey: keyDiagnostic.apiKey,
448
+ expectedRemoteUrl: process.env.FRAIM_REMOTE_URL || fraim_mcp_diagnostics_1.DEFAULT_FRAIM_REMOTE_URL
449
+ });
450
+ if (consistencyDiagnostic.status !== 'passed') {
451
+ return {
452
+ ...consistencyDiagnostic,
453
+ details: {
454
+ ...consistencyDiagnostic.details,
455
+ configPath,
456
+ keySource: keyDiagnostic.source,
457
+ keyConfigPath: keyDiagnostic.configPath
458
+ }
459
+ };
460
+ }
461
+ }
404
462
  // Check for auth header (for HTTP servers)
405
463
  if (fraimServer.url) {
406
464
  const hasAuth = fraimServer.headers?.Authorization || fraimServer.http_headers?.Authorization;
@@ -432,6 +490,84 @@ async function validateIDEMCPConfig(ide) {
432
490
  };
433
491
  }
434
492
  }
493
+ async function testFraimLocalProxyTroubleshooting() {
494
+ if (process.env.NODE_ENV === 'test') {
495
+ return {
496
+ status: 'warning',
497
+ message: 'FRAIM local proxy troubleshooting skipped (test mode)',
498
+ details: {
499
+ testMode: true,
500
+ skipped: true,
501
+ diagnosticVersion: 'fraim-mcp-startup-v1'
502
+ }
503
+ };
504
+ }
505
+ const keyDiagnostic = resolveFraimApiKeyForDiagnostics();
506
+ const logPath = path_1.default.join(os_1.default.tmpdir(), fraim_mcp_diagnostics_1.FRAIM_PROXY_LOG_FILE);
507
+ if (!keyDiagnostic.present || !keyDiagnostic.apiKey) {
508
+ return {
509
+ status: 'error',
510
+ message: 'FRAIM local proxy troubleshooting cannot resolve an API key',
511
+ suggestion: keyDiagnostic.source === 'config-error'
512
+ ? `Fix JSON syntax in ${keyDiagnostic.configPath}`
513
+ : 'Run: fraim setup or set FRAIM_API_KEY before running MCP diagnostics',
514
+ details: {
515
+ diagnosticVersion: 'fraim-mcp-startup-v1',
516
+ keySource: keyDiagnostic.source,
517
+ keyConfigPath: keyDiagnostic.configPath,
518
+ keyError: keyDiagnostic.error
519
+ }
520
+ };
521
+ }
522
+ if (!fs_1.default.existsSync(logPath)) {
523
+ const classification = 'no-log-file';
524
+ return {
525
+ status: 'warning',
526
+ message: 'FRAIM local proxy log not found',
527
+ suggestion: (0, fraim_mcp_diagnostics_1.buildAgentDebugSteps)(classification)[0],
528
+ details: {
529
+ diagnosticVersion: 'fraim-mcp-startup-v1',
530
+ classification,
531
+ interpretation: (0, fraim_mcp_diagnostics_1.interpretationForClassification)(classification),
532
+ agentDebugSteps: (0, fraim_mcp_diagnostics_1.buildAgentDebugSteps)(classification),
533
+ logPath,
534
+ keySource: keyDiagnostic.source,
535
+ keyFingerprint: keyDiagnostic.fingerprint
536
+ }
537
+ };
538
+ }
539
+ const logTail = (0, fraim_mcp_diagnostics_1.readFileTailUtf8)(logPath);
540
+ const analysis = (0, fraim_mcp_diagnostics_1.analyzeFraimProxyLog)(logTail.content, {
541
+ expectedKey: keyDiagnostic.apiKey,
542
+ maxLines: 300
543
+ });
544
+ const errorClassifications = [
545
+ 'message-processing-failed',
546
+ 'remote-request-failed'
547
+ ];
548
+ const passedClassifications = [
549
+ 'local-proxy-forwarded-to-remote'
550
+ ];
551
+ const status = errorClassifications.includes(analysis.classification)
552
+ ? 'error'
553
+ : passedClassifications.includes(analysis.classification)
554
+ ? 'passed'
555
+ : 'warning';
556
+ return {
557
+ status,
558
+ message: `FRAIM local proxy diagnostic: ${analysis.interpretation}`,
559
+ suggestion: status === 'passed' ? undefined : analysis.agentDebugSteps[0],
560
+ details: {
561
+ diagnosticVersion: 'fraim-mcp-startup-v1',
562
+ logPath,
563
+ logBytesRead: logTail.bytesRead,
564
+ logFileSize: logTail.fileSize,
565
+ keySource: keyDiagnostic.source,
566
+ keyFingerprint: keyDiagnostic.fingerprint,
567
+ ...analysis
568
+ }
569
+ };
570
+ }
435
571
  // ============================================================
436
572
  // Issue #532: Stdio MCP Runtime Connectivity Check
437
573
  // ============================================================
@@ -447,6 +583,8 @@ const STDIO_HANDSHAKE_TIMEOUT_MS = 15000;
447
583
  async function spawnAndHandshake(command, args, timeoutMs = STDIO_HANDSHAKE_TIMEOUT_MS) {
448
584
  return new Promise((resolve) => {
449
585
  const startTime = Date.now();
586
+ const keyDiagnostic = resolveFraimApiKeyForDiagnostics();
587
+ const knownSecrets = keyDiagnostic.apiKey ? [keyDiagnostic.apiKey] : [];
450
588
  let spawnCommand;
451
589
  let spawnArgs;
452
590
  // spawnOptions is typed loosely so we can set windowsVerbatimArguments below.
@@ -492,14 +630,34 @@ async function spawnAndHandshake(command, args, timeoutMs = STDIO_HANDSHAKE_TIME
492
630
  });
493
631
  return;
494
632
  }
633
+ let settled = false;
634
+ let initializeResponse = null;
635
+ let toolsListResponse = null;
636
+ const settle = (result, killChild = false) => {
637
+ if (settled)
638
+ return;
639
+ settled = true;
640
+ clearTimeout(timer);
641
+ if (killChild) {
642
+ try {
643
+ child.stdin?.end();
644
+ }
645
+ catch { /* best effort */ }
646
+ try {
647
+ child.kill('SIGTERM');
648
+ }
649
+ catch { /* best effort */ }
650
+ }
651
+ resolve(result);
652
+ };
495
653
  const timer = setTimeout(() => {
496
654
  try {
497
655
  child.kill('SIGKILL');
498
656
  }
499
657
  catch { /* best effort */ }
500
- resolve({
658
+ settle({
501
659
  status: 'error',
502
- phase: 'initialize',
660
+ phase: initializeResponse ? 'tools-list' : 'initialize',
503
661
  message: `MCP server did not respond within ${timeoutMs}ms`,
504
662
  suggestion: 'The server may be slow to start. Check that the package is installed and npx cache is warm.',
505
663
  details: { command, args, timeoutMs, elapsed: Date.now() - startTime }
@@ -507,46 +665,89 @@ async function spawnAndHandshake(command, args, timeoutMs = STDIO_HANDSHAKE_TIME
507
665
  }, timeoutMs);
508
666
  let stderrBuffer = '';
509
667
  let stdoutBuffer = '';
668
+ let stdoutLineBuffer = '';
510
669
  let spawnError = null;
511
670
  child.stderr?.on('data', (d) => {
512
- stderrBuffer += d.toString();
671
+ stderrBuffer = (0, fraim_mcp_diagnostics_1.appendLimited)(stderrBuffer, d.toString());
513
672
  });
514
673
  child.on('error', (err) => {
515
674
  spawnError = err;
516
675
  });
676
+ const handleStdoutLine = (line) => {
677
+ const trimmed = line.trim();
678
+ if (!trimmed)
679
+ return;
680
+ try {
681
+ const msg = JSON.parse(trimmed);
682
+ if (msg?.id === 1 && msg?.result !== undefined) {
683
+ initializeResponse = msg;
684
+ }
685
+ if (msg?.id === 2 && msg?.result !== undefined) {
686
+ toolsListResponse = msg;
687
+ }
688
+ if (initializeResponse && toolsListResponse) {
689
+ const toolCount = Array.isArray(toolsListResponse.result?.tools)
690
+ ? toolsListResponse.result.tools.length
691
+ : 0;
692
+ const elapsed = Date.now() - startTime;
693
+ settle({
694
+ status: 'passed',
695
+ message: `MCP server responded with ${toolCount} tool(s) in ${elapsed}ms`,
696
+ details: { command, args, toolCount, elapsed }
697
+ }, true);
698
+ }
699
+ }
700
+ catch {
701
+ // Ignore non-JSON lines (e.g., startup log output)
702
+ }
703
+ };
517
704
  child.stdout?.on('data', (d) => {
518
- stdoutBuffer += d.toString();
705
+ const chunk = d.toString();
706
+ stdoutBuffer = (0, fraim_mcp_diagnostics_1.appendLimited)(stdoutBuffer, chunk);
707
+ stdoutLineBuffer += chunk;
708
+ let newlineIndex;
709
+ while ((newlineIndex = stdoutLineBuffer.indexOf('\n')) !== -1) {
710
+ const line = stdoutLineBuffer.slice(0, newlineIndex);
711
+ stdoutLineBuffer = stdoutLineBuffer.slice(newlineIndex + 1);
712
+ handleStdoutLine(line);
713
+ }
519
714
  });
520
715
  child.on('close', (code) => {
716
+ if (settled)
717
+ return;
521
718
  clearTimeout(timer);
522
719
  if (spawnError) {
523
720
  const msg = spawnError.message || '';
524
721
  const isEnoent = msg.includes('ENOENT') || msg.includes('not found');
525
- resolve({
722
+ settle({
526
723
  status: 'error',
527
724
  phase: 'spawn',
528
725
  message: `Failed to start MCP server: ${msg}`,
529
726
  suggestion: isEnoent
530
727
  ? 'Verify that npx is installed. Run: npm install -g npm to update npm/npx.'
531
728
  : 'Check that the required package can be installed via npx.',
532
- details: { command, args, error: msg }
729
+ details: { command, args, error: (0, fraim_mcp_diagnostics_1.sanitizeDiagnosticText)(msg, knownSecrets) }
533
730
  });
534
731
  return;
535
732
  }
536
733
  if (code !== 0 && stdoutBuffer.trim() === '') {
537
- resolve({
734
+ settle({
538
735
  status: 'error',
539
736
  phase: 'spawn',
540
737
  message: `MCP server exited with code ${code} before responding`,
541
738
  suggestion: 'Run: npx -y <package> manually to check for installation errors.',
542
- details: { command, args, exitCode: code, stderr: stderrBuffer.slice(0, 500) }
739
+ details: { command, args, exitCode: code, stderr: (0, fraim_mcp_diagnostics_1.sanitizeDiagnosticText)(stderrBuffer.slice(-1000), knownSecrets) }
543
740
  });
544
741
  return;
545
742
  }
546
- // Parse all JSON-RPC messages received from stdout
743
+ if (stdoutLineBuffer.trim()) {
744
+ handleStdoutLine(stdoutLineBuffer);
745
+ }
746
+ if (settled)
747
+ return;
748
+ // Parse all JSON-RPC messages received from stdout as a fallback for servers
749
+ // that closed without a trailing newline.
547
750
  const lines = stdoutBuffer.split('\n').map((l) => l.trim()).filter(Boolean);
548
- let initializeResponse = null;
549
- let toolsListResponse = null;
550
751
  for (const line of lines) {
551
752
  try {
552
753
  const msg = JSON.parse(line);
@@ -562,17 +763,17 @@ async function spawnAndHandshake(command, args, timeoutMs = STDIO_HANDSHAKE_TIME
562
763
  }
563
764
  }
564
765
  if (!initializeResponse) {
565
- resolve({
766
+ settle({
566
767
  status: 'error',
567
768
  phase: 'initialize',
568
769
  message: 'MCP server did not return a valid initialize response',
569
770
  suggestion: 'The server may have crashed during startup. Check stderr for error details.',
570
- details: { command, args, exitCode: code, stderr: stderrBuffer.slice(0, 500) }
771
+ details: { command, args, exitCode: code, stderr: (0, fraim_mcp_diagnostics_1.sanitizeDiagnosticText)(stderrBuffer.slice(-1000), knownSecrets) }
571
772
  });
572
773
  return;
573
774
  }
574
775
  if (!toolsListResponse) {
575
- resolve({
776
+ settle({
576
777
  status: 'error',
577
778
  phase: 'tools-list',
578
779
  message: 'MCP server did not respond to tools/list',
@@ -585,7 +786,7 @@ async function spawnAndHandshake(command, args, timeoutMs = STDIO_HANDSHAKE_TIME
585
786
  ? toolsListResponse.result.tools.length
586
787
  : 0;
587
788
  const elapsed = Date.now() - startTime;
588
- resolve({
789
+ settle({
589
790
  status: 'passed',
590
791
  message: `MCP server responded with ${toolCount} tool(s) in ${elapsed}ms`,
591
792
  details: { command, args, toolCount, elapsed }
@@ -611,7 +812,6 @@ async function spawnAndHandshake(command, args, timeoutMs = STDIO_HANDSHAKE_TIME
611
812
  params: {}
612
813
  }) + '\n';
613
814
  child.stdin?.write(toolsMsg);
614
- child.stdin?.end();
615
815
  }
616
816
  catch {
617
817
  // stdin may not be writable if the process exited immediately
@@ -686,6 +886,12 @@ function getMCPConnectivityChecks() {
686
886
  critical: true,
687
887
  run: testFraimConnectivity
688
888
  });
889
+ checks.push({
890
+ name: 'FRAIM local proxy troubleshooting',
891
+ category: 'mcpConnectivity',
892
+ critical: false,
893
+ run: testFraimLocalProxyTroubleshooting
894
+ });
689
895
  // Check 2: Validate each installed IDE's MCP config
690
896
  const installedIDEs = (0, ide_detector_1.detectInstalledIDEs)();
691
897
  for (const ide of installedIDEs) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fraim-framework",
3
- "version": "2.0.197",
3
+ "version": "2.0.198",
4
4
  "description": "FRAIM: AI Workforce Infrastructure — the organizational capability that turns AI agents into an accountable workforce, their operators into capable AI managers, and executives into leaders with clear optics on AI proficiency.",
5
5
  "main": "index.js",
6
6
  "bin": {