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.
Files changed (33) hide show
  1. package/dist/src/ai-hub/catalog.js +280 -44
  2. package/dist/src/ai-hub/desktop-main.js +2 -2
  3. package/dist/src/ai-hub/hosts.js +384 -10
  4. package/dist/src/ai-hub/server.js +255 -9
  5. package/dist/src/cli/commands/add-ide.js +4 -3
  6. package/dist/src/cli/commands/first-run.js +61 -0
  7. package/dist/src/cli/commands/hub.js +4 -4
  8. package/dist/src/cli/commands/init-project.js +4 -4
  9. package/dist/src/cli/commands/setup.js +4 -3
  10. package/dist/src/cli/commands/sync.js +21 -2
  11. package/dist/src/cli/doctor/checks/ide-config-checks.js +20 -2
  12. package/dist/src/cli/fraim.js +2 -0
  13. package/dist/src/cli/mcp/ide-formats.js +29 -1
  14. package/dist/src/cli/mcp/mcp-server-registry.js +1 -0
  15. package/dist/src/cli/setup/auto-mcp-setup.js +14 -8
  16. package/dist/src/cli/setup/ide-detector.js +32 -1
  17. package/dist/src/cli/setup/ide-global-integration.js +5 -1
  18. package/dist/src/cli/setup/ide-invocation-surfaces.js +14 -0
  19. package/dist/src/cli/setup/mcp-config-generator.js +12 -1
  20. package/dist/src/cli/utils/agent-adapters.js +10 -0
  21. package/dist/src/core/utils/git-utils.js +14 -6
  22. package/dist/src/first-run/install-state.js +68 -0
  23. package/dist/src/first-run/server.js +153 -0
  24. package/dist/src/first-run/session-service.js +302 -0
  25. package/dist/src/first-run/types.js +40 -0
  26. package/dist/src/local-mcp-server/otlp-metrics-receiver.js +7 -1
  27. package/dist/src/local-mcp-server/stdio-server.js +41 -9
  28. package/package.json +3 -1
  29. package/public/ai-hub/index.html +149 -102
  30. package/public/ai-hub/script.js +1154 -271
  31. package/public/ai-hub/styles.css +753 -450
  32. package/public/first-run/index.html +221 -0
  33. package/public/first-run/script.js +361 -0
