fraim-framework 2.0.173 → 2.0.175
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/cli/commands/cleanup-artifacts.js +1 -0
- package/dist/src/cli/setup/ide-detector.js +26 -14
- package/dist/src/first-run/session-service.js +15 -1
- package/dist/src/local-mcp-server/artifact-retention-cleanup.js +50 -7
- package/package.json +1 -1
- package/public/first-run/script.js +62 -56
- package/public/first-run/styles.css +47 -0
|
@@ -21,6 +21,7 @@ function runCleanupArtifactsCommand(options) {
|
|
|
21
21
|
process.stdout.write(`${JSON.stringify(report, null, 2)}\n`);
|
|
22
22
|
}
|
|
23
23
|
exports.cleanupArtifactsCommand = new commander_1.Command('cleanup-artifacts')
|
|
24
|
+
.alias('cleanup-artifact')
|
|
24
25
|
.description('Evaluate and optionally apply artifact retention cleanup for the current FRAIM workspace')
|
|
25
26
|
.option('--user <email>', 'Active user email used only for user-scoped cleanup categories')
|
|
26
27
|
.option('--workspace-root <path>', 'Workspace root containing fraim/config.json')
|
|
@@ -3,7 +3,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
3
3
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
-
exports.expandPath = exports.findIDEByName = exports.getAllSupportedIDEs = exports.detectInstalledIDEs = exports.IDE_CONFIGS = void 0;
|
|
6
|
+
exports.expandPath = exports.findIDEByName = exports.getAllSupportedIDEs = exports.detectInstalledIDEs = exports.CLI_PROBE_CONFIGTYPES = exports.IDE_CONFIGS = void 0;
|
|
7
7
|
exports.getAdapterConfigType = getAdapterConfigType;
|
|
8
8
|
const fs_1 = __importDefault(require("fs"));
|
|
9
9
|
const path_1 = __importDefault(require("path"));
|
|
@@ -71,9 +71,9 @@ const detectGeminiCli = () => {
|
|
|
71
71
|
};
|
|
72
72
|
const detectGeminiSurface = () => {
|
|
73
73
|
const paths = [
|
|
74
|
-
'~/.gemini',
|
|
75
|
-
'~/AppData/Roaming/gemini',
|
|
76
|
-
'~/.config/gemini'
|
|
74
|
+
'~/.gemini/settings.json',
|
|
75
|
+
'~/AppData/Roaming/gemini/settings.json',
|
|
76
|
+
'~/.config/gemini/settings.json'
|
|
77
77
|
];
|
|
78
78
|
return checkMultiplePaths(paths);
|
|
79
79
|
};
|
|
@@ -103,7 +103,8 @@ exports.IDE_CONFIGS = [
|
|
|
103
103
|
alternativePaths: [
|
|
104
104
|
'~/.claude/settings.json'
|
|
105
105
|
],
|
|
106
|
-
description: 'Anthropic Claude Code local surfaces (CLI and Desktop Code tab)'
|
|
106
|
+
description: 'Anthropic Claude Code local surfaces (CLI and Desktop Code tab)',
|
|
107
|
+
downloadUrl: 'https://claude.ai/code',
|
|
107
108
|
},
|
|
108
109
|
{
|
|
109
110
|
name: 'Claude Desktop / Cowork',
|
|
@@ -116,7 +117,8 @@ exports.IDE_CONFIGS = [
|
|
|
116
117
|
alternativePaths: [
|
|
117
118
|
'~/Library/Application Support/Claude/claude_desktop_config.json'
|
|
118
119
|
],
|
|
119
|
-
description: 'Anthropic Claude Desktop chat and Cowork surfaces'
|
|
120
|
+
description: 'Anthropic Claude Desktop chat and Cowork surfaces',
|
|
121
|
+
downloadUrl: 'https://claude.ai/download',
|
|
120
122
|
},
|
|
121
123
|
{
|
|
122
124
|
name: 'Antigravity',
|
|
@@ -125,7 +127,8 @@ exports.IDE_CONFIGS = [
|
|
|
125
127
|
configType: 'standard',
|
|
126
128
|
invocationProfile: 'cursor-mention',
|
|
127
129
|
detectMethod: () => fs_1.default.existsSync(expandPath('~/.gemini/antigravity')),
|
|
128
|
-
description: 'Google Gemini Antigravity IDE'
|
|
130
|
+
description: 'Google Gemini Antigravity IDE',
|
|
131
|
+
downloadUrl: 'https://deepmind.google/technologies/gemini/',
|
|
129
132
|
},
|
|
130
133
|
{
|
|
131
134
|
name: 'Gemini CLI',
|
|
@@ -140,7 +143,8 @@ exports.IDE_CONFIGS = [
|
|
|
140
143
|
'~/AppData/Roaming/gemini/settings.json',
|
|
141
144
|
'~/.config/gemini/settings.json'
|
|
142
145
|
],
|
|
143
|
-
description: 'Google Gemini CLI local settings'
|
|
146
|
+
description: 'Google Gemini CLI local settings',
|
|
147
|
+
downloadUrl: 'https://github.com/google-gemini/gemini-cli',
|
|
144
148
|
},
|
|
145
149
|
{
|
|
146
150
|
name: 'Kiro',
|
|
@@ -149,7 +153,8 @@ exports.IDE_CONFIGS = [
|
|
|
149
153
|
configType: 'kiro',
|
|
150
154
|
invocationProfile: 'kiro-hashtag',
|
|
151
155
|
detectMethod: () => fs_1.default.existsSync(expandPath('~/.kiro')),
|
|
152
|
-
description: 'Kiro AI-powered IDE'
|
|
156
|
+
description: 'Kiro AI-powered IDE',
|
|
157
|
+
downloadUrl: 'https://kiro.dev/',
|
|
153
158
|
},
|
|
154
159
|
{
|
|
155
160
|
name: 'Cursor',
|
|
@@ -164,7 +169,8 @@ exports.IDE_CONFIGS = [
|
|
|
164
169
|
'~/AppData/Roaming/Cursor/mcp.json',
|
|
165
170
|
'~/.config/Cursor/mcp.json'
|
|
166
171
|
],
|
|
167
|
-
description: 'Cursor AI code editor'
|
|
172
|
+
description: 'Cursor AI code editor',
|
|
173
|
+
downloadUrl: 'https://cursor.com/',
|
|
168
174
|
},
|
|
169
175
|
{
|
|
170
176
|
name: 'VSCode',
|
|
@@ -182,7 +188,8 @@ exports.IDE_CONFIGS = [
|
|
|
182
188
|
'~/AppData/Roaming/Code/User/mcp.json',
|
|
183
189
|
'~/.config/Code/User/mcp.json'
|
|
184
190
|
],
|
|
185
|
-
description: 'Visual Studio Code'
|
|
191
|
+
description: 'Visual Studio Code',
|
|
192
|
+
downloadUrl: 'https://code.visualstudio.com/',
|
|
186
193
|
},
|
|
187
194
|
{
|
|
188
195
|
name: 'Codex',
|
|
@@ -191,7 +198,8 @@ exports.IDE_CONFIGS = [
|
|
|
191
198
|
configType: 'codex',
|
|
192
199
|
invocationProfile: 'codex-skill',
|
|
193
200
|
detectMethod: detectCodexSurface,
|
|
194
|
-
description: 'Codex AI development environment'
|
|
201
|
+
description: 'Codex AI development environment',
|
|
202
|
+
downloadUrl: 'https://github.com/openai/codex',
|
|
195
203
|
},
|
|
196
204
|
{
|
|
197
205
|
name: 'Grok',
|
|
@@ -202,7 +210,8 @@ exports.IDE_CONFIGS = [
|
|
|
202
210
|
detectMethod: detectGrokSurface,
|
|
203
211
|
supportsConfigBootstrap: true,
|
|
204
212
|
aliases: ['grok', 'grok-cli', 'grok cli', 'grok build'],
|
|
205
|
-
description: 'xAI Grok Build CLI local settings'
|
|
213
|
+
description: 'xAI Grok Build CLI local settings',
|
|
214
|
+
downloadUrl: 'https://x.ai/grok',
|
|
206
215
|
},
|
|
207
216
|
{
|
|
208
217
|
name: 'Windsurf',
|
|
@@ -216,7 +225,8 @@ exports.IDE_CONFIGS = [
|
|
|
216
225
|
'~/AppData/Roaming/Windsurf/mcp_config.json',
|
|
217
226
|
'~/.config/windsurf/mcp_config.json'
|
|
218
227
|
],
|
|
219
|
-
description: 'Codeium Windsurf IDE'
|
|
228
|
+
description: 'Codeium Windsurf IDE',
|
|
229
|
+
downloadUrl: 'https://windsurf.com/',
|
|
220
230
|
}
|
|
221
231
|
];
|
|
222
232
|
const findBestConfigPath = (ide) => {
|
|
@@ -236,6 +246,8 @@ const _cachedIDEs = new Map();
|
|
|
236
246
|
const _cacheTimestamps = new Map();
|
|
237
247
|
const _cacheHomeDirs = new Map();
|
|
238
248
|
const DETECT_CACHE_TTL_MS = 5000;
|
|
249
|
+
/** configTypes that require a binary probe in cli-runnable mode; config folder alone is insufficient. */
|
|
250
|
+
exports.CLI_PROBE_CONFIGTYPES = new Set(['claude-code', 'codex', 'grok', 'gemini-cli']);
|
|
239
251
|
const isDetectedForMode = (ide, mode) => {
|
|
240
252
|
if (mode === 'cli-runnable') {
|
|
241
253
|
switch (ide.configType) {
|
|
@@ -175,7 +175,13 @@ function normalizeRows(rows) {
|
|
|
175
175
|
}));
|
|
176
176
|
}
|
|
177
177
|
function buildRunnableAgentSurfaces() {
|
|
178
|
-
|
|
178
|
+
// GUI agents (Claude Desktop, VSCode, Cursor, Windsurf, Kiro) are detected by config-surface:
|
|
179
|
+
// their config folder is sufficient evidence of installation.
|
|
180
|
+
// CLI agents require a binary on PATH; a config folder alone does not mean the agent is
|
|
181
|
+
// runnable, so they are gated through cli-runnable as well.
|
|
182
|
+
const configSurface = (0, ide_detector_1.detectInstalledIDEs)('config-surface');
|
|
183
|
+
const cliRunnableTypes = new Set((0, ide_detector_1.detectInstalledIDEs)('cli-runnable').map((ide) => ide.configType));
|
|
184
|
+
const ides = configSurface.filter((ide) => !ide_detector_1.CLI_PROBE_CONFIGTYPES.has(ide.configType) || cliRunnableTypes.has(ide.configType));
|
|
179
185
|
const hints = (0, ide_global_integration_1.describeOnboardingInvocationSurfaces)(ides);
|
|
180
186
|
return ides.map((ide, index) => ({
|
|
181
187
|
id: ide.configType,
|
|
@@ -426,6 +432,13 @@ class FirstRunSessionService {
|
|
|
426
432
|
getSession() {
|
|
427
433
|
this.detectRowsOnLoad();
|
|
428
434
|
this.persist();
|
|
435
|
+
const detectedNames = new Set((this.state.setupResult?.configuredSurfaces ?? []).map((s) => s.name));
|
|
436
|
+
const supportedAgents = ide_detector_1.IDE_CONFIGS
|
|
437
|
+
.map((ide) => ({
|
|
438
|
+
name: ide.name,
|
|
439
|
+
detected: detectedNames.has(ide.name),
|
|
440
|
+
downloadUrl: ide.downloadUrl ?? null,
|
|
441
|
+
}));
|
|
429
442
|
return {
|
|
430
443
|
state: this.state,
|
|
431
444
|
rows: this.state.rows,
|
|
@@ -435,6 +448,7 @@ class FirstRunSessionService {
|
|
|
435
448
|
requestToken: this.requestToken,
|
|
436
449
|
agentOptions: types_1.FIRST_RUN_AGENT_OPTIONS,
|
|
437
450
|
currentAgentId: this.state.agentId,
|
|
451
|
+
supportedAgents,
|
|
438
452
|
};
|
|
439
453
|
}
|
|
440
454
|
respond(message, ok) {
|
|
@@ -8,6 +8,7 @@ exports.resolveArtifactRetentionConfig = resolveArtifactRetentionConfig;
|
|
|
8
8
|
exports.runArtifactCleanup = runArtifactCleanup;
|
|
9
9
|
exports.planArtifactCleanup = planArtifactCleanup;
|
|
10
10
|
const node_fs_1 = __importDefault(require("node:fs"));
|
|
11
|
+
const node_child_process_1 = require("node:child_process");
|
|
11
12
|
const node_path_1 = __importDefault(require("node:path"));
|
|
12
13
|
const project_fraim_paths_1 = require("../core/utils/project-fraim-paths");
|
|
13
14
|
exports.ARTIFACT_RETENTION_CATEGORIES = [
|
|
@@ -96,6 +97,41 @@ function fileMtime(absolutePath) {
|
|
|
96
97
|
return undefined;
|
|
97
98
|
}
|
|
98
99
|
}
|
|
100
|
+
function parseDate(value) {
|
|
101
|
+
if (!value)
|
|
102
|
+
return undefined;
|
|
103
|
+
const parsed = new Date(value);
|
|
104
|
+
return Number.isNaN(parsed.getTime()) ? undefined : parsed;
|
|
105
|
+
}
|
|
106
|
+
function loadGitLastChangeDates(workspaceRoot, relativeDir) {
|
|
107
|
+
const dates = new Map();
|
|
108
|
+
try {
|
|
109
|
+
const output = (0, node_child_process_1.execFileSync)('git', ['log', '--format=__FRAIM_CLEANUP_DATE__%cI', '--name-only', '--', relativeDir], {
|
|
110
|
+
cwd: workspaceRoot,
|
|
111
|
+
encoding: 'utf8',
|
|
112
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
113
|
+
timeout: 10_000,
|
|
114
|
+
maxBuffer: 80 * 1024 * 1024,
|
|
115
|
+
});
|
|
116
|
+
let currentDate;
|
|
117
|
+
for (const rawLine of output.split(/\r?\n/)) {
|
|
118
|
+
const line = rawLine.trim();
|
|
119
|
+
if (!line)
|
|
120
|
+
continue;
|
|
121
|
+
if (line.startsWith('__FRAIM_CLEANUP_DATE__')) {
|
|
122
|
+
currentDate = parseDate(line.slice('__FRAIM_CLEANUP_DATE__'.length));
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
const relativePath = line.replace(/\\/g, '/');
|
|
126
|
+
if (currentDate && !dates.has(relativePath))
|
|
127
|
+
dates.set(relativePath, currentDate);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
catch {
|
|
131
|
+
return new Map();
|
|
132
|
+
}
|
|
133
|
+
return dates;
|
|
134
|
+
}
|
|
99
135
|
function readFrontmatter(content) {
|
|
100
136
|
const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---/);
|
|
101
137
|
if (!match)
|
|
@@ -124,6 +160,10 @@ function synthesizedDate(absolutePath) {
|
|
|
124
160
|
function userScoped(fileName, activeUserEmail) {
|
|
125
161
|
return fileName.startsWith(`${activeUserEmail}-`);
|
|
126
162
|
}
|
|
163
|
+
function evidenceEligibilityDate(workspaceRoot, absolutePath, gitDates) {
|
|
164
|
+
const relativePath = normalizeRelative(workspaceRoot, absolutePath);
|
|
165
|
+
return gitDates.get(relativePath) ?? fileMtime(absolutePath);
|
|
166
|
+
}
|
|
127
167
|
function collectCandidates(workspaceRoot, activeUserEmail) {
|
|
128
168
|
const candidates = [];
|
|
129
169
|
const add = (category, absolutePath, eligible, reason, eligibilityDate) => {
|
|
@@ -155,23 +195,26 @@ function collectCandidates(workspaceRoot, activeUserEmail) {
|
|
|
155
195
|
add('retrospectives', filePath, false, 'Retrospective has no synthesized frontmatter.');
|
|
156
196
|
}
|
|
157
197
|
const evidenceDir = node_path_1.default.join(workspaceRoot, 'docs', 'evidence');
|
|
198
|
+
const evidenceGitDates = loadGitLastChangeDates(workspaceRoot, 'docs/evidence');
|
|
158
199
|
for (const filePath of listFiles(evidenceDir)) {
|
|
159
200
|
const fileName = node_path_1.default.basename(filePath);
|
|
160
201
|
const isMarkdown = fileName.endsWith('.md');
|
|
161
202
|
const isJson = fileName.endsWith('.json');
|
|
203
|
+
const eligibilityDate = evidenceEligibilityDate(workspaceRoot, filePath, evidenceGitDates);
|
|
162
204
|
if (fileName.includes('cleanup-manifest') || fileName.includes('cleanup_manifest')) {
|
|
163
|
-
if (
|
|
164
|
-
|
|
165
|
-
|
|
205
|
+
if (isMarkdown || isJson) {
|
|
206
|
+
add('cleanup_manifests', filePath, true, 'Cleanup manifest file.', eligibilityDate);
|
|
207
|
+
}
|
|
208
|
+
else {
|
|
209
|
+
add('evidence', filePath, true, 'Evidence file.', eligibilityDate);
|
|
210
|
+
}
|
|
166
211
|
continue;
|
|
167
212
|
}
|
|
168
|
-
if (!isMarkdown)
|
|
169
|
-
continue;
|
|
170
213
|
if (fileName.endsWith('-feedback.md')) {
|
|
171
|
-
add('feedback', filePath, true, 'Feedback evidence file.',
|
|
214
|
+
add('feedback', filePath, true, 'Feedback evidence file.', eligibilityDate);
|
|
172
215
|
continue;
|
|
173
216
|
}
|
|
174
|
-
add('evidence', filePath, true, 'Evidence file.',
|
|
217
|
+
add('evidence', filePath, true, 'Evidence file.', eligibilityDate);
|
|
175
218
|
}
|
|
176
219
|
return candidates;
|
|
177
220
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "fraim-framework",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.175",
|
|
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": {
|
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
configureError: null,
|
|
14
14
|
};
|
|
15
15
|
|
|
16
|
+
|
|
16
17
|
const STEP_LABELS = {
|
|
17
18
|
prereqs: 'Prerequisites',
|
|
18
19
|
agents: 'Local Agents',
|
|
@@ -86,15 +87,6 @@
|
|
|
86
87
|
return configuredSurfaces().some((surface) => surface && (surface.id === opt.id || surface.name === opt.label));
|
|
87
88
|
}
|
|
88
89
|
|
|
89
|
-
function readyAgentLabels() {
|
|
90
|
-
const labels = new Set();
|
|
91
|
-
for (const agent of readyAgents()) labels.add(agent.label);
|
|
92
|
-
for (const surface of configuredSurfaces()) {
|
|
93
|
-
if (surface && surface.name) labels.add(surface.name);
|
|
94
|
-
}
|
|
95
|
-
return Array.from(labels);
|
|
96
|
-
}
|
|
97
|
-
|
|
98
90
|
function detectedAgentCount() {
|
|
99
91
|
const setupResult = state.session && state.session.state ? state.session.state.setupResult : null;
|
|
100
92
|
return (setupResult && setupResult.detectedSurfaceCount) || readyAgents().length;
|
|
@@ -216,60 +208,74 @@
|
|
|
216
208
|
h.textContent = 'Locally installed AI agents';
|
|
217
209
|
pane.appendChild(h);
|
|
218
210
|
|
|
211
|
+
const anyDetected = detectedAgentCount() > 0;
|
|
219
212
|
const p = document.createElement('p');
|
|
220
213
|
p.className = 'pane-copy';
|
|
221
|
-
p.textContent =
|
|
222
|
-
? '
|
|
223
|
-
: '
|
|
214
|
+
p.textContent = anyDetected
|
|
215
|
+
? 'An AI tool is ready. You can add more or continue to configure FRAIM.'
|
|
216
|
+
: 'Install any of the AI tools below, then click Rescan once it is running.';
|
|
224
217
|
pane.appendChild(p);
|
|
225
218
|
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
readyList.className = 'ready-strip';
|
|
230
|
-
readyList.setAttribute('data-testid', 'ready-agents');
|
|
231
|
-
readyList.textContent = `Already installed: ${readyLabels.join(', ')}`;
|
|
232
|
-
pane.appendChild(readyList);
|
|
233
|
-
} else {
|
|
234
|
-
const readyList = document.createElement('div');
|
|
235
|
-
readyList.className = 'ready-strip ready-strip--muted';
|
|
236
|
-
readyList.setAttribute('data-testid', 'ready-agents');
|
|
237
|
-
readyList.textContent = 'Already installed: none detected yet';
|
|
238
|
-
pane.appendChild(readyList);
|
|
239
|
-
}
|
|
219
|
+
// Server-driven list: session.supportedAgents contains every GUI-detectable IDE
|
|
220
|
+
// with detection status and download URL already resolved.
|
|
221
|
+
const agents = (state.session && state.session.supportedAgents) || [];
|
|
240
222
|
|
|
241
|
-
const
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
const
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
223
|
+
const list = document.createElement('ul');
|
|
224
|
+
list.className = 'agent-list';
|
|
225
|
+
list.setAttribute('data-testid', 'agent-list');
|
|
226
|
+
|
|
227
|
+
for (const agent of agents) {
|
|
228
|
+
const li = document.createElement('li');
|
|
229
|
+
li.className = 'agent-row' + (agent.detected ? ' agent-row--detected' : '');
|
|
230
|
+
li.setAttribute('data-testid', `agent-row-${agent.name.toLowerCase().replace(/\s+/g, '-')}`);
|
|
231
|
+
|
|
232
|
+
const nameSpan = document.createElement('span');
|
|
233
|
+
nameSpan.className = 'agent-row__name';
|
|
234
|
+
nameSpan.textContent = agent.name;
|
|
235
|
+
|
|
236
|
+
const actionSpan = document.createElement('span');
|
|
237
|
+
actionSpan.className = 'agent-row__action';
|
|
238
|
+
|
|
239
|
+
if (agent.detected) {
|
|
240
|
+
const badge = document.createElement('span');
|
|
241
|
+
badge.className = 'agent-badge agent-badge--installed';
|
|
242
|
+
badge.setAttribute('data-testid', `agent-installed-${agent.name.toLowerCase().replace(/\s+/g, '-')}`);
|
|
243
|
+
badge.textContent = 'Installed';
|
|
244
|
+
actionSpan.appendChild(badge);
|
|
245
|
+
} else if (agent.downloadUrl) {
|
|
246
|
+
const link = document.createElement('a');
|
|
247
|
+
link.className = 'agent-download-link';
|
|
248
|
+
link.href = agent.downloadUrl;
|
|
249
|
+
link.target = '_blank';
|
|
250
|
+
link.rel = 'noopener noreferrer';
|
|
251
|
+
link.textContent = 'Download';
|
|
252
|
+
link.setAttribute('data-testid', `agent-download-${agent.name.toLowerCase().replace(/\s+/g, '-')}`);
|
|
253
|
+
actionSpan.appendChild(link);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
li.appendChild(nameSpan);
|
|
257
|
+
li.appendChild(actionSpan);
|
|
258
|
+
list.appendChild(li);
|
|
266
259
|
}
|
|
267
260
|
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
261
|
+
pane.appendChild(list);
|
|
262
|
+
|
|
263
|
+
const otherNote = document.createElement('p');
|
|
264
|
+
otherNote.className = 'pane-copy';
|
|
265
|
+
otherNote.innerHTML = 'Using a different tool? Run <code>npx fraim add-ide</code> from your project directory after setup.';
|
|
266
|
+
pane.appendChild(otherNote);
|
|
267
|
+
|
|
268
|
+
const rescanBtn = button('Rescan installed tools', 'ghost');
|
|
269
|
+
rescanBtn.setAttribute('data-testid', 'rescan-agents');
|
|
270
|
+
rescanBtn.addEventListener('click', async () => {
|
|
271
|
+
rescanBtn.disabled = true;
|
|
272
|
+
rescanBtn.textContent = 'Scanning...';
|
|
273
|
+
try { await loadSession(false); } finally {
|
|
274
|
+
state.activeStep = 'agents';
|
|
275
|
+
render();
|
|
276
|
+
}
|
|
277
|
+
});
|
|
278
|
+
pane.appendChild(rescanBtn);
|
|
273
279
|
|
|
274
280
|
const done = button('Next', 'primary');
|
|
275
281
|
done.setAttribute('data-testid', 'done-installing-agents');
|
|
@@ -516,6 +516,53 @@ body {
|
|
|
516
516
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
517
517
|
gap: 12px;
|
|
518
518
|
}
|
|
519
|
+
|
|
520
|
+
/* Compact server-driven agent list */
|
|
521
|
+
.agent-list {
|
|
522
|
+
list-style: none;
|
|
523
|
+
margin: 0 0 12px;
|
|
524
|
+
padding: 0;
|
|
525
|
+
border: 1px solid var(--line);
|
|
526
|
+
border-radius: 10px;
|
|
527
|
+
overflow: hidden;
|
|
528
|
+
}
|
|
529
|
+
.agent-row {
|
|
530
|
+
display: flex;
|
|
531
|
+
align-items: center;
|
|
532
|
+
justify-content: space-between;
|
|
533
|
+
padding: 12px 16px;
|
|
534
|
+
border-bottom: 1px solid var(--line);
|
|
535
|
+
background: var(--surface);
|
|
536
|
+
gap: 12px;
|
|
537
|
+
}
|
|
538
|
+
.agent-row:last-child { border-bottom: 0; }
|
|
539
|
+
.agent-row--detected { background: var(--accent-soft); }
|
|
540
|
+
.agent-row__name {
|
|
541
|
+
font-size: 14px;
|
|
542
|
+
font-weight: 500;
|
|
543
|
+
color: var(--text);
|
|
544
|
+
flex: 1;
|
|
545
|
+
}
|
|
546
|
+
.agent-row--detected .agent-row__name { color: var(--accent-strong); }
|
|
547
|
+
.agent-row__action { flex-shrink: 0; }
|
|
548
|
+
.agent-badge {
|
|
549
|
+
font-size: 12px;
|
|
550
|
+
font-weight: 600;
|
|
551
|
+
padding: 3px 10px;
|
|
552
|
+
border-radius: 20px;
|
|
553
|
+
}
|
|
554
|
+
.agent-badge--installed {
|
|
555
|
+
background: var(--accent);
|
|
556
|
+
color: #fff;
|
|
557
|
+
}
|
|
558
|
+
.agent-download-link {
|
|
559
|
+
font-size: 13px;
|
|
560
|
+
font-weight: 500;
|
|
561
|
+
color: var(--accent-strong);
|
|
562
|
+
text-decoration: none;
|
|
563
|
+
}
|
|
564
|
+
.agent-download-link:hover { text-decoration: underline; }
|
|
565
|
+
|
|
519
566
|
.ready-strip {
|
|
520
567
|
background: var(--accent-soft);
|
|
521
568
|
border: 1px solid var(--accent);
|