fraim-framework 2.0.126 → 2.0.127
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/dist/src/ai-hub/catalog.js +280 -44
- package/dist/src/ai-hub/desktop-main.js +2 -2
- package/dist/src/ai-hub/hosts.js +384 -10
- package/dist/src/ai-hub/server.js +255 -9
- package/dist/src/cli/commands/add-ide.js +4 -3
- package/dist/src/cli/commands/first-run.js +61 -0
- package/dist/src/cli/commands/hub.js +4 -4
- package/dist/src/cli/commands/init-project.js +4 -4
- package/dist/src/cli/commands/setup.js +4 -3
- package/dist/src/cli/commands/sync.js +21 -2
- package/dist/src/cli/doctor/checks/ide-config-checks.js +20 -2
- package/dist/src/cli/fraim.js +2 -0
- package/dist/src/cli/mcp/ide-formats.js +29 -1
- package/dist/src/cli/mcp/mcp-server-registry.js +1 -0
- package/dist/src/cli/setup/auto-mcp-setup.js +14 -8
- package/dist/src/cli/setup/ide-detector.js +32 -1
- package/dist/src/cli/setup/ide-global-integration.js +5 -1
- package/dist/src/cli/setup/ide-invocation-surfaces.js +14 -0
- package/dist/src/cli/setup/mcp-config-generator.js +12 -1
- package/dist/src/cli/utils/agent-adapters.js +10 -0
- package/dist/src/core/utils/git-utils.js +14 -6
- package/dist/src/first-run/install-state.js +68 -0
- package/dist/src/first-run/server.js +153 -0
- package/dist/src/first-run/session-service.js +302 -0
- package/dist/src/first-run/types.js +40 -0
- package/dist/src/local-mcp-server/otlp-metrics-receiver.js +7 -1
- package/dist/src/local-mcp-server/stdio-server.js +41 -9
- package/package.json +3 -1
- package/public/ai-hub/index.html +149 -102
- package/public/ai-hub/script.js +1154 -271
- package/public/ai-hub/styles.css +753 -450
- package/public/first-run/index.html +221 -0
- package/public/first-run/script.js +361 -0
|
@@ -0,0 +1,302 @@
|
|
|
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.FirstRunSessionService = void 0;
|
|
7
|
+
const fs_1 = __importDefault(require("fs"));
|
|
8
|
+
const os_1 = __importDefault(require("os"));
|
|
9
|
+
const path_1 = __importDefault(require("path"));
|
|
10
|
+
const crypto_1 = __importDefault(require("crypto"));
|
|
11
|
+
const child_process_1 = require("child_process");
|
|
12
|
+
const ide_detector_1 = require("../cli/setup/ide-detector");
|
|
13
|
+
const auto_mcp_setup_1 = require("../cli/setup/auto-mcp-setup");
|
|
14
|
+
const setup_1 = require("../cli/commands/setup");
|
|
15
|
+
const init_project_1 = require("../cli/commands/init-project");
|
|
16
|
+
const script_sync_utils_1 = require("../cli/utils/script-sync-utils");
|
|
17
|
+
const types_1 = require("./types");
|
|
18
|
+
const install_state_1 = require("./install-state");
|
|
19
|
+
function commandExists(command) {
|
|
20
|
+
const executable = process.platform === 'win32' ? 'cmd.exe' : command;
|
|
21
|
+
const args = process.platform === 'win32'
|
|
22
|
+
? ['/d', '/s', '/c', `${command} --version`]
|
|
23
|
+
: ['--version'];
|
|
24
|
+
const result = (0, child_process_1.spawnSync)(executable, args, {
|
|
25
|
+
encoding: 'utf8',
|
|
26
|
+
stdio: ['ignore', 'ignore', 'ignore'],
|
|
27
|
+
shell: false,
|
|
28
|
+
});
|
|
29
|
+
return result.status === 0;
|
|
30
|
+
}
|
|
31
|
+
function ensureOutputDirs() {
|
|
32
|
+
fs_1.default.mkdirSync((0, script_sync_utils_1.getUserFraimDir)(), { recursive: true });
|
|
33
|
+
fs_1.default.mkdirSync(path_1.default.join((0, script_sync_utils_1.getUserFraimDir)(), 'last-install'), { recursive: true });
|
|
34
|
+
}
|
|
35
|
+
function getNextPromptPath() {
|
|
36
|
+
return path_1.default.join((0, script_sync_utils_1.getUserFraimDir)(), 'last-install', 'next-prompt.txt');
|
|
37
|
+
}
|
|
38
|
+
function getInstallLogPath() {
|
|
39
|
+
return path_1.default.join((0, script_sync_utils_1.getUserFraimDir)(), 'install-log.txt');
|
|
40
|
+
}
|
|
41
|
+
function appendInstallLog(line) {
|
|
42
|
+
ensureOutputDirs();
|
|
43
|
+
fs_1.default.appendFileSync(getInstallLogPath(), `[${new Date().toISOString()}] ${line}${os_1.default.EOL}`);
|
|
44
|
+
}
|
|
45
|
+
function getLaunchInstruction(agentId, projectRoot) {
|
|
46
|
+
const cwd = projectRoot || process.cwd();
|
|
47
|
+
switch (agentId) {
|
|
48
|
+
case 'claude-code':
|
|
49
|
+
return `cd "${cwd}" && claude`;
|
|
50
|
+
case 'codex':
|
|
51
|
+
return `cd "${cwd}" && codex`;
|
|
52
|
+
case 'gemini-cli':
|
|
53
|
+
return `cd "${cwd}" && gemini`;
|
|
54
|
+
default:
|
|
55
|
+
return `cd "${cwd}"`;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
function maybeLaunchInteractiveShell(agentId, projectRoot) {
|
|
59
|
+
const commandLine = getLaunchInstruction(agentId, projectRoot);
|
|
60
|
+
try {
|
|
61
|
+
if (process.platform === 'win32') {
|
|
62
|
+
(0, child_process_1.spawn)('cmd.exe', ['/d', '/s', '/c', `start "" cmd /k ${commandLine}`], {
|
|
63
|
+
detached: true,
|
|
64
|
+
stdio: 'ignore',
|
|
65
|
+
}).unref();
|
|
66
|
+
return true;
|
|
67
|
+
}
|
|
68
|
+
if (process.platform === 'darwin') {
|
|
69
|
+
(0, child_process_1.spawn)('open', ['-a', 'Terminal', projectRoot], {
|
|
70
|
+
detached: true,
|
|
71
|
+
stdio: 'ignore',
|
|
72
|
+
}).unref();
|
|
73
|
+
return true;
|
|
74
|
+
}
|
|
75
|
+
(0, child_process_1.spawn)('x-terminal-emulator', ['-e', 'bash', '-lc', commandLine], {
|
|
76
|
+
detached: true,
|
|
77
|
+
stdio: 'ignore',
|
|
78
|
+
}).unref();
|
|
79
|
+
return true;
|
|
80
|
+
}
|
|
81
|
+
catch {
|
|
82
|
+
return false;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
function probeCodex(projectRoot) {
|
|
86
|
+
const result = (0, child_process_1.spawnSync)('codex', ['exec', '--json', '--skip-git-repo-check', '--dangerously-bypass-approvals-and-sandbox'], {
|
|
87
|
+
cwd: projectRoot,
|
|
88
|
+
input: 'Reply exactly "FRAIM_READY" if FRAIM is available in this workspace. Otherwise reply exactly "NOT_READY".',
|
|
89
|
+
encoding: 'utf8',
|
|
90
|
+
timeout: 60000,
|
|
91
|
+
});
|
|
92
|
+
const output = `${result.stdout || ''}${result.stderr || ''}`.trim();
|
|
93
|
+
return { ok: result.status === 0 && output.toUpperCase().includes('FRAIM'), output };
|
|
94
|
+
}
|
|
95
|
+
function probeClaude(projectRoot) {
|
|
96
|
+
const result = (0, child_process_1.spawnSync)('claude', ['-p', '--verbose', '--output-format', 'stream-json', '--dangerously-skip-permissions'], {
|
|
97
|
+
cwd: projectRoot,
|
|
98
|
+
input: 'Reply exactly "FRAIM_READY" if FRAIM is available in this workspace. Otherwise reply exactly "NOT_READY".',
|
|
99
|
+
encoding: 'utf8',
|
|
100
|
+
timeout: 60000,
|
|
101
|
+
});
|
|
102
|
+
const output = `${result.stdout || ''}${result.stderr || ''}`.trim();
|
|
103
|
+
return { ok: result.status === 0 && output.toUpperCase().includes('FRAIM'), output };
|
|
104
|
+
}
|
|
105
|
+
function probeGemini(projectRoot) {
|
|
106
|
+
const result = (0, child_process_1.spawnSync)('gemini', ['--help'], {
|
|
107
|
+
cwd: projectRoot,
|
|
108
|
+
encoding: 'utf8',
|
|
109
|
+
timeout: 30000,
|
|
110
|
+
});
|
|
111
|
+
const output = `${result.stdout || ''}${result.stderr || ''}`.trim();
|
|
112
|
+
return {
|
|
113
|
+
ok: result.status === 0,
|
|
114
|
+
output: output || 'Gemini CLI responded to --help; runtime FRAIM probe still requires manual confirmation.',
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
class FirstRunSessionService {
|
|
118
|
+
constructor(options) {
|
|
119
|
+
this.key = options.key;
|
|
120
|
+
this.headless = options.headless === true;
|
|
121
|
+
this.fakeMode = process.env.FRAIM_FIRST_RUN_FAKE === '1';
|
|
122
|
+
this.requestToken = crypto_1.default.randomUUID();
|
|
123
|
+
this.state = options.resume ? (0, install_state_1.loadFirstRunState)() || (0, install_state_1.createInitialFirstRunState)(options.key) : (0, install_state_1.createInitialFirstRunState)(options.key);
|
|
124
|
+
if (options.projectRoot) {
|
|
125
|
+
this.state.workspacePath = path_1.default.resolve(options.projectRoot);
|
|
126
|
+
this.state.stepStates.project = 'running';
|
|
127
|
+
(0, install_state_1.saveFirstRunState)(this.state);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
persist() {
|
|
131
|
+
(0, install_state_1.saveFirstRunState)(this.state);
|
|
132
|
+
}
|
|
133
|
+
markStep(step, status) {
|
|
134
|
+
this.state.stepStates[step] = status;
|
|
135
|
+
this.persist();
|
|
136
|
+
}
|
|
137
|
+
detectAgents() {
|
|
138
|
+
if (this.fakeMode) {
|
|
139
|
+
return types_1.FIRST_RUN_AGENT_OPTIONS.map((agent) => ({
|
|
140
|
+
id: agent.id,
|
|
141
|
+
label: agent.label,
|
|
142
|
+
detected: true,
|
|
143
|
+
detail: 'Fake test-mode agent available.',
|
|
144
|
+
}));
|
|
145
|
+
}
|
|
146
|
+
const detected = (0, ide_detector_1.detectInstalledIDEs)();
|
|
147
|
+
return types_1.FIRST_RUN_AGENT_OPTIONS.map((agent) => {
|
|
148
|
+
const ide = detected.find((entry) => entry.aliases?.some((alias) => agent.detectAliases.includes(alias)) ||
|
|
149
|
+
agent.detectAliases.some((alias) => entry.name.toLowerCase().includes(alias)));
|
|
150
|
+
return {
|
|
151
|
+
id: agent.id,
|
|
152
|
+
label: agent.label,
|
|
153
|
+
detected: Boolean(ide) || commandExists(agent.launchCommand),
|
|
154
|
+
detail: ide ? `Config path: ${ide.configPath}` : 'CLI/config not detected yet.',
|
|
155
|
+
};
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
getRequestToken() {
|
|
159
|
+
return this.requestToken;
|
|
160
|
+
}
|
|
161
|
+
getSession() {
|
|
162
|
+
const agents = this.detectAgents();
|
|
163
|
+
this.state.detectedAgents = agents.filter((agent) => agent.detected).map((agent) => agent.id);
|
|
164
|
+
this.persist();
|
|
165
|
+
return {
|
|
166
|
+
state: this.state,
|
|
167
|
+
agents,
|
|
168
|
+
prompt: types_1.FIRST_RUN_PROMPT,
|
|
169
|
+
headless: this.headless,
|
|
170
|
+
requestToken: this.requestToken,
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
runPrereqChecks() {
|
|
174
|
+
this.markStep('prereqs', 'running');
|
|
175
|
+
const checks = [
|
|
176
|
+
{ label: 'node', ok: commandExists('node') },
|
|
177
|
+
{ label: 'npx', ok: commandExists('npx') },
|
|
178
|
+
{ label: 'git', ok: commandExists('git') },
|
|
179
|
+
];
|
|
180
|
+
const failures = checks.filter((check) => !check.ok).map((check) => check.label);
|
|
181
|
+
const message = failures.length === 0
|
|
182
|
+
? 'Node.js, npx, and git are available.'
|
|
183
|
+
: `Missing prerequisites: ${failures.join(', ')}.`;
|
|
184
|
+
appendInstallLog(`prereq-check ${message}`);
|
|
185
|
+
this.markStep('prereqs', failures.length === 0 ? 'complete' : 'failed');
|
|
186
|
+
if (failures.length === 0) {
|
|
187
|
+
this.markStep('agent', 'running');
|
|
188
|
+
}
|
|
189
|
+
return { ok: failures.length === 0, message, state: this.state };
|
|
190
|
+
}
|
|
191
|
+
selectAgent(agentId) {
|
|
192
|
+
this.state.selectedAgentId = agentId;
|
|
193
|
+
this.markStep('agent', 'complete');
|
|
194
|
+
this.markStep('configure', 'running');
|
|
195
|
+
appendInstallLog(`agent-selected ${agentId}`);
|
|
196
|
+
return {
|
|
197
|
+
ok: true,
|
|
198
|
+
message: `Selected ${types_1.FIRST_RUN_AGENT_OPTIONS.find((agent) => agent.id === agentId)?.label || agentId}.`,
|
|
199
|
+
state: this.state,
|
|
200
|
+
launchCommand: types_1.FIRST_RUN_AGENT_OPTIONS.find((agent) => agent.id === agentId)?.loginCommand,
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
async configureFraim() {
|
|
204
|
+
this.markStep('configure', 'running');
|
|
205
|
+
(0, setup_1.saveGlobalConfig)(this.key, 'conversational', {}, {});
|
|
206
|
+
if (!this.fakeMode) {
|
|
207
|
+
const selectedAgent = this.state.selectedAgentId ? (0, ide_detector_1.findIDEByName)(this.state.selectedAgentId) : undefined;
|
|
208
|
+
const targetNames = selectedAgent ? [selectedAgent.name] : undefined;
|
|
209
|
+
await (0, auto_mcp_setup_1.autoConfigureMCP)(this.key, {}, targetNames, {});
|
|
210
|
+
this.state.configuredAgents = this.detectAgents().filter((agent) => agent.detected).map((agent) => agent.id);
|
|
211
|
+
}
|
|
212
|
+
else {
|
|
213
|
+
this.state.configuredAgents = this.state.selectedAgentId ? [this.state.selectedAgentId] : [];
|
|
214
|
+
}
|
|
215
|
+
appendInstallLog(`configure configuredAgents=${this.state.configuredAgents.join(',')}`);
|
|
216
|
+
this.markStep('configure', 'complete');
|
|
217
|
+
this.markStep('project', 'running');
|
|
218
|
+
return {
|
|
219
|
+
ok: true,
|
|
220
|
+
message: 'Saved FRAIM global configuration and wrote agent config where available.',
|
|
221
|
+
state: this.state,
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
async initializeProject(projectPath, initializeGit = true) {
|
|
225
|
+
const resolvedPath = path_1.default.resolve(projectPath);
|
|
226
|
+
fs_1.default.mkdirSync(resolvedPath, { recursive: true });
|
|
227
|
+
this.markStep('project', 'running');
|
|
228
|
+
if (initializeGit && commandExists('git')) {
|
|
229
|
+
try {
|
|
230
|
+
(0, child_process_1.execSync)('git rev-parse --git-dir', { cwd: resolvedPath, stdio: 'ignore' });
|
|
231
|
+
}
|
|
232
|
+
catch {
|
|
233
|
+
(0, child_process_1.execSync)('git init', { cwd: resolvedPath, stdio: 'ignore' });
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
await (0, init_project_1.runInitProject)({ projectRoot: resolvedPath });
|
|
237
|
+
this.state.workspacePath = resolvedPath;
|
|
238
|
+
this.markStep('project', 'complete');
|
|
239
|
+
this.markStep('launch', 'running');
|
|
240
|
+
appendInstallLog(`project-initialized ${resolvedPath}`);
|
|
241
|
+
return {
|
|
242
|
+
ok: true,
|
|
243
|
+
message: `Initialized FRAIM in ${resolvedPath}.`,
|
|
244
|
+
state: this.state,
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
launchAndProbe() {
|
|
248
|
+
const agentId = this.state.selectedAgentId;
|
|
249
|
+
const projectRoot = this.state.workspacePath;
|
|
250
|
+
if (!agentId || !projectRoot) {
|
|
251
|
+
this.markStep('launch', 'failed');
|
|
252
|
+
return {
|
|
253
|
+
ok: false,
|
|
254
|
+
message: 'Select an agent and project folder before launch.',
|
|
255
|
+
state: this.state,
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
const launchCommand = getLaunchInstruction(agentId, projectRoot);
|
|
259
|
+
const opened = this.fakeMode ? true : maybeLaunchInteractiveShell(agentId, projectRoot);
|
|
260
|
+
let probeResult = { ok: true, output: 'Fake test-mode launch succeeded.' };
|
|
261
|
+
if (!this.fakeMode) {
|
|
262
|
+
if (agentId === 'codex') {
|
|
263
|
+
probeResult = probeCodex(projectRoot);
|
|
264
|
+
}
|
|
265
|
+
else if (agentId === 'claude-code') {
|
|
266
|
+
probeResult = probeClaude(projectRoot);
|
|
267
|
+
}
|
|
268
|
+
else {
|
|
269
|
+
probeResult = probeGemini(projectRoot);
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
this.state.lastLaunchCommand = launchCommand;
|
|
273
|
+
this.state.lastProbeMessage = probeResult.output;
|
|
274
|
+
this.markStep('launch', probeResult.ok ? 'complete' : 'failed');
|
|
275
|
+
if (probeResult.ok) {
|
|
276
|
+
this.markStep('finish', 'running');
|
|
277
|
+
}
|
|
278
|
+
appendInstallLog(`launch agent=${agentId} opened=${opened} probeOk=${probeResult.ok}`);
|
|
279
|
+
return {
|
|
280
|
+
ok: probeResult.ok,
|
|
281
|
+
message: opened
|
|
282
|
+
? 'Agent launch was attempted and the probe completed.'
|
|
283
|
+
: 'Agent launch could not be opened automatically; use the provided command and review the probe output.',
|
|
284
|
+
state: this.state,
|
|
285
|
+
launchCommand,
|
|
286
|
+
output: probeResult.output,
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
finish() {
|
|
290
|
+
ensureOutputDirs();
|
|
291
|
+
fs_1.default.writeFileSync(getNextPromptPath(), `${types_1.FIRST_RUN_PROMPT}${os_1.default.EOL}`, 'utf8');
|
|
292
|
+
this.state.nextPrompt = types_1.FIRST_RUN_PROMPT;
|
|
293
|
+
this.markStep('finish', 'complete');
|
|
294
|
+
appendInstallLog('finish prompt-written');
|
|
295
|
+
return {
|
|
296
|
+
ok: true,
|
|
297
|
+
message: 'Wrote the next prompt artifact and completed first-run.',
|
|
298
|
+
state: this.state,
|
|
299
|
+
};
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
exports.FirstRunSessionService = FirstRunSessionService;
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.FIRST_RUN_AGENT_OPTIONS = exports.FIRST_RUN_RESOURCES_URL = exports.FIRST_RUN_PROMPT = void 0;
|
|
4
|
+
exports.createDefaultStepStates = createDefaultStepStates;
|
|
5
|
+
exports.FIRST_RUN_PROMPT = 'Onboard this project';
|
|
6
|
+
exports.FIRST_RUN_RESOURCES_URL = 'https://fraimworks.ai/resources.html';
|
|
7
|
+
exports.FIRST_RUN_AGENT_OPTIONS = [
|
|
8
|
+
{
|
|
9
|
+
id: 'claude-code',
|
|
10
|
+
label: 'Claude Code',
|
|
11
|
+
detectAliases: ['claude-code', 'claude code', 'claude'],
|
|
12
|
+
loginCommand: 'claude',
|
|
13
|
+
launchCommand: 'claude',
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
id: 'codex',
|
|
17
|
+
label: 'Codex',
|
|
18
|
+
detectAliases: ['codex'],
|
|
19
|
+
loginCommand: 'codex login',
|
|
20
|
+
launchCommand: 'codex',
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
id: 'gemini-cli',
|
|
24
|
+
label: 'Gemini CLI',
|
|
25
|
+
detectAliases: ['gemini-cli', 'gemini cli', 'gemini'],
|
|
26
|
+
loginCommand: 'gemini',
|
|
27
|
+
launchCommand: 'gemini',
|
|
28
|
+
},
|
|
29
|
+
];
|
|
30
|
+
function createDefaultStepStates() {
|
|
31
|
+
return {
|
|
32
|
+
welcome: 'pending',
|
|
33
|
+
prereqs: 'pending',
|
|
34
|
+
agent: 'pending',
|
|
35
|
+
configure: 'pending',
|
|
36
|
+
project: 'pending',
|
|
37
|
+
launch: 'pending',
|
|
38
|
+
finish: 'pending',
|
|
39
|
+
};
|
|
40
|
+
}
|
|
@@ -257,6 +257,12 @@ function startOtlpReceiver(log) {
|
|
|
257
257
|
* Stop the OTLP receiver and clear stored snapshots.
|
|
258
258
|
*/
|
|
259
259
|
function stopOtlpReceiver(server) {
|
|
260
|
-
|
|
260
|
+
try {
|
|
261
|
+
server.close();
|
|
262
|
+
}
|
|
263
|
+
catch {
|
|
264
|
+
// The receiver may have failed to bind because another FRAIM proxy owns
|
|
265
|
+
// the port. In that case there is no local listener to close.
|
|
266
|
+
}
|
|
261
267
|
snapshots.clear();
|
|
262
268
|
}
|
|
@@ -402,6 +402,7 @@ class FraimLocalMCPServer {
|
|
|
402
402
|
this.repoInfo = null;
|
|
403
403
|
this.engine = null;
|
|
404
404
|
this.otlpServer = null;
|
|
405
|
+
this.isShutdown = false;
|
|
405
406
|
this.writer = writer || process.stdout.write.bind(process.stdout);
|
|
406
407
|
this.remoteUrl = process.env.FRAIM_REMOTE_URL || 'https://fraim.wellnessatwork.me';
|
|
407
408
|
this.apiKey = this.loadApiKey();
|
|
@@ -422,6 +423,16 @@ class FraimLocalMCPServer {
|
|
|
422
423
|
// Start OTLP metrics receiver for Claude Code token telemetry
|
|
423
424
|
this.otlpServer = (0, otlp_metrics_receiver_js_1.startOtlpReceiver)((msg) => this.log(`📊 ${msg}`));
|
|
424
425
|
}
|
|
426
|
+
shutdown() {
|
|
427
|
+
if (this.isShutdown)
|
|
428
|
+
return;
|
|
429
|
+
this.isShutdown = true;
|
|
430
|
+
this.usageCollector.shutdown();
|
|
431
|
+
if (this.otlpServer?.server) {
|
|
432
|
+
(0, otlp_metrics_receiver_js_1.stopOtlpReceiver)(this.otlpServer.server);
|
|
433
|
+
this.otlpServer = null;
|
|
434
|
+
}
|
|
435
|
+
}
|
|
425
436
|
/**
|
|
426
437
|
* Load API key from environment variable or user config file
|
|
427
438
|
* Priority: FRAIM_API_KEY env var > ~/.fraim/config.json
|
|
@@ -1436,6 +1447,33 @@ class FraimLocalMCPServer {
|
|
|
1436
1447
|
projectPath: `${issueTracking.namespace}/${issueTracking.name}`
|
|
1437
1448
|
};
|
|
1438
1449
|
}
|
|
1450
|
+
hasRemoteRepoLocator(value) {
|
|
1451
|
+
if (typeof value !== 'string')
|
|
1452
|
+
return false;
|
|
1453
|
+
return /^[a-z]+:\/\//i.test(value) || /^[^@\s]+@[^:\s]+:/i.test(value);
|
|
1454
|
+
}
|
|
1455
|
+
mergeRepoContexts(agentRepo, detectedRepo) {
|
|
1456
|
+
const normalizedAgent = this.normalizeRepoContext(agentRepo);
|
|
1457
|
+
const normalizedDetected = this.normalizeRepoContext(detectedRepo);
|
|
1458
|
+
if (!normalizedAgent)
|
|
1459
|
+
return normalizedDetected;
|
|
1460
|
+
if (!normalizedDetected)
|
|
1461
|
+
return normalizedAgent;
|
|
1462
|
+
const agentHasRemoteUrl = this.hasRemoteRepoLocator(normalizedAgent.url);
|
|
1463
|
+
const detectedHasRemoteUrl = this.hasRemoteRepoLocator(normalizedDetected.url);
|
|
1464
|
+
// Preserve an explicit agent-supplied remote repository when local auto-detection
|
|
1465
|
+
// only found a workspace label or other non-remote fallback.
|
|
1466
|
+
if (agentHasRemoteUrl && !detectedHasRemoteUrl) {
|
|
1467
|
+
return this.normalizeRepoContext({
|
|
1468
|
+
...normalizedDetected,
|
|
1469
|
+
...normalizedAgent
|
|
1470
|
+
});
|
|
1471
|
+
}
|
|
1472
|
+
return this.normalizeRepoContext({
|
|
1473
|
+
...normalizedAgent,
|
|
1474
|
+
...normalizedDetected
|
|
1475
|
+
});
|
|
1476
|
+
}
|
|
1439
1477
|
/**
|
|
1440
1478
|
* Internal method to perform the actual proxy request to the remote server.
|
|
1441
1479
|
* This method does NOT inject raw: true, as it is used for both top-level
|
|
@@ -1457,11 +1495,7 @@ class FraimLocalMCPServer {
|
|
|
1457
1495
|
// REQUIRED: Auto-detect and inject repo info
|
|
1458
1496
|
const detectedRepo = this.detectRepoInfo();
|
|
1459
1497
|
if (detectedRepo) {
|
|
1460
|
-
args.repo =
|
|
1461
|
-
...args.repo, // Agent values as fallback
|
|
1462
|
-
...detectedRepo // Detected values override (always win)
|
|
1463
|
-
};
|
|
1464
|
-
args.repo = this.normalizeRepoContext(args.repo);
|
|
1498
|
+
args.repo = this.mergeRepoContexts(args.repo, detectedRepo);
|
|
1465
1499
|
const repoLabel = args.repo.owner ? `${args.repo.owner}/${args.repo.name}` : args.repo.name;
|
|
1466
1500
|
this.log(`[req:${requestId}] Auto-detected and injected repo info: ${repoLabel}`);
|
|
1467
1501
|
}
|
|
@@ -1894,13 +1928,11 @@ class FraimLocalMCPServer {
|
|
|
1894
1928
|
this.log(`⚠️ Failed to upload usage data: ${error.message}`);
|
|
1895
1929
|
});
|
|
1896
1930
|
}, 60000); // Upload every minute
|
|
1931
|
+
uploadInterval.unref?.();
|
|
1897
1932
|
// Clean up interval on shutdown
|
|
1898
1933
|
const cleanup = () => {
|
|
1899
1934
|
clearInterval(uploadInterval);
|
|
1900
|
-
this.
|
|
1901
|
-
if (this.otlpServer?.server) {
|
|
1902
|
-
(0, otlp_metrics_receiver_js_1.stopOtlpReceiver)(this.otlpServer.server);
|
|
1903
|
-
}
|
|
1935
|
+
this.shutdown();
|
|
1904
1936
|
};
|
|
1905
1937
|
process.stdin.on('data', async (chunk) => {
|
|
1906
1938
|
buffer += chunk;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "fraim-framework",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.127",
|
|
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": {
|
|
@@ -114,10 +114,12 @@
|
|
|
114
114
|
"dist/src/local-mcp-server/",
|
|
115
115
|
"dist/src/cli/",
|
|
116
116
|
"dist/src/ai-hub/",
|
|
117
|
+
"dist/src/first-run/",
|
|
117
118
|
"dist/src/core/",
|
|
118
119
|
"bin/fraim.js",
|
|
119
120
|
"bin/fraim-mcp.js",
|
|
120
121
|
"public/ai-hub/",
|
|
122
|
+
"public/first-run/",
|
|
121
123
|
"index.js",
|
|
122
124
|
"README.md",
|
|
123
125
|
"CHANGELOG.md",
|