@@ -0,0 +1,221 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1">
6
+ <title>FRAIM First Run</title>
7
+ <style>
8
+ :root {
9
+ --bg: #f4efe5;
10
+ --panel: #fffdf8;
11
+ --ink: #1f2a1f;
12
+ --muted: #68736a;
13
+ --line: #d8d0c1;
14
+ --accent: #0f6b4d;
15
+ --accent-2: #d6efe4;
16
+ --error: #9f2d2d;
17
+ }
18
+ * { box-sizing: border-box; }
19
+ body {
20
+ margin: 0;
21
+ font-family: "Segoe UI", "Helvetica Neue", sans-serif;
22
+ color: var(--ink);
23
+ background:
24
+ radial-gradient(circle at top right, #efe7d7 0, transparent 32%),
25
+ linear-gradient(180deg, #f7f1e7 0%, var(--bg) 100%);
26
+ }
27
+ .shell {
28
+ min-height: 100vh;
29
+ display: grid;
30
+ grid-template-columns: 280px 1fr;
31
+ }
32
+ aside {
33
+ padding: 32px 24px;
34
+ border-right: 1px solid var(--line);
35
+ background: rgba(255, 253, 248, 0.72);
36
+ backdrop-filter: blur(8px);
37
+ }
38
+ main {
39
+ padding: 32px;
40
+ }
41
+ h1 {
42
+ margin: 0 0 8px;
43
+ font-size: 28px;
44
+ }
45
+ .lede {
46
+ margin: 0 0 20px;
47
+ color: var(--muted);
48
+ line-height: 1.5;
49
+ }
50
+ .steps {
51
+ list-style: none;
52
+ padding: 0;
53
+ margin: 24px 0 0;
54
+ display: grid;
55
+ gap: 10px;
56
+ }
57
+ .step {
58
+ border: 1px solid var(--line);
59
+ border-radius: 16px;
60
+ padding: 12px 14px;
61
+ background: #fff;
62
+ }
63
+ .step strong {
64
+ display: block;
65
+ margin-bottom: 4px;
66
+ font-size: 14px;
67
+ }
68
+ .step span {
69
+ color: var(--muted);
70
+ font-size: 13px;
71
+ text-transform: capitalize;
72
+ }
73
+ .step.active {
74
+ border-color: var(--accent);
75
+ background: var(--accent-2);
76
+ }
77
+ .card {
78
+ max-width: 860px;
79
+ background: var(--panel);
80
+ border: 1px solid var(--line);
81
+ border-radius: 24px;
82
+ padding: 28px;
83
+ box-shadow: 0 24px 64px rgba(49, 45, 32, 0.08);
84
+ }
85
+ .grid {
86
+ display: grid;
87
+ gap: 14px;
88
+ grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
89
+ margin: 20px 0;
90
+ }
91
+ .agent-card, .stat {
92
+ border: 1px solid var(--line);
93
+ border-radius: 18px;
94
+ padding: 16px;
95
+ background: #fff;
96
+ }
97
+ .agent-card h3, .stat h3 {
98
+ margin: 0 0 8px;
99
+ font-size: 18px;
100
+ }
101
+ .agent-card p, .stat p {
102
+ margin: 0;
103
+ color: var(--muted);
104
+ line-height: 1.45;
105
+ }
106
+ .actions {
107
+ display: flex;
108
+ gap: 12px;
109
+ flex-wrap: wrap;
110
+ margin-top: 20px;
111
+ }
112
+ button, input[type="text"] {
113
+ font: inherit;
114
+ }
115
+ button {
116
+ border: 0;
117
+ border-radius: 999px;
118
+ padding: 12px 18px;
119
+ background: var(--accent);
120
+ color: #fff;
121
+ cursor: pointer;
122
+ }
123
+ button.secondary {
124
+ background: #fff;
125
+ color: var(--ink);
126
+ border: 1px solid var(--line);
127
+ }
128
+ button:disabled {
129
+ opacity: 0.6;
130
+ cursor: default;
131
+ }
132
+ label {
133
+ display: block;
134
+ margin: 18px 0 8px;
135
+ font-weight: 600;
136
+ }
137
+ .path-row {
138
+ display: flex;
139
+ gap: 12px;
140
+ flex-wrap: wrap;
141
+ margin-top: 12px;
142
+ }
143
+ #project-path {
144
+ flex: 1 1 440px;
145
+ min-width: 260px;
146
+ border: 1px solid var(--line);
147
+ border-radius: 14px;
148
+ padding: 12px 14px;
149
+ background: #fff;
150
+ }
151
+ pre {
152
+ margin: 18px 0 0;
153
+ padding: 16px;
154
+ border-radius: 18px;
155
+ background: #12201a;
156
+ color: #d8fbe8;
157
+ overflow: auto;
158
+ white-space: pre-wrap;
159
+ min-height: 88px;
160
+ }
161
+ .status {
162
+ margin-top: 18px;
163
+ padding: 14px 16px;
164
+ border-radius: 16px;
165
+ background: #fff;
166
+ border: 1px solid var(--line);
167
+ color: var(--muted);
168
+ line-height: 1.5;
169
+ }
170
+ .error {
171
+ border-color: #e7b8b8;
172
+ color: var(--error);
173
+ background: #fff5f5;
174
+ }
175
+ .prompt {
176
+ border: 1px dashed var(--accent);
177
+ border-radius: 20px;
178
+ padding: 18px;
179
+ margin-top: 18px;
180
+ background: #fcfffd;
181
+ font-size: 22px;
182
+ font-weight: 700;
183
+ }
184
+ a {
185
+ color: var(--accent);
186
+ }
187
+ @media (max-width: 860px) {
188
+ .shell {
189
+ grid-template-columns: 1fr;
190
+ }
191
+ aside {
192
+ border-right: 0;
193
+ border-bottom: 1px solid var(--line);
194
+ }
195
+ main {
196
+ padding: 20px;
197
+ }
198
+ .card {
199
+ padding: 22px;
200
+ }
201
+ }
202
+ </style>
203
+ </head>
204
+ <body>
205
+ <div class="shell">
206
+ <aside>
207
+ <h1>FRAIM First Run</h1>
208
+ <p class="lede">We’ll verify your machine, connect FRAIM to an AI agent, initialize your folder, and leave you with the exact next prompt.</p>
209
+ <ul id="steps" class="steps"></ul>
210
+ </aside>
211
+ <main>
212
+ <section class="card">
213
+ <div id="content"></div>
214
+ <div id="status" class="status">Loading first-run session…</div>
215
+ <pre id="details" hidden></pre>
216
+ </section>
217
+ </main>
218
+ </div>
219
+ <script src="/first-run/script.js"></script>
220
+ </body>
221
+ </html>
@@ -0,0 +1,361 @@
1
+ (function () {
2
+ const stepOrder = ['welcome', 'prereqs', 'agent', 'configure', 'project', 'launch', 'finish'];
3
+ const stepLabels = {
4
+ welcome: 'Welcome',
5
+ prereqs: 'System prerequisites',
6
+ agent: 'AI agent',
7
+ configure: 'Configure FRAIM',
8
+ project: 'Project folder',
9
+ launch: 'Open your AI',
10
+ finish: 'Your first prompt',
11
+ };
12
+
13
+ const state = {
14
+ session: null,
15
+ selectedAgentId: null,
16
+ };
17
+
18
+ const content = document.getElementById('content');
19
+ const statusEl = document.getElementById('status');
20
+ const detailsEl = document.getElementById('details');
21
+ const stepsEl = document.getElementById('steps');
22
+
23
+ function escapeHtml(value) {
24
+ return String(value)
25
+ .replace(/&/g, '&amp;')
26
+ .replace(/</g, '&lt;')
27
+ .replace(/>/g, '&gt;')
28
+ .replace(/"/g, '&quot;')
29
+ .replace(/'/g, '&#39;');
30
+ }
31
+
32
+ function setStatus(message, error) {
33
+ statusEl.textContent = message;
34
+ statusEl.classList.toggle('error', Boolean(error));
35
+ }
36
+
37
+ function setDetails(text) {
38
+ if (!text) {
39
+ detailsEl.hidden = true;
40
+ detailsEl.textContent = '';
41
+ return;
42
+ }
43
+ detailsEl.hidden = false;
44
+ detailsEl.textContent = text;
45
+ }
46
+
47
+ function firstIncompleteStep(session) {
48
+ for (const stepId of stepOrder) {
49
+ const status = session.state.stepStates[stepId];
50
+ if (status !== 'complete') {
51
+ return stepId;
52
+ }
53
+ }
54
+ return 'finish';
55
+ }
56
+
57
+ function renderSteps(session) {
58
+ const active = firstIncompleteStep(session);
59
+ stepsEl.innerHTML = stepOrder.map((stepId) => {
60
+ const status = session.state.stepStates[stepId];
61
+ const activeClass = stepId === active ? ' active' : '';
62
+ return `<li class="step${activeClass}" data-step="${stepId}"><strong>${stepLabels[stepId]}</strong><span>${status}</span></li>`;
63
+ }).join('');
64
+ }
65
+
66
+ async function api(path, method, body) {
67
+ const headers = {};
68
+ if (body) {
69
+ headers['Content-Type'] = 'application/json';
70
+ }
71
+ if (state.session && state.session.requestToken) {
72
+ headers['x-fraim-first-run-token'] = state.session.requestToken;
73
+ }
74
+
75
+ const response = await fetch(path, {
76
+ method: method || 'GET',
77
+ headers: Object.keys(headers).length > 0 ? headers : undefined,
78
+ body: body ? JSON.stringify(body) : undefined,
79
+ });
80
+
81
+ if (response.status === 204) {
82
+ return null;
83
+ }
84
+
85
+ const json = await response.json();
86
+ if (!response.ok) {
87
+ throw new Error(json.error || 'Request failed.');
88
+ }
89
+ return json;
90
+ }
91
+
92
+ async function loadSession() {
93
+ state.session = await api('/api/first-run/session');
94
+ if (!state.selectedAgentId && state.session.state.selectedAgentId) {
95
+ state.selectedAgentId = state.session.state.selectedAgentId;
96
+ }
97
+ render();
98
+ }
99
+
100
+ function renderWelcome() {
101
+ return `
102
+ <h2>One guided setup</h2>
103
+ <p class="lede">This flow uses your FRAIM key, writes FRAIM config where it belongs, initializes your project, and gives you a clean handoff prompt.</p>
104
+ <div class="actions">
105
+ <button id="start-prereqs">Get started</button>
106
+ </div>
107
+ `;
108
+ }
109
+
110
+ function renderPrereqs() {
111
+ return `
112
+ <h2>System prerequisites</h2>
113
+ <p class="lede">We’ll confirm that Node.js, npx, and git are callable before moving into agent setup.</p>
114
+ <div class="grid">
115
+ <article class="stat"><h3>Node.js</h3><p>Required for FRAIM CLI orchestration.</p></article>
116
+ <article class="stat"><h3>npx</h3><p>Used to invoke the packaged FRAIM runtime.</p></article>
117
+ <article class="stat"><h3>git</h3><p>Required for project initialization and issue-based workflows.</p></article>
118
+ </div>
119
+ <div class="actions">
120
+ <button id="run-prereqs">Run checks</button>
121
+ </div>
122
+ `;
123
+ }
124
+
125
+ function renderAgents(session) {
126
+ const cards = session.agents.map((agent) => {
127
+ const selected = state.selectedAgentId === agent.id ? ' style="border-color: var(--accent); background: var(--accent-2);"' : '';
128
+ const actionLabel = agent.detected ? `Use ${agent.label}` : `Choose ${agent.label}`;
129
+ return `
130
+ <article class="agent-card"${selected}>
131
+ <h3>${agent.label}</h3>
132
+ <p>${agent.detected ? 'Detected on this machine.' : 'Not detected yet. You can still choose it and follow the vendor login/install flow.'}</p>
133
+ <p style="margin-top:10px">${agent.detail}</p>
134
+ <div class="actions">
135
+ <button class="select-agent" data-agent="${agent.id}">${actionLabel}</button>
136
+ </div>
137
+ </article>
138
+ `;
139
+ }).join('');
140
+
141
+ return `
142
+ <h2>Choose your AI agent</h2>
143
+ <p class="lede">Pick the agent you want FRAIM to guide for your first run. Subscription/vendor-owned login remains outside FRAIM.</p>
144
+ <div class="grid">${cards}</div>
145
+ `;
146
+ }
147
+
148
+ function renderConfigure() {
149
+ return `
150
+ <h2>Configure FRAIM</h2>
151
+ <p class="lede">This writes your FRAIM global config and updates supported agent config on the machine where possible.</p>
152
+ <div class="actions">
153
+ <button id="configure-fraim">Write config</button>
154
+ </div>
155
+ `;
156
+ }
157
+
158
+ function renderProject(session) {
159
+ const projectPath = escapeHtml(session.state.workspacePath || '');
160
+ return `
161
+ <h2>Pick a project folder</h2>
162
+ <p class="lede">Choose an existing folder or create a new one. FRAIM will initialize git when needed and run <code>fraim init-project</code> there.</p>
163
+ <label for="project-path">Project path</label>
164
+ <div class="path-row">
165
+ <input id="project-path" type="text" value="${projectPath}" placeholder="C:\\Projects\\my-project or /Users/you/Projects/my-project">
166
+ <button class="secondary" id="pick-folder">Browse</button>
167
+ </div>
168
+ <div class="actions">
169
+ <button id="init-project">Initialize project</button>
170
+ </div>
171
+ `;
172
+ }
173
+
174
+ function renderLaunch(session) {
175
+ const launchCommand = session.state.lastLaunchCommand
176
+ ? `<p><strong>Launch command:</strong> <code>${escapeHtml(session.state.lastLaunchCommand)}</code></p>`
177
+ : '';
178
+ return `
179
+ <h2>Open your AI</h2>
180
+ <p class="lede">FRAIM will attempt a best-effort launch, then run a deterministic probe where the selected CLI supports it.</p>
181
+ ${launchCommand}
182
+ <div class="actions">
183
+ <button id="launch-agent">Launch and verify</button>
184
+ </div>
185
+ `;
186
+ }
187
+
188
+ function renderFinish(session) {
189
+ const prompt = escapeHtml(session.prompt || 'Onboard this project');
190
+ const resourcesUrl = escapeHtml(session.state.resourcesUrl);
191
+ return `
192
+ <h2>Your first prompt</h2>
193
+ <p class="lede">Copy this into your agent if it is not already open. FRAIM also writes it to your local install artifact for later recovery.</p>
194
+ <div class="prompt" id="prompt-box">${prompt}</div>
195
+ <div class="actions">
196
+ <button class="secondary" id="copy-prompt">Copy prompt</button>
197
+ <button id="finish-flow">Done</button>
198
+ </div>
199
+ <p style="margin-top:18px"><a href="${resourcesUrl}" target="_blank" rel="noreferrer">Open FRAIM resources</a></p>
200
+ `;
201
+ }
202
+
203
+ function render() {
204
+ const session = state.session;
205
+ if (!session) {
206
+ return;
207
+ }
208
+
209
+ renderSteps(session);
210
+ const active = firstIncompleteStep(session);
211
+ if (active === 'prereqs' && session.state.stepStates.welcome === 'complete') {
212
+ content.innerHTML = renderPrereqs();
213
+ } else if (active === 'agent') {
214
+ content.innerHTML = renderAgents(session);
215
+ } else if (active === 'configure') {
216
+ content.innerHTML = renderConfigure();
217
+ } else if (active === 'project') {
218
+ content.innerHTML = renderProject(session);
219
+ } else if (active === 'launch') {
220
+ content.innerHTML = renderLaunch(session);
221
+ } else if (active === 'finish') {
222
+ content.innerHTML = renderFinish(session);
223
+ } else {
224
+ content.innerHTML = renderWelcome();
225
+ }
226
+ bindActions();
227
+ }
228
+
229
+ function bindActions() {
230
+ const startPrereqs = document.getElementById('start-prereqs');
231
+ if (startPrereqs) {
232
+ startPrereqs.addEventListener('click', async function () {
233
+ state.session.state.stepStates.welcome = 'complete';
234
+ state.session.state.stepStates.prereqs = 'running';
235
+ render();
236
+ });
237
+ }
238
+
239
+ const runPrereqs = document.getElementById('run-prereqs');
240
+ if (runPrereqs) {
241
+ runPrereqs.addEventListener('click', async function () {
242
+ setStatus('Checking prerequisites…');
243
+ try {
244
+ const result = await api('/api/first-run/prereqs', 'POST');
245
+ setStatus(result.message, !result.ok);
246
+ setDetails('');
247
+ await loadSession();
248
+ } catch (error) {
249
+ setStatus(error.message, true);
250
+ }
251
+ });
252
+ }
253
+
254
+ document.querySelectorAll('.select-agent').forEach(function (button) {
255
+ button.addEventListener('click', async function () {
256
+ const agentId = this.getAttribute('data-agent');
257
+ setStatus('Saving agent selection…');
258
+ try {
259
+ const result = await api('/api/first-run/agents/select', 'POST', { agentId: agentId });
260
+ state.selectedAgentId = agentId;
261
+ setStatus(result.message);
262
+ setDetails(result.launchCommand ? `Vendor login command: ${result.launchCommand}` : '');
263
+ await loadSession();
264
+ } catch (error) {
265
+ setStatus(error.message, true);
266
+ }
267
+ });
268
+ });
269
+
270
+ const configureFraim = document.getElementById('configure-fraim');
271
+ if (configureFraim) {
272
+ configureFraim.addEventListener('click', async function () {
273
+ setStatus('Writing FRAIM config…');
274
+ try {
275
+ const result = await api('/api/first-run/configure', 'POST');
276
+ setStatus(result.message, !result.ok);
277
+ setDetails('');
278
+ await loadSession();
279
+ } catch (error) {
280
+ setStatus(error.message, true);
281
+ }
282
+ });
283
+ }
284
+
285
+ const pickFolder = document.getElementById('pick-folder');
286
+ if (pickFolder) {
287
+ pickFolder.addEventListener('click', async function () {
288
+ try {
289
+ const picked = await api('/api/first-run/project-path/pick', 'POST');
290
+ if (picked && picked.path) {
291
+ document.getElementById('project-path').value = picked.path;
292
+ }
293
+ } catch (error) {
294
+ setStatus(error.message, true);
295
+ }
296
+ });
297
+ }
298
+
299
+ const initProject = document.getElementById('init-project');
300
+ if (initProject) {
301
+ initProject.addEventListener('click', async function () {
302
+ const projectPath = document.getElementById('project-path').value.trim();
303
+ if (!projectPath) {
304
+ setStatus('Project path is required.', true);
305
+ return;
306
+ }
307
+ setStatus('Initializing project…');
308
+ try {
309
+ const result = await api('/api/first-run/project', 'POST', { projectPath: projectPath, initializeGit: true });
310
+ setStatus(result.message, !result.ok);
311
+ setDetails('');
312
+ await loadSession();
313
+ } catch (error) {
314
+ setStatus(error.message, true);
315
+ }
316
+ });
317
+ }
318
+
319
+ const launchAgent = document.getElementById('launch-agent');
320
+ if (launchAgent) {
321
+ launchAgent.addEventListener('click', async function () {
322
+ setStatus('Launching agent and running probe…');
323
+ try {
324
+ const result = await api('/api/first-run/launch', 'POST');
325
+ setStatus(result.message, !result.ok);
326
+ setDetails(result.output || result.launchCommand || '');
327
+ await loadSession();
328
+ } catch (error) {
329
+ setStatus(error.message, true);
330
+ }
331
+ });
332
+ }
333
+
334
+ const copyPrompt = document.getElementById('copy-prompt');
335
+ if (copyPrompt) {
336
+ copyPrompt.addEventListener('click', async function () {
337
+ const prompt = document.getElementById('prompt-box').textContent || '';
338
+ await navigator.clipboard.writeText(prompt);
339
+ setStatus('Prompt copied.');
340
+ });
341
+ }
342
+
343
+ const finishFlow = document.getElementById('finish-flow');
344
+ if (finishFlow) {
345
+ finishFlow.addEventListener('click', async function () {
346
+ setStatus('Writing final prompt artifact…');
347
+ try {
348
+ const result = await api('/api/first-run/finish', 'POST');
349
+ setStatus(result.message);
350
+ setDetails('');
351
+ } catch (error) {
352
+ setStatus(error.message, true);
353
+ }
354
+ });
355
+ }
356
+ }
357
+
358
+ loadSession().catch(function (error) {
359
+ setStatus(error.message || 'Could not load first-run.', true);
360
+ });
361
+ }());