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 =
|
|
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 ||
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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": {
|