@cocaxcode/ai-context-inspector 0.1.0 → 0.3.0
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/README.md +330 -76
- package/dist/chunk-T5N3KKUJ.js +2912 -0
- package/dist/index.js +2 -2
- package/dist/{server-6EILEUO5.js → server-EMQUALI5.js} +1 -1
- package/package.json +3 -2
- package/dist/chunk-6HBCUZQJ.js +0 -1565
|
@@ -0,0 +1,2912 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/scanner/mcp-configs.ts
|
|
4
|
+
import { readFile } from "fs/promises";
|
|
5
|
+
import { join } from "path";
|
|
6
|
+
import { homedir } from "os";
|
|
7
|
+
import { platform } from "process";
|
|
8
|
+
var ENV_VAR_REGEX = /\$\{[^}]+\}/;
|
|
9
|
+
function parseServerEntry(name, entry, source) {
|
|
10
|
+
let transport = "stdio";
|
|
11
|
+
if (entry.type === "http" || entry.type === "sse") {
|
|
12
|
+
transport = entry.type;
|
|
13
|
+
} else if (entry.url && !entry.command) {
|
|
14
|
+
transport = "http";
|
|
15
|
+
}
|
|
16
|
+
const envStr = JSON.stringify(entry.env ?? {}) + JSON.stringify(entry.args ?? []);
|
|
17
|
+
const hasEnvVars = ENV_VAR_REGEX.test(envStr);
|
|
18
|
+
return {
|
|
19
|
+
name,
|
|
20
|
+
source,
|
|
21
|
+
config: {
|
|
22
|
+
transport,
|
|
23
|
+
command: entry.command,
|
|
24
|
+
args: entry.args,
|
|
25
|
+
env: entry.env,
|
|
26
|
+
url: entry.url,
|
|
27
|
+
hasEnvVars
|
|
28
|
+
},
|
|
29
|
+
introspection: null
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
async function readMcpFile(filePath, source) {
|
|
33
|
+
const servers = [];
|
|
34
|
+
const warnings = [];
|
|
35
|
+
try {
|
|
36
|
+
const content = await readFile(filePath, "utf-8");
|
|
37
|
+
const parsed = JSON.parse(content);
|
|
38
|
+
const mcpServers = parsed.mcpServers ?? {};
|
|
39
|
+
for (const [name, entry] of Object.entries(mcpServers)) {
|
|
40
|
+
servers.push(parseServerEntry(name, entry, source));
|
|
41
|
+
}
|
|
42
|
+
} catch (err) {
|
|
43
|
+
const code = err.code;
|
|
44
|
+
if (code !== "ENOENT") {
|
|
45
|
+
warnings.push({
|
|
46
|
+
scanner: "mcp-configs",
|
|
47
|
+
message: `Error leyendo ${filePath}: ${err.message}`,
|
|
48
|
+
path: filePath
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return { servers, warnings };
|
|
53
|
+
}
|
|
54
|
+
function getDesktopConfigPath() {
|
|
55
|
+
if (platform === "win32") {
|
|
56
|
+
return join(
|
|
57
|
+
process.env.APPDATA ?? join(homedir(), "AppData", "Roaming"),
|
|
58
|
+
"Claude",
|
|
59
|
+
"claude_desktop_config.json"
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
if (platform === "darwin") {
|
|
63
|
+
return join(
|
|
64
|
+
homedir(),
|
|
65
|
+
"Library",
|
|
66
|
+
"Application Support",
|
|
67
|
+
"Claude",
|
|
68
|
+
"claude_desktop_config.json"
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
return join(homedir(), ".config", "claude-desktop", "claude_desktop_config.json");
|
|
72
|
+
}
|
|
73
|
+
async function scanMcpConfigs(config) {
|
|
74
|
+
const allServers = [];
|
|
75
|
+
const allWarnings = [];
|
|
76
|
+
const project = await readMcpFile(join(config.dir, ".mcp.json"), "project");
|
|
77
|
+
allServers.push(...project.servers);
|
|
78
|
+
allWarnings.push(...project.warnings);
|
|
79
|
+
const vscode = await readMcpFile(join(config.dir, ".vscode", "mcp.json"), "vscode");
|
|
80
|
+
allServers.push(...vscode.servers);
|
|
81
|
+
allWarnings.push(...vscode.warnings);
|
|
82
|
+
if (config.includeUser) {
|
|
83
|
+
const user = await readMcpFile(join(homedir(), ".claude.json"), "user");
|
|
84
|
+
allServers.push(...user.servers);
|
|
85
|
+
allWarnings.push(...user.warnings);
|
|
86
|
+
const desktop = await readMcpFile(getDesktopConfigPath(), "desktop");
|
|
87
|
+
allServers.push(...desktop.servers);
|
|
88
|
+
allWarnings.push(...desktop.warnings);
|
|
89
|
+
}
|
|
90
|
+
return { servers: allServers, warnings: allWarnings };
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// src/scanner/mcp-introspect.ts
|
|
94
|
+
async function introspectOne(server, timeout) {
|
|
95
|
+
if (server.config.transport !== "stdio" || !server.config.command) {
|
|
96
|
+
return {
|
|
97
|
+
status: "error",
|
|
98
|
+
error: `Transport "${server.config.transport}" no soportado para introspecci\xF3n (solo stdio en MVP)`,
|
|
99
|
+
tools: [],
|
|
100
|
+
resources: [],
|
|
101
|
+
prompts: []
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
const controller = new AbortController();
|
|
105
|
+
const timer = setTimeout(() => controller.abort(), timeout);
|
|
106
|
+
try {
|
|
107
|
+
const { Client } = await import("@modelcontextprotocol/sdk/client/index.js");
|
|
108
|
+
const { StdioClientTransport } = await import("@modelcontextprotocol/sdk/client/stdio.js");
|
|
109
|
+
const transport = new StdioClientTransport({
|
|
110
|
+
command: server.config.command,
|
|
111
|
+
args: server.config.args,
|
|
112
|
+
env: server.config.env ? { ...process.env, ...server.config.env } : void 0
|
|
113
|
+
});
|
|
114
|
+
const client = new Client(
|
|
115
|
+
{ name: "ai-context-inspector", version: "0.1.0" }
|
|
116
|
+
);
|
|
117
|
+
controller.signal.addEventListener("abort", () => {
|
|
118
|
+
transport.close().catch(() => {
|
|
119
|
+
});
|
|
120
|
+
});
|
|
121
|
+
await client.connect(transport);
|
|
122
|
+
const serverInfo = client.getServerVersion();
|
|
123
|
+
const capabilities = client.getServerCapabilities();
|
|
124
|
+
const instructions = client.getInstructions();
|
|
125
|
+
const tools = [];
|
|
126
|
+
let toolsCursor;
|
|
127
|
+
do {
|
|
128
|
+
const result = await client.listTools(
|
|
129
|
+
toolsCursor ? { cursor: toolsCursor } : void 0
|
|
130
|
+
);
|
|
131
|
+
for (const t of result.tools) {
|
|
132
|
+
tools.push({
|
|
133
|
+
name: t.name,
|
|
134
|
+
description: t.description,
|
|
135
|
+
inputSchema: t.inputSchema
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
toolsCursor = result.nextCursor;
|
|
139
|
+
} while (toolsCursor);
|
|
140
|
+
const resources = [];
|
|
141
|
+
try {
|
|
142
|
+
let resCursor;
|
|
143
|
+
do {
|
|
144
|
+
const result = await client.listResources(
|
|
145
|
+
resCursor ? { cursor: resCursor } : void 0
|
|
146
|
+
);
|
|
147
|
+
for (const r of result.resources) {
|
|
148
|
+
resources.push({
|
|
149
|
+
name: r.name,
|
|
150
|
+
uri: r.uri,
|
|
151
|
+
description: r.description,
|
|
152
|
+
mimeType: r.mimeType
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
resCursor = result.nextCursor;
|
|
156
|
+
} while (resCursor);
|
|
157
|
+
} catch {
|
|
158
|
+
}
|
|
159
|
+
const prompts = [];
|
|
160
|
+
try {
|
|
161
|
+
let promptCursor;
|
|
162
|
+
do {
|
|
163
|
+
const result = await client.listPrompts(
|
|
164
|
+
promptCursor ? { cursor: promptCursor } : void 0
|
|
165
|
+
);
|
|
166
|
+
for (const p of result.prompts) {
|
|
167
|
+
prompts.push({
|
|
168
|
+
name: p.name,
|
|
169
|
+
description: p.description,
|
|
170
|
+
arguments: p.arguments?.map((a) => ({
|
|
171
|
+
name: a.name,
|
|
172
|
+
description: a.description,
|
|
173
|
+
required: a.required
|
|
174
|
+
}))
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
promptCursor = result.nextCursor;
|
|
178
|
+
} while (promptCursor);
|
|
179
|
+
} catch {
|
|
180
|
+
}
|
|
181
|
+
await transport.close();
|
|
182
|
+
return {
|
|
183
|
+
status: "ok",
|
|
184
|
+
serverInfo: serverInfo ? { name: serverInfo.name, version: serverInfo.version } : void 0,
|
|
185
|
+
capabilities,
|
|
186
|
+
instructions,
|
|
187
|
+
tools,
|
|
188
|
+
resources,
|
|
189
|
+
prompts
|
|
190
|
+
};
|
|
191
|
+
} catch (err) {
|
|
192
|
+
if (controller.signal.aborted) {
|
|
193
|
+
return {
|
|
194
|
+
status: "timeout",
|
|
195
|
+
error: `Timeout despu\xE9s de ${timeout}ms`,
|
|
196
|
+
tools: [],
|
|
197
|
+
resources: [],
|
|
198
|
+
prompts: []
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
return {
|
|
202
|
+
status: "error",
|
|
203
|
+
error: err.message,
|
|
204
|
+
tools: [],
|
|
205
|
+
resources: [],
|
|
206
|
+
prompts: []
|
|
207
|
+
};
|
|
208
|
+
} finally {
|
|
209
|
+
clearTimeout(timer);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
async function introspectServers(servers, timeout) {
|
|
213
|
+
const stidoServers = servers.filter(
|
|
214
|
+
(s) => s.config.transport === "stdio" && s.config.command
|
|
215
|
+
);
|
|
216
|
+
const results = await Promise.allSettled(
|
|
217
|
+
stidoServers.map((s) => introspectOne(s, timeout))
|
|
218
|
+
);
|
|
219
|
+
for (let i = 0; i < stidoServers.length; i++) {
|
|
220
|
+
const result = results[i];
|
|
221
|
+
stidoServers[i].introspection = result.status === "fulfilled" ? result.value : {
|
|
222
|
+
status: "error",
|
|
223
|
+
error: result.reason?.message ?? "Error desconocido",
|
|
224
|
+
tools: [],
|
|
225
|
+
resources: [],
|
|
226
|
+
prompts: []
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// src/scanner/index.ts
|
|
232
|
+
import { readFile as readFile6 } from "fs/promises";
|
|
233
|
+
import { join as join6, basename as basename2 } from "path";
|
|
234
|
+
import { resolve } from "path";
|
|
235
|
+
|
|
236
|
+
// src/scanner/context-files.ts
|
|
237
|
+
import { readFile as readFile2, stat, readdir } from "fs/promises";
|
|
238
|
+
import { join as join2, relative } from "path";
|
|
239
|
+
import { homedir as homedir2 } from "os";
|
|
240
|
+
|
|
241
|
+
// src/scanner/catalog.ts
|
|
242
|
+
var AI_FILE_CATALOG = [
|
|
243
|
+
// ── Claude ──
|
|
244
|
+
{
|
|
245
|
+
path: "CLAUDE.md",
|
|
246
|
+
tool: "claude",
|
|
247
|
+
type: "file",
|
|
248
|
+
scope: "project",
|
|
249
|
+
description: "Instrucciones de proyecto para Claude Code"
|
|
250
|
+
},
|
|
251
|
+
{
|
|
252
|
+
path: ".claude",
|
|
253
|
+
tool: "claude",
|
|
254
|
+
type: "directory",
|
|
255
|
+
scope: "project",
|
|
256
|
+
description: "Directorio de configuraci\xF3n de Claude Code"
|
|
257
|
+
},
|
|
258
|
+
{
|
|
259
|
+
path: ".mcp.json",
|
|
260
|
+
tool: "claude",
|
|
261
|
+
type: "file",
|
|
262
|
+
scope: "project",
|
|
263
|
+
description: "Configuraci\xF3n de servidores MCP del proyecto"
|
|
264
|
+
},
|
|
265
|
+
// ── Cursor ──
|
|
266
|
+
{
|
|
267
|
+
path: ".cursorrules",
|
|
268
|
+
tool: "cursor",
|
|
269
|
+
type: "file",
|
|
270
|
+
scope: "project",
|
|
271
|
+
description: "Reglas AI de Cursor (legacy)"
|
|
272
|
+
},
|
|
273
|
+
{
|
|
274
|
+
path: ".cursor/rules",
|
|
275
|
+
tool: "cursor",
|
|
276
|
+
type: "directory",
|
|
277
|
+
scope: "project",
|
|
278
|
+
description: "Directorio de reglas de Cursor (.mdc)"
|
|
279
|
+
},
|
|
280
|
+
{
|
|
281
|
+
path: ".cursorignore",
|
|
282
|
+
tool: "cursor",
|
|
283
|
+
type: "file",
|
|
284
|
+
scope: "project",
|
|
285
|
+
description: "Archivos ignorados por Cursor AI"
|
|
286
|
+
},
|
|
287
|
+
{
|
|
288
|
+
path: ".cursorindexingignore",
|
|
289
|
+
tool: "cursor",
|
|
290
|
+
type: "file",
|
|
291
|
+
scope: "project",
|
|
292
|
+
description: "Archivos excluidos del indexado de Cursor"
|
|
293
|
+
},
|
|
294
|
+
// ── Windsurf / Codeium ──
|
|
295
|
+
{
|
|
296
|
+
path: ".windsurfrules",
|
|
297
|
+
tool: "windsurf",
|
|
298
|
+
type: "file",
|
|
299
|
+
scope: "project",
|
|
300
|
+
description: "Reglas AI de Windsurf"
|
|
301
|
+
},
|
|
302
|
+
{
|
|
303
|
+
path: ".windsurf/rules",
|
|
304
|
+
tool: "windsurf",
|
|
305
|
+
type: "directory",
|
|
306
|
+
scope: "project",
|
|
307
|
+
description: "Directorio de reglas de Windsurf"
|
|
308
|
+
},
|
|
309
|
+
// ── GitHub Copilot ──
|
|
310
|
+
{
|
|
311
|
+
path: ".github/copilot-instructions.md",
|
|
312
|
+
tool: "copilot",
|
|
313
|
+
type: "file",
|
|
314
|
+
scope: "project",
|
|
315
|
+
description: "Instrucciones personalizadas de GitHub Copilot"
|
|
316
|
+
},
|
|
317
|
+
{
|
|
318
|
+
path: ".github/instructions",
|
|
319
|
+
tool: "copilot",
|
|
320
|
+
type: "directory",
|
|
321
|
+
scope: "project",
|
|
322
|
+
description: "Instrucciones por ruta de Copilot"
|
|
323
|
+
},
|
|
324
|
+
{
|
|
325
|
+
path: ".github/agents",
|
|
326
|
+
tool: "copilot",
|
|
327
|
+
type: "directory",
|
|
328
|
+
scope: "project",
|
|
329
|
+
description: "Agentes personalizados de Copilot"
|
|
330
|
+
},
|
|
331
|
+
{
|
|
332
|
+
path: ".copilotignore",
|
|
333
|
+
tool: "copilot",
|
|
334
|
+
type: "file",
|
|
335
|
+
scope: "project",
|
|
336
|
+
description: "Archivos excluidos de Copilot"
|
|
337
|
+
},
|
|
338
|
+
// ── Gemini ──
|
|
339
|
+
{
|
|
340
|
+
path: "GEMINI.md",
|
|
341
|
+
tool: "gemini",
|
|
342
|
+
type: "file",
|
|
343
|
+
scope: "project",
|
|
344
|
+
description: "Instrucciones de proyecto para Gemini CLI"
|
|
345
|
+
},
|
|
346
|
+
{
|
|
347
|
+
path: ".gemini",
|
|
348
|
+
tool: "gemini",
|
|
349
|
+
type: "directory",
|
|
350
|
+
scope: "project",
|
|
351
|
+
description: "Directorio de configuraci\xF3n de Gemini"
|
|
352
|
+
},
|
|
353
|
+
{
|
|
354
|
+
path: ".geminiignore",
|
|
355
|
+
tool: "gemini",
|
|
356
|
+
type: "file",
|
|
357
|
+
scope: "project",
|
|
358
|
+
description: "Archivos excluidos de Gemini"
|
|
359
|
+
},
|
|
360
|
+
// ── OpenAI Codex ──
|
|
361
|
+
{
|
|
362
|
+
path: "AGENTS.md",
|
|
363
|
+
tool: "codex",
|
|
364
|
+
alsoUsedBy: ["copilot", "cursor", "gemini"],
|
|
365
|
+
type: "file",
|
|
366
|
+
scope: "project",
|
|
367
|
+
description: "Instrucciones universales de agentes (multi-tool)"
|
|
368
|
+
},
|
|
369
|
+
{
|
|
370
|
+
path: "AGENT.md",
|
|
371
|
+
tool: "codex",
|
|
372
|
+
alsoUsedBy: ["gemini"],
|
|
373
|
+
type: "file",
|
|
374
|
+
scope: "project",
|
|
375
|
+
description: "Instrucciones de agente (alias)"
|
|
376
|
+
},
|
|
377
|
+
{
|
|
378
|
+
path: ".codex",
|
|
379
|
+
tool: "codex",
|
|
380
|
+
type: "directory",
|
|
381
|
+
scope: "project",
|
|
382
|
+
description: "Directorio de configuraci\xF3n de Codex"
|
|
383
|
+
},
|
|
384
|
+
// ── OpenCode ──
|
|
385
|
+
{
|
|
386
|
+
path: "OPENCODE.md",
|
|
387
|
+
tool: "opencode",
|
|
388
|
+
type: "file",
|
|
389
|
+
scope: "project",
|
|
390
|
+
description: "Instrucciones de proyecto para OpenCode CLI"
|
|
391
|
+
},
|
|
392
|
+
{
|
|
393
|
+
path: ".opencode",
|
|
394
|
+
tool: "opencode",
|
|
395
|
+
type: "directory",
|
|
396
|
+
scope: "project",
|
|
397
|
+
description: "Directorio de configuraci\xF3n de OpenCode"
|
|
398
|
+
},
|
|
399
|
+
{
|
|
400
|
+
path: "opencode.json",
|
|
401
|
+
tool: "opencode",
|
|
402
|
+
type: "file",
|
|
403
|
+
scope: "project",
|
|
404
|
+
description: "Configuraci\xF3n de OpenCode"
|
|
405
|
+
},
|
|
406
|
+
// ── Roo Code (ex-Cline fork) ──
|
|
407
|
+
{
|
|
408
|
+
path: ".roo/rules",
|
|
409
|
+
tool: "roo",
|
|
410
|
+
type: "directory",
|
|
411
|
+
scope: "project",
|
|
412
|
+
description: "Directorio de reglas de Roo Code"
|
|
413
|
+
},
|
|
414
|
+
{
|
|
415
|
+
path: ".roorules",
|
|
416
|
+
tool: "roo",
|
|
417
|
+
type: "file",
|
|
418
|
+
scope: "project",
|
|
419
|
+
description: "Reglas de Roo Code (archivo \xFAnico)"
|
|
420
|
+
},
|
|
421
|
+
{
|
|
422
|
+
path: ".rooignore",
|
|
423
|
+
tool: "roo",
|
|
424
|
+
type: "file",
|
|
425
|
+
scope: "project",
|
|
426
|
+
description: "Archivos excluidos de Roo Code"
|
|
427
|
+
},
|
|
428
|
+
// ── Aider ──
|
|
429
|
+
{
|
|
430
|
+
path: ".aider.conf.yml",
|
|
431
|
+
tool: "aider",
|
|
432
|
+
type: "file",
|
|
433
|
+
scope: "project",
|
|
434
|
+
description: "Configuraci\xF3n YAML de Aider"
|
|
435
|
+
},
|
|
436
|
+
{
|
|
437
|
+
path: ".aiderignore",
|
|
438
|
+
tool: "aider",
|
|
439
|
+
type: "file",
|
|
440
|
+
scope: "project",
|
|
441
|
+
description: "Archivos ignorados por Aider"
|
|
442
|
+
},
|
|
443
|
+
{
|
|
444
|
+
path: ".aider.model.settings.yml",
|
|
445
|
+
tool: "aider",
|
|
446
|
+
type: "file",
|
|
447
|
+
scope: "project",
|
|
448
|
+
description: "Configuraci\xF3n de modelo de Aider"
|
|
449
|
+
},
|
|
450
|
+
{
|
|
451
|
+
path: ".aider.model.metadata.json",
|
|
452
|
+
tool: "aider",
|
|
453
|
+
type: "file",
|
|
454
|
+
scope: "project",
|
|
455
|
+
description: "Metadatos de modelo de Aider"
|
|
456
|
+
},
|
|
457
|
+
// ── Cline ──
|
|
458
|
+
{
|
|
459
|
+
path: ".clinerules",
|
|
460
|
+
tool: "cline",
|
|
461
|
+
type: "file",
|
|
462
|
+
scope: "project",
|
|
463
|
+
description: "Reglas de Cline (archivo \xFAnico)"
|
|
464
|
+
},
|
|
465
|
+
{
|
|
466
|
+
path: ".clinerules",
|
|
467
|
+
tool: "cline",
|
|
468
|
+
type: "directory",
|
|
469
|
+
scope: "project",
|
|
470
|
+
description: "Directorio de reglas de Cline"
|
|
471
|
+
},
|
|
472
|
+
{
|
|
473
|
+
path: ".clineignore",
|
|
474
|
+
tool: "cline",
|
|
475
|
+
type: "file",
|
|
476
|
+
scope: "project",
|
|
477
|
+
description: "Archivos excluidos de Cline"
|
|
478
|
+
},
|
|
479
|
+
// ── Amazon Q ──
|
|
480
|
+
{
|
|
481
|
+
path: ".amazonq/rules",
|
|
482
|
+
tool: "amazonq",
|
|
483
|
+
type: "directory",
|
|
484
|
+
scope: "project",
|
|
485
|
+
description: "Directorio de reglas de Amazon Q Developer"
|
|
486
|
+
},
|
|
487
|
+
// ── Augment ──
|
|
488
|
+
{
|
|
489
|
+
path: ".augment/rules",
|
|
490
|
+
tool: "augment",
|
|
491
|
+
type: "directory",
|
|
492
|
+
scope: "project",
|
|
493
|
+
description: "Directorio de reglas de Augment Code"
|
|
494
|
+
},
|
|
495
|
+
{
|
|
496
|
+
path: ".augment-guidelines",
|
|
497
|
+
tool: "augment",
|
|
498
|
+
type: "file",
|
|
499
|
+
scope: "project",
|
|
500
|
+
description: "Directrices de Augment (legacy)"
|
|
501
|
+
},
|
|
502
|
+
// ── Replit ──
|
|
503
|
+
{
|
|
504
|
+
path: ".replit.md",
|
|
505
|
+
tool: "replit",
|
|
506
|
+
type: "file",
|
|
507
|
+
scope: "project",
|
|
508
|
+
description: "Instrucciones de Replit Agent"
|
|
509
|
+
},
|
|
510
|
+
// ── Firebase Studio ──
|
|
511
|
+
{
|
|
512
|
+
path: ".idx/airules.md",
|
|
513
|
+
tool: "firebase",
|
|
514
|
+
type: "file",
|
|
515
|
+
scope: "project",
|
|
516
|
+
description: "Reglas AI de Firebase Studio"
|
|
517
|
+
},
|
|
518
|
+
// ── VS Code ──
|
|
519
|
+
{
|
|
520
|
+
path: ".vscode/mcp.json",
|
|
521
|
+
tool: "vscode",
|
|
522
|
+
type: "file",
|
|
523
|
+
scope: "project",
|
|
524
|
+
description: "Configuraci\xF3n MCP de VS Code"
|
|
525
|
+
},
|
|
526
|
+
// ── Universal ──
|
|
527
|
+
{
|
|
528
|
+
path: "CONVENTIONS.md",
|
|
529
|
+
tool: "universal",
|
|
530
|
+
type: "file",
|
|
531
|
+
scope: "project",
|
|
532
|
+
description: "Convenciones de c\xF3digo (multi-tool)"
|
|
533
|
+
},
|
|
534
|
+
// ── Tabnine ──
|
|
535
|
+
{
|
|
536
|
+
path: ".tabnine.yaml",
|
|
537
|
+
tool: "tabnine",
|
|
538
|
+
type: "file",
|
|
539
|
+
scope: "project",
|
|
540
|
+
description: "Configuraci\xF3n de Tabnine AI"
|
|
541
|
+
},
|
|
542
|
+
// ── Sourcegraph / Cody ──
|
|
543
|
+
{
|
|
544
|
+
path: ".sourcegraph",
|
|
545
|
+
tool: "sourcegraph",
|
|
546
|
+
type: "directory",
|
|
547
|
+
scope: "project",
|
|
548
|
+
description: "Directorio de Sourcegraph Cody"
|
|
549
|
+
},
|
|
550
|
+
// ── Continue.dev (project level) ──
|
|
551
|
+
{
|
|
552
|
+
path: ".continuerules",
|
|
553
|
+
tool: "continue",
|
|
554
|
+
type: "file",
|
|
555
|
+
scope: "project",
|
|
556
|
+
description: "Reglas de Continue.dev"
|
|
557
|
+
},
|
|
558
|
+
{
|
|
559
|
+
path: ".continue/config.yaml",
|
|
560
|
+
tool: "continue",
|
|
561
|
+
type: "file",
|
|
562
|
+
scope: "project",
|
|
563
|
+
description: "Configuraci\xF3n de Continue.dev"
|
|
564
|
+
},
|
|
565
|
+
// ── User-level entries ──
|
|
566
|
+
{
|
|
567
|
+
path: "~/.claude",
|
|
568
|
+
tool: "claude",
|
|
569
|
+
type: "directory",
|
|
570
|
+
scope: "user",
|
|
571
|
+
description: "Configuraci\xF3n global de Claude Code"
|
|
572
|
+
},
|
|
573
|
+
{
|
|
574
|
+
path: "~/.claude.json",
|
|
575
|
+
tool: "claude",
|
|
576
|
+
type: "file",
|
|
577
|
+
scope: "user",
|
|
578
|
+
description: "Servidores MCP a nivel de usuario"
|
|
579
|
+
},
|
|
580
|
+
{
|
|
581
|
+
path: "~/.claude/CLAUDE.md",
|
|
582
|
+
tool: "claude",
|
|
583
|
+
type: "file",
|
|
584
|
+
scope: "user",
|
|
585
|
+
description: "Instrucciones globales de Claude Code"
|
|
586
|
+
},
|
|
587
|
+
{
|
|
588
|
+
path: "~/.gemini",
|
|
589
|
+
tool: "gemini",
|
|
590
|
+
type: "directory",
|
|
591
|
+
scope: "user",
|
|
592
|
+
description: "Configuraci\xF3n global de Gemini CLI"
|
|
593
|
+
},
|
|
594
|
+
{
|
|
595
|
+
path: "~/.gemini/GEMINI.md",
|
|
596
|
+
tool: "gemini",
|
|
597
|
+
type: "file",
|
|
598
|
+
scope: "user",
|
|
599
|
+
description: "Instrucciones globales de Gemini CLI"
|
|
600
|
+
},
|
|
601
|
+
{
|
|
602
|
+
path: "~/.codex",
|
|
603
|
+
tool: "codex",
|
|
604
|
+
type: "directory",
|
|
605
|
+
scope: "user",
|
|
606
|
+
description: "Configuraci\xF3n global de Codex CLI"
|
|
607
|
+
},
|
|
608
|
+
{
|
|
609
|
+
path: "~/.continue",
|
|
610
|
+
tool: "continue",
|
|
611
|
+
type: "directory",
|
|
612
|
+
scope: "user",
|
|
613
|
+
description: "Configuraci\xF3n global de Continue.dev"
|
|
614
|
+
},
|
|
615
|
+
{
|
|
616
|
+
path: "~/.aider.conf.yml",
|
|
617
|
+
tool: "aider",
|
|
618
|
+
type: "file",
|
|
619
|
+
scope: "user",
|
|
620
|
+
description: "Configuraci\xF3n global de Aider"
|
|
621
|
+
},
|
|
622
|
+
{
|
|
623
|
+
path: "~/.augment/rules",
|
|
624
|
+
tool: "augment",
|
|
625
|
+
type: "directory",
|
|
626
|
+
scope: "user",
|
|
627
|
+
description: "Reglas globales de Augment"
|
|
628
|
+
},
|
|
629
|
+
{
|
|
630
|
+
path: "~/.github/agents",
|
|
631
|
+
tool: "copilot",
|
|
632
|
+
type: "directory",
|
|
633
|
+
scope: "user",
|
|
634
|
+
description: "Agentes globales de Copilot"
|
|
635
|
+
},
|
|
636
|
+
{
|
|
637
|
+
path: "~/.codeium",
|
|
638
|
+
tool: "windsurf",
|
|
639
|
+
type: "directory",
|
|
640
|
+
scope: "user",
|
|
641
|
+
description: "Configuraci\xF3n global de Codeium/Windsurf"
|
|
642
|
+
},
|
|
643
|
+
{
|
|
644
|
+
path: "~/.opencode",
|
|
645
|
+
tool: "opencode",
|
|
646
|
+
type: "directory",
|
|
647
|
+
scope: "user",
|
|
648
|
+
description: "Configuraci\xF3n global de OpenCode CLI"
|
|
649
|
+
},
|
|
650
|
+
{
|
|
651
|
+
path: "~/.tabnine",
|
|
652
|
+
tool: "tabnine",
|
|
653
|
+
type: "directory",
|
|
654
|
+
scope: "user",
|
|
655
|
+
description: "Configuraci\xF3n global de Tabnine"
|
|
656
|
+
}
|
|
657
|
+
];
|
|
658
|
+
|
|
659
|
+
// src/scanner/context-files.ts
|
|
660
|
+
var PREVIEW_MAX_CHARS = 2e3;
|
|
661
|
+
function resolveHome(p) {
|
|
662
|
+
return p.startsWith("~/") ? join2(homedir2(), p.slice(2)) : p;
|
|
663
|
+
}
|
|
664
|
+
async function scanDirectory(dirPath, projectDir, scope) {
|
|
665
|
+
const children = [];
|
|
666
|
+
try {
|
|
667
|
+
const entries = await readdir(dirPath, { withFileTypes: true });
|
|
668
|
+
for (const entry of entries) {
|
|
669
|
+
const fullPath = join2(dirPath, entry.name);
|
|
670
|
+
try {
|
|
671
|
+
const s = await stat(fullPath);
|
|
672
|
+
let preview = null;
|
|
673
|
+
if (entry.isFile()) {
|
|
674
|
+
try {
|
|
675
|
+
const content = await readFile2(fullPath, "utf-8");
|
|
676
|
+
preview = content.slice(0, PREVIEW_MAX_CHARS);
|
|
677
|
+
} catch {
|
|
678
|
+
preview = null;
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
children.push({
|
|
682
|
+
path: relative(projectDir, fullPath),
|
|
683
|
+
absolutePath: fullPath,
|
|
684
|
+
tool: "claude",
|
|
685
|
+
alsoUsedBy: [],
|
|
686
|
+
type: entry.isDirectory() ? "directory" : "file",
|
|
687
|
+
scope,
|
|
688
|
+
size: s.size,
|
|
689
|
+
preview
|
|
690
|
+
});
|
|
691
|
+
} catch {
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
} catch {
|
|
695
|
+
}
|
|
696
|
+
return children;
|
|
697
|
+
}
|
|
698
|
+
async function scanContextFiles(config) {
|
|
699
|
+
const files = [];
|
|
700
|
+
const warnings = [];
|
|
701
|
+
for (const entry of AI_FILE_CATALOG) {
|
|
702
|
+
if (entry.scope === "user" && !config.includeUser) continue;
|
|
703
|
+
const resolvedPath = entry.scope === "user" ? resolveHome(entry.path) : join2(config.dir, entry.path);
|
|
704
|
+
try {
|
|
705
|
+
const s = await stat(resolvedPath);
|
|
706
|
+
const isDir = s.isDirectory();
|
|
707
|
+
if (entry.tool === "cline" && entry.path === ".clinerules") {
|
|
708
|
+
if (entry.type === "file" && isDir || entry.type === "directory" && !isDir) {
|
|
709
|
+
continue;
|
|
710
|
+
}
|
|
711
|
+
} else if (entry.type === "file" && isDir) {
|
|
712
|
+
continue;
|
|
713
|
+
} else if (entry.type === "directory" && !isDir) {
|
|
714
|
+
continue;
|
|
715
|
+
}
|
|
716
|
+
let preview = null;
|
|
717
|
+
let children;
|
|
718
|
+
if (isDir) {
|
|
719
|
+
children = await scanDirectory(resolvedPath, config.dir, entry.scope);
|
|
720
|
+
} else {
|
|
721
|
+
try {
|
|
722
|
+
const content = await readFile2(resolvedPath, "utf-8");
|
|
723
|
+
preview = content.slice(0, PREVIEW_MAX_CHARS);
|
|
724
|
+
} catch {
|
|
725
|
+
preview = null;
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
const result = {
|
|
729
|
+
path: entry.scope === "user" ? entry.path : relative(config.dir, resolvedPath),
|
|
730
|
+
absolutePath: resolvedPath,
|
|
731
|
+
tool: entry.tool,
|
|
732
|
+
alsoUsedBy: entry.alsoUsedBy ?? [],
|
|
733
|
+
type: isDir ? "directory" : "file",
|
|
734
|
+
scope: entry.scope,
|
|
735
|
+
size: s.size,
|
|
736
|
+
preview,
|
|
737
|
+
children
|
|
738
|
+
};
|
|
739
|
+
files.push(result);
|
|
740
|
+
} catch (err) {
|
|
741
|
+
const code = err.code;
|
|
742
|
+
if (code === "EACCES") {
|
|
743
|
+
warnings.push({
|
|
744
|
+
scanner: "context-files",
|
|
745
|
+
message: `Permiso denegado: ${resolvedPath}`,
|
|
746
|
+
path: resolvedPath
|
|
747
|
+
});
|
|
748
|
+
files.push({
|
|
749
|
+
path: entry.scope === "user" ? entry.path : relative(config.dir, resolvedPath),
|
|
750
|
+
absolutePath: resolvedPath,
|
|
751
|
+
tool: entry.tool,
|
|
752
|
+
alsoUsedBy: entry.alsoUsedBy ?? [],
|
|
753
|
+
type: entry.type,
|
|
754
|
+
scope: entry.scope,
|
|
755
|
+
size: 0,
|
|
756
|
+
preview: null,
|
|
757
|
+
error: "EACCES"
|
|
758
|
+
});
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
}
|
|
762
|
+
return { files, warnings };
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
// src/scanner/skills.ts
|
|
766
|
+
import { readFile as readFile3, readdir as readdir2, stat as stat2, lstat } from "fs/promises";
|
|
767
|
+
import { join as join3 } from "path";
|
|
768
|
+
import { homedir as homedir3 } from "os";
|
|
769
|
+
var SKILL_DIRS_PROJECT = [".claude/skills"];
|
|
770
|
+
var SKILL_DIRS_USER = ["~/.claude/skills"];
|
|
771
|
+
function parseFrontmatter(content) {
|
|
772
|
+
const match = content.match(/^---\s*\n([\s\S]*?)\n---/);
|
|
773
|
+
if (!match) return null;
|
|
774
|
+
const result = {};
|
|
775
|
+
let currentKey = "";
|
|
776
|
+
let currentValue = "";
|
|
777
|
+
for (const line of match[1].split("\n")) {
|
|
778
|
+
const kvMatch = line.match(/^(\w[\w-]*):\s*(.*)$/);
|
|
779
|
+
if (kvMatch) {
|
|
780
|
+
if (currentKey) result[currentKey] = currentValue.trim();
|
|
781
|
+
currentKey = kvMatch[1];
|
|
782
|
+
currentValue = kvMatch[2].replace(/^["'>-]\s*/, "").replace(/"$/, "");
|
|
783
|
+
} else if (currentKey && line.match(/^\s+/)) {
|
|
784
|
+
currentValue += " " + line.trim();
|
|
785
|
+
}
|
|
786
|
+
}
|
|
787
|
+
if (currentKey) result[currentKey] = currentValue.trim();
|
|
788
|
+
return result;
|
|
789
|
+
}
|
|
790
|
+
async function scanSkillDir(dirPath, scope) {
|
|
791
|
+
const skills = [];
|
|
792
|
+
try {
|
|
793
|
+
const entries = await readdir2(dirPath, { withFileTypes: true });
|
|
794
|
+
for (const entry of entries) {
|
|
795
|
+
if (entry.name.startsWith("_")) continue;
|
|
796
|
+
const entryPath = join3(dirPath, entry.name);
|
|
797
|
+
let isDir = entry.isDirectory();
|
|
798
|
+
if (entry.isSymbolicLink()) {
|
|
799
|
+
try {
|
|
800
|
+
const targetStat = await stat2(entryPath);
|
|
801
|
+
isDir = targetStat.isDirectory();
|
|
802
|
+
} catch {
|
|
803
|
+
continue;
|
|
804
|
+
}
|
|
805
|
+
}
|
|
806
|
+
if (!isDir) continue;
|
|
807
|
+
const skillMdPath = join3(entryPath, "SKILL.md");
|
|
808
|
+
try {
|
|
809
|
+
await stat2(skillMdPath);
|
|
810
|
+
const content = await readFile3(skillMdPath, "utf-8");
|
|
811
|
+
let description;
|
|
812
|
+
let name = entry.name;
|
|
813
|
+
const triggers = [];
|
|
814
|
+
const frontmatter = parseFrontmatter(content);
|
|
815
|
+
if (frontmatter) {
|
|
816
|
+
if (frontmatter.name) name = frontmatter.name;
|
|
817
|
+
if (frontmatter.description) {
|
|
818
|
+
description = frontmatter.description.slice(0, 200);
|
|
819
|
+
}
|
|
820
|
+
}
|
|
821
|
+
if (!description) {
|
|
822
|
+
const purposeMatch = content.match(/##\s*Purpose\s*\n+(.+)/i);
|
|
823
|
+
if (purposeMatch) {
|
|
824
|
+
description = purposeMatch[1].trim().slice(0, 200);
|
|
825
|
+
}
|
|
826
|
+
}
|
|
827
|
+
const triggerMatch = content.match(
|
|
828
|
+
/(?:trigger|triggers?):\s*(.+)/gi
|
|
829
|
+
);
|
|
830
|
+
if (triggerMatch) {
|
|
831
|
+
for (const match of triggerMatch) {
|
|
832
|
+
const value = match.replace(/(?:trigger|triggers?):\s*/i, "").trim();
|
|
833
|
+
if (value) triggers.push(value);
|
|
834
|
+
}
|
|
835
|
+
}
|
|
836
|
+
let isSymlink = false;
|
|
837
|
+
try {
|
|
838
|
+
const lstats = await lstat(entryPath);
|
|
839
|
+
isSymlink = lstats.isSymbolicLink();
|
|
840
|
+
} catch {
|
|
841
|
+
}
|
|
842
|
+
skills.push({
|
|
843
|
+
name,
|
|
844
|
+
path: skillMdPath,
|
|
845
|
+
scope,
|
|
846
|
+
description,
|
|
847
|
+
triggers: triggers.length > 0 ? triggers : void 0,
|
|
848
|
+
isSymlink
|
|
849
|
+
});
|
|
850
|
+
} catch {
|
|
851
|
+
}
|
|
852
|
+
}
|
|
853
|
+
} catch {
|
|
854
|
+
}
|
|
855
|
+
return skills;
|
|
856
|
+
}
|
|
857
|
+
async function parseSkillRegistry(registryPath) {
|
|
858
|
+
const skills = [];
|
|
859
|
+
try {
|
|
860
|
+
const content = await readFile3(registryPath, "utf-8");
|
|
861
|
+
const lines = content.split("\n");
|
|
862
|
+
let inTable = false;
|
|
863
|
+
for (const line of lines) {
|
|
864
|
+
if (line.includes("| Skill") && line.includes("Trigger")) {
|
|
865
|
+
inTable = true;
|
|
866
|
+
continue;
|
|
867
|
+
}
|
|
868
|
+
if (inTable && line.startsWith("|---")) continue;
|
|
869
|
+
if (inTable && line.startsWith("|")) {
|
|
870
|
+
const cells = line.split("|").map((c) => c.trim()).filter(Boolean);
|
|
871
|
+
if (cells.length >= 2) {
|
|
872
|
+
skills.push({
|
|
873
|
+
name: cells[0],
|
|
874
|
+
path: cells[2] ?? registryPath,
|
|
875
|
+
scope: "project",
|
|
876
|
+
triggers: cells[1] ? [cells[1]] : void 0
|
|
877
|
+
});
|
|
878
|
+
}
|
|
879
|
+
} else if (inTable) {
|
|
880
|
+
inTable = false;
|
|
881
|
+
}
|
|
882
|
+
}
|
|
883
|
+
} catch {
|
|
884
|
+
}
|
|
885
|
+
return skills;
|
|
886
|
+
}
|
|
887
|
+
async function scanSkills(config) {
|
|
888
|
+
const allSkills = [];
|
|
889
|
+
const warnings = [];
|
|
890
|
+
for (const dir of SKILL_DIRS_PROJECT) {
|
|
891
|
+
const fullPath = join3(config.dir, dir);
|
|
892
|
+
const found = await scanSkillDir(fullPath, "project");
|
|
893
|
+
allSkills.push(...found);
|
|
894
|
+
}
|
|
895
|
+
if (config.includeUser) {
|
|
896
|
+
for (const dir of SKILL_DIRS_USER) {
|
|
897
|
+
const fullPath = dir.startsWith("~/") ? join3(homedir3(), dir.slice(2)) : dir;
|
|
898
|
+
const found = await scanSkillDir(fullPath, "user");
|
|
899
|
+
allSkills.push(...found);
|
|
900
|
+
}
|
|
901
|
+
}
|
|
902
|
+
const registryPath = join3(config.dir, ".atl", "skill-registry.md");
|
|
903
|
+
const registrySkills = await parseSkillRegistry(registryPath);
|
|
904
|
+
const seen = new Set(allSkills.map((s) => s.name));
|
|
905
|
+
for (const rs of registrySkills) {
|
|
906
|
+
if (!seen.has(rs.name)) {
|
|
907
|
+
allSkills.push(rs);
|
|
908
|
+
seen.add(rs.name);
|
|
909
|
+
}
|
|
910
|
+
}
|
|
911
|
+
return { skills: allSkills, warnings };
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
// src/scanner/agents.ts
|
|
915
|
+
import { readFile as readFile4, readdir as readdir3, stat as stat3 } from "fs/promises";
|
|
916
|
+
import { join as join4 } from "path";
|
|
917
|
+
import { homedir as homedir4 } from "os";
|
|
918
|
+
var AGENT_DIRS_PROJECT = [".claude/agents"];
|
|
919
|
+
var AGENT_DIRS_USER = ["~/.claude/agents"];
|
|
920
|
+
var AGENT_MEMORY_DIRS_USER = ["~/.claude/agent-memory"];
|
|
921
|
+
function parseFrontmatter2(content) {
|
|
922
|
+
const match = content.match(/^---\s*\n([\s\S]*?)\n---/);
|
|
923
|
+
if (!match) return null;
|
|
924
|
+
const result = {};
|
|
925
|
+
let currentKey = "";
|
|
926
|
+
let currentValue = "";
|
|
927
|
+
for (const line of match[1].split("\n")) {
|
|
928
|
+
const kvMatch = line.match(/^(\w[\w-]*):\s*(.*)$/);
|
|
929
|
+
if (kvMatch) {
|
|
930
|
+
if (currentKey) result[currentKey] = currentValue.trim();
|
|
931
|
+
currentKey = kvMatch[1];
|
|
932
|
+
currentValue = kvMatch[2].replace(/^["'>-]\s*/, "").replace(/"$/, "");
|
|
933
|
+
} else if (currentKey && line.match(/^\s+/)) {
|
|
934
|
+
currentValue += " " + line.trim();
|
|
935
|
+
}
|
|
936
|
+
}
|
|
937
|
+
if (currentKey) result[currentKey] = currentValue.trim();
|
|
938
|
+
return result;
|
|
939
|
+
}
|
|
940
|
+
async function scanAgentDir(dirPath, memoryDirPath, scope) {
|
|
941
|
+
const agents = [];
|
|
942
|
+
try {
|
|
943
|
+
const entries = await readdir3(dirPath);
|
|
944
|
+
for (const entry of entries) {
|
|
945
|
+
if (!entry.endsWith(".md")) continue;
|
|
946
|
+
const filePath = join4(dirPath, entry);
|
|
947
|
+
try {
|
|
948
|
+
const s = await stat3(filePath);
|
|
949
|
+
if (!s.isFile()) continue;
|
|
950
|
+
const content = await readFile4(filePath, "utf-8");
|
|
951
|
+
const agentName = entry.replace(/\.md$/, "");
|
|
952
|
+
let name = agentName;
|
|
953
|
+
let description;
|
|
954
|
+
let model;
|
|
955
|
+
const frontmatter = parseFrontmatter2(content);
|
|
956
|
+
if (frontmatter) {
|
|
957
|
+
if (frontmatter.name) name = frontmatter.name;
|
|
958
|
+
if (frontmatter.description) {
|
|
959
|
+
description = frontmatter.description.slice(0, 300);
|
|
960
|
+
}
|
|
961
|
+
if (frontmatter.model) model = frontmatter.model;
|
|
962
|
+
}
|
|
963
|
+
let hasMemory = false;
|
|
964
|
+
if (memoryDirPath) {
|
|
965
|
+
try {
|
|
966
|
+
await stat3(join4(memoryDirPath, agentName));
|
|
967
|
+
hasMemory = true;
|
|
968
|
+
} catch {
|
|
969
|
+
}
|
|
970
|
+
}
|
|
971
|
+
agents.push({
|
|
972
|
+
name,
|
|
973
|
+
path: filePath,
|
|
974
|
+
scope,
|
|
975
|
+
description,
|
|
976
|
+
model,
|
|
977
|
+
hasMemory
|
|
978
|
+
});
|
|
979
|
+
} catch {
|
|
980
|
+
}
|
|
981
|
+
}
|
|
982
|
+
} catch {
|
|
983
|
+
}
|
|
984
|
+
return agents;
|
|
985
|
+
}
|
|
986
|
+
async function scanAgents(config) {
|
|
987
|
+
const allAgents = [];
|
|
988
|
+
const warnings = [];
|
|
989
|
+
for (const dir of AGENT_DIRS_PROJECT) {
|
|
990
|
+
const fullPath = join4(config.dir, dir);
|
|
991
|
+
const found = await scanAgentDir(fullPath, null, "project");
|
|
992
|
+
allAgents.push(...found);
|
|
993
|
+
}
|
|
994
|
+
if (config.includeUser) {
|
|
995
|
+
for (let i = 0; i < AGENT_DIRS_USER.length; i++) {
|
|
996
|
+
const dir = AGENT_DIRS_USER[i];
|
|
997
|
+
const memDir = AGENT_MEMORY_DIRS_USER[i];
|
|
998
|
+
const fullPath = dir.startsWith("~/") ? join4(homedir4(), dir.slice(2)) : dir;
|
|
999
|
+
const memPath = memDir?.startsWith("~/") ? join4(homedir4(), memDir.slice(2)) : memDir ?? null;
|
|
1000
|
+
const found = await scanAgentDir(fullPath, memPath, "user");
|
|
1001
|
+
allAgents.push(...found);
|
|
1002
|
+
}
|
|
1003
|
+
}
|
|
1004
|
+
return { agents: allAgents, warnings };
|
|
1005
|
+
}
|
|
1006
|
+
|
|
1007
|
+
// src/scanner/memories.ts
|
|
1008
|
+
import { readFile as readFile5, readdir as readdir4, stat as stat4 } from "fs/promises";
|
|
1009
|
+
import { join as join5, basename } from "path";
|
|
1010
|
+
import { homedir as homedir5 } from "os";
|
|
1011
|
+
async function detectOpenspec(dir) {
|
|
1012
|
+
const openspecDir = join5(dir, "openspec");
|
|
1013
|
+
try {
|
|
1014
|
+
const s = await stat4(openspecDir);
|
|
1015
|
+
if (!s.isDirectory()) return null;
|
|
1016
|
+
let specsCount = 0;
|
|
1017
|
+
let changesCount = 0;
|
|
1018
|
+
try {
|
|
1019
|
+
const specsDir = join5(openspecDir, "specs");
|
|
1020
|
+
const specs = await readdir4(specsDir);
|
|
1021
|
+
specsCount = specs.filter((f) => !f.startsWith(".")).length;
|
|
1022
|
+
} catch {
|
|
1023
|
+
}
|
|
1024
|
+
try {
|
|
1025
|
+
const changesDir = join5(openspecDir, "changes");
|
|
1026
|
+
const changes = await readdir4(changesDir);
|
|
1027
|
+
changesCount = changes.filter(
|
|
1028
|
+
(f) => !f.startsWith(".") && f !== "archive"
|
|
1029
|
+
).length;
|
|
1030
|
+
} catch {
|
|
1031
|
+
}
|
|
1032
|
+
return {
|
|
1033
|
+
type: "openspec",
|
|
1034
|
+
path: "openspec/",
|
|
1035
|
+
source: "filesystem",
|
|
1036
|
+
status: "active",
|
|
1037
|
+
details: { specs: specsCount, changes: changesCount }
|
|
1038
|
+
};
|
|
1039
|
+
} catch {
|
|
1040
|
+
return null;
|
|
1041
|
+
}
|
|
1042
|
+
}
|
|
1043
|
+
async function detectAtl(dir) {
|
|
1044
|
+
const atlDir = join5(dir, ".atl");
|
|
1045
|
+
try {
|
|
1046
|
+
const s = await stat4(atlDir);
|
|
1047
|
+
if (!s.isDirectory()) return null;
|
|
1048
|
+
let files = [];
|
|
1049
|
+
try {
|
|
1050
|
+
files = await readdir4(atlDir);
|
|
1051
|
+
} catch {
|
|
1052
|
+
}
|
|
1053
|
+
return {
|
|
1054
|
+
type: "atl",
|
|
1055
|
+
path: ".atl/",
|
|
1056
|
+
source: "filesystem",
|
|
1057
|
+
status: "active",
|
|
1058
|
+
details: { files: files.length }
|
|
1059
|
+
};
|
|
1060
|
+
} catch {
|
|
1061
|
+
return null;
|
|
1062
|
+
}
|
|
1063
|
+
}
|
|
1064
|
+
function detectEngramMcp(mcpServers) {
|
|
1065
|
+
const engram = mcpServers.find(
|
|
1066
|
+
(s) => s.name.toLowerCase().includes("engram") || s.config.command && s.config.command.includes("engram") || s.config.args && s.config.args.some((a) => a.toLowerCase().includes("engram"))
|
|
1067
|
+
);
|
|
1068
|
+
if (!engram) return null;
|
|
1069
|
+
return {
|
|
1070
|
+
type: "engram",
|
|
1071
|
+
source: "mcp",
|
|
1072
|
+
status: "configured",
|
|
1073
|
+
details: { server: engram.name }
|
|
1074
|
+
};
|
|
1075
|
+
}
|
|
1076
|
+
async function detectEngramPlugin() {
|
|
1077
|
+
try {
|
|
1078
|
+
const settingsPath = join5(homedir5(), ".claude", "settings.json");
|
|
1079
|
+
const content = await readFile5(settingsPath, "utf-8");
|
|
1080
|
+
const settings = JSON.parse(content);
|
|
1081
|
+
if (!settings.enabledPlugins) return null;
|
|
1082
|
+
const engramKey = Object.keys(settings.enabledPlugins).find(
|
|
1083
|
+
(k) => k.toLowerCase().includes("engram") && settings.enabledPlugins[k]
|
|
1084
|
+
);
|
|
1085
|
+
if (!engramKey) return null;
|
|
1086
|
+
return {
|
|
1087
|
+
type: "engram",
|
|
1088
|
+
source: "plugin",
|
|
1089
|
+
status: "active",
|
|
1090
|
+
details: { plugin: engramKey }
|
|
1091
|
+
};
|
|
1092
|
+
} catch {
|
|
1093
|
+
return null;
|
|
1094
|
+
}
|
|
1095
|
+
}
|
|
1096
|
+
async function detectClaudeMemories(_dir) {
|
|
1097
|
+
const memories = [];
|
|
1098
|
+
const projectsDir = join5(homedir5(), ".claude", "projects");
|
|
1099
|
+
try {
|
|
1100
|
+
const entries = await readdir4(projectsDir);
|
|
1101
|
+
for (const entry of entries) {
|
|
1102
|
+
const memoryPath = join5(projectsDir, entry, "memory", "MEMORY.md");
|
|
1103
|
+
try {
|
|
1104
|
+
const s = await stat4(memoryPath);
|
|
1105
|
+
if (!s.isFile()) continue;
|
|
1106
|
+
const content = await readFile5(memoryPath, "utf-8");
|
|
1107
|
+
const previewLength = Math.min(content.length, 300);
|
|
1108
|
+
const preview = content.slice(0, previewLength);
|
|
1109
|
+
const projectName = entry.replace(/--/g, "/").replace(/-/g, "/");
|
|
1110
|
+
memories.push({
|
|
1111
|
+
type: "claude-memory",
|
|
1112
|
+
path: memoryPath,
|
|
1113
|
+
source: "filesystem",
|
|
1114
|
+
status: "active",
|
|
1115
|
+
details: {
|
|
1116
|
+
project: projectName,
|
|
1117
|
+
size: s.size,
|
|
1118
|
+
preview
|
|
1119
|
+
}
|
|
1120
|
+
});
|
|
1121
|
+
} catch {
|
|
1122
|
+
}
|
|
1123
|
+
}
|
|
1124
|
+
} catch {
|
|
1125
|
+
}
|
|
1126
|
+
return memories;
|
|
1127
|
+
}
|
|
1128
|
+
async function detectAgentMemories() {
|
|
1129
|
+
const memories = [];
|
|
1130
|
+
const agentMemDir = join5(homedir5(), ".claude", "agent-memory");
|
|
1131
|
+
try {
|
|
1132
|
+
const entries = await readdir4(agentMemDir, { withFileTypes: true });
|
|
1133
|
+
for (const entry of entries) {
|
|
1134
|
+
if (entry.isDirectory()) {
|
|
1135
|
+
const memDir = join5(agentMemDir, entry.name);
|
|
1136
|
+
try {
|
|
1137
|
+
const files = await readdir4(memDir);
|
|
1138
|
+
const mdFiles = files.filter((f) => f.endsWith(".md"));
|
|
1139
|
+
if (mdFiles.length > 0) {
|
|
1140
|
+
memories.push({
|
|
1141
|
+
type: "agent-memory",
|
|
1142
|
+
path: memDir,
|
|
1143
|
+
source: "filesystem",
|
|
1144
|
+
status: "active",
|
|
1145
|
+
details: {
|
|
1146
|
+
agent: entry.name,
|
|
1147
|
+
files: mdFiles.length
|
|
1148
|
+
}
|
|
1149
|
+
});
|
|
1150
|
+
}
|
|
1151
|
+
} catch {
|
|
1152
|
+
}
|
|
1153
|
+
} else if (entry.name.endsWith(".md")) {
|
|
1154
|
+
memories.push({
|
|
1155
|
+
type: "agent-memory",
|
|
1156
|
+
path: join5(agentMemDir, entry.name),
|
|
1157
|
+
source: "filesystem",
|
|
1158
|
+
status: "active",
|
|
1159
|
+
details: {
|
|
1160
|
+
agent: basename(entry.name, ".md")
|
|
1161
|
+
}
|
|
1162
|
+
});
|
|
1163
|
+
}
|
|
1164
|
+
}
|
|
1165
|
+
} catch {
|
|
1166
|
+
}
|
|
1167
|
+
return memories;
|
|
1168
|
+
}
|
|
1169
|
+
async function scanMemories(config, mcpServers) {
|
|
1170
|
+
const memories = [];
|
|
1171
|
+
const warnings = [];
|
|
1172
|
+
const openspec = await detectOpenspec(config.dir);
|
|
1173
|
+
if (openspec) memories.push(openspec);
|
|
1174
|
+
const atl = await detectAtl(config.dir);
|
|
1175
|
+
if (atl) memories.push(atl);
|
|
1176
|
+
const engramMcp = detectEngramMcp(mcpServers);
|
|
1177
|
+
if (engramMcp) {
|
|
1178
|
+
memories.push(engramMcp);
|
|
1179
|
+
} else if (config.includeUser) {
|
|
1180
|
+
const engramPlugin = await detectEngramPlugin();
|
|
1181
|
+
if (engramPlugin) memories.push(engramPlugin);
|
|
1182
|
+
}
|
|
1183
|
+
if (config.includeUser) {
|
|
1184
|
+
const claudeMemories = await detectClaudeMemories(config.dir);
|
|
1185
|
+
memories.push(...claudeMemories);
|
|
1186
|
+
const agentMemories = await detectAgentMemories();
|
|
1187
|
+
memories.push(...agentMemories);
|
|
1188
|
+
}
|
|
1189
|
+
return { memories, warnings };
|
|
1190
|
+
}
|
|
1191
|
+
|
|
1192
|
+
// src/scanner/index.ts
|
|
1193
|
+
async function detectProjectName(dir) {
|
|
1194
|
+
try {
|
|
1195
|
+
const pkg = await readFile6(join6(dir, "package.json"), "utf-8");
|
|
1196
|
+
const parsed = JSON.parse(pkg);
|
|
1197
|
+
if (parsed.name) return parsed.name;
|
|
1198
|
+
} catch {
|
|
1199
|
+
}
|
|
1200
|
+
return basename2(dir);
|
|
1201
|
+
}
|
|
1202
|
+
async function runAllScanners(config) {
|
|
1203
|
+
const start = performance.now();
|
|
1204
|
+
const warnings = [];
|
|
1205
|
+
const absDir = resolve(config.dir);
|
|
1206
|
+
const name = await detectProjectName(absDir);
|
|
1207
|
+
const project = {
|
|
1208
|
+
name,
|
|
1209
|
+
path: absDir,
|
|
1210
|
+
scannedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1211
|
+
};
|
|
1212
|
+
const [contextResult, mcpResult, skillsResult, agentsResult] = await Promise.all([
|
|
1213
|
+
scanContextFiles({ ...config, dir: absDir }),
|
|
1214
|
+
scanMcpConfigs({ dir: absDir, includeUser: config.includeUser }),
|
|
1215
|
+
scanSkills({ ...config, dir: absDir }),
|
|
1216
|
+
scanAgents({ ...config, dir: absDir })
|
|
1217
|
+
]);
|
|
1218
|
+
warnings.push(...contextResult.warnings);
|
|
1219
|
+
warnings.push(...mcpResult.warnings);
|
|
1220
|
+
warnings.push(...skillsResult.warnings);
|
|
1221
|
+
warnings.push(...agentsResult.warnings);
|
|
1222
|
+
if (config.introspect && mcpResult.servers.length > 0) {
|
|
1223
|
+
await introspectServers(mcpResult.servers, config.timeout);
|
|
1224
|
+
}
|
|
1225
|
+
const memoriesResult = await scanMemories(
|
|
1226
|
+
{ ...config, dir: absDir },
|
|
1227
|
+
mcpResult.servers
|
|
1228
|
+
);
|
|
1229
|
+
warnings.push(...memoriesResult.warnings);
|
|
1230
|
+
const scanDuration = Math.round(performance.now() - start);
|
|
1231
|
+
return {
|
|
1232
|
+
project,
|
|
1233
|
+
contextFiles: contextResult.files,
|
|
1234
|
+
mcpServers: mcpResult.servers,
|
|
1235
|
+
skills: skillsResult.skills,
|
|
1236
|
+
agents: agentsResult.agents,
|
|
1237
|
+
memories: memoriesResult.memories,
|
|
1238
|
+
warnings,
|
|
1239
|
+
scanDuration
|
|
1240
|
+
};
|
|
1241
|
+
}
|
|
1242
|
+
|
|
1243
|
+
// src/report/styles.ts
|
|
1244
|
+
var CSS_STYLES = `
|
|
1245
|
+
/* \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
|
|
1246
|
+
AI Context Inspector \u2014 Dashboard Styles
|
|
1247
|
+
\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 */
|
|
1248
|
+
|
|
1249
|
+
:root {
|
|
1250
|
+
--bg: #0a0a0f;
|
|
1251
|
+
--bg-alt: #0e0e16;
|
|
1252
|
+
--bg-card: #12121a;
|
|
1253
|
+
--bg-card-hover: #1a1a25;
|
|
1254
|
+
--border: #2a2a3a;
|
|
1255
|
+
--border-hover: #3a3a4a;
|
|
1256
|
+
--text: #e0e0e8;
|
|
1257
|
+
--text-dim: #8888a0;
|
|
1258
|
+
--text-bright: #ffffff;
|
|
1259
|
+
--accent: #00d4ff;
|
|
1260
|
+
--accent-dim: #0099bb;
|
|
1261
|
+
--accent-glow: #00d4ff30;
|
|
1262
|
+
--green: #00e676;
|
|
1263
|
+
--red: #ff5252;
|
|
1264
|
+
--orange: #ffab40;
|
|
1265
|
+
--purple: #b388ff;
|
|
1266
|
+
--pink: #ff80ab;
|
|
1267
|
+
--blue: #4285f4;
|
|
1268
|
+
--font-mono: 'JetBrains Mono', 'Fira Code', 'Cascadia Code', 'Consolas', monospace;
|
|
1269
|
+
--font-sans: system-ui, -apple-system, sans-serif;
|
|
1270
|
+
--radius: 10px;
|
|
1271
|
+
--radius-sm: 6px;
|
|
1272
|
+
--shadow: 0 2px 12px rgba(0,0,0,0.3);
|
|
1273
|
+
--shadow-hover: 0 4px 20px rgba(0,212,255,0.1);
|
|
1274
|
+
--transition: 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
|
1275
|
+
}
|
|
1276
|
+
|
|
1277
|
+
@media (prefers-color-scheme: light) {
|
|
1278
|
+
:root:not([data-theme="dark"]) {
|
|
1279
|
+
--bg: #f5f5f8;
|
|
1280
|
+
--bg-alt: #eeeef3;
|
|
1281
|
+
--bg-card: #ffffff;
|
|
1282
|
+
--bg-card-hover: #f0f0f5;
|
|
1283
|
+
--border: #d0d0dd;
|
|
1284
|
+
--border-hover: #b0b0c0;
|
|
1285
|
+
--text: #2a2a3a;
|
|
1286
|
+
--text-dim: #666680;
|
|
1287
|
+
--text-bright: #000000;
|
|
1288
|
+
--accent: #0088cc;
|
|
1289
|
+
--accent-dim: #006699;
|
|
1290
|
+
--accent-glow: #0088cc20;
|
|
1291
|
+
--shadow: 0 2px 12px rgba(0,0,0,0.08);
|
|
1292
|
+
--shadow-hover: 0 4px 20px rgba(0,136,204,0.1);
|
|
1293
|
+
}
|
|
1294
|
+
}
|
|
1295
|
+
|
|
1296
|
+
[data-theme="light"] {
|
|
1297
|
+
--bg: #f5f5f8;
|
|
1298
|
+
--bg-alt: #eeeef3;
|
|
1299
|
+
--bg-card: #ffffff;
|
|
1300
|
+
--bg-card-hover: #f0f0f5;
|
|
1301
|
+
--border: #d0d0dd;
|
|
1302
|
+
--border-hover: #b0b0c0;
|
|
1303
|
+
--text: #2a2a3a;
|
|
1304
|
+
--text-dim: #666680;
|
|
1305
|
+
--text-bright: #000000;
|
|
1306
|
+
--accent: #0088cc;
|
|
1307
|
+
--accent-dim: #006699;
|
|
1308
|
+
--accent-glow: #0088cc20;
|
|
1309
|
+
--shadow: 0 2px 12px rgba(0,0,0,0.08);
|
|
1310
|
+
--shadow-hover: 0 4px 20px rgba(0,136,204,0.1);
|
|
1311
|
+
}
|
|
1312
|
+
|
|
1313
|
+
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
1314
|
+
|
|
1315
|
+
body {
|
|
1316
|
+
background: var(--bg);
|
|
1317
|
+
color: var(--text);
|
|
1318
|
+
font-family: var(--font-sans);
|
|
1319
|
+
line-height: 1.6;
|
|
1320
|
+
min-height: 100vh;
|
|
1321
|
+
}
|
|
1322
|
+
|
|
1323
|
+
/* \u2500\u2500 Nav Bar \u2500\u2500 */
|
|
1324
|
+
.nav-bar {
|
|
1325
|
+
position: sticky;
|
|
1326
|
+
top: 0;
|
|
1327
|
+
z-index: 100;
|
|
1328
|
+
background: color-mix(in srgb, var(--bg) 85%, transparent);
|
|
1329
|
+
backdrop-filter: blur(12px);
|
|
1330
|
+
-webkit-backdrop-filter: blur(12px);
|
|
1331
|
+
border-bottom: 1px solid var(--border);
|
|
1332
|
+
padding: 0.5rem 2rem;
|
|
1333
|
+
display: flex;
|
|
1334
|
+
align-items: center;
|
|
1335
|
+
justify-content: space-between;
|
|
1336
|
+
gap: 1rem;
|
|
1337
|
+
}
|
|
1338
|
+
|
|
1339
|
+
.nav-links {
|
|
1340
|
+
display: flex;
|
|
1341
|
+
gap: 0.25rem;
|
|
1342
|
+
flex-wrap: wrap;
|
|
1343
|
+
align-items: center;
|
|
1344
|
+
}
|
|
1345
|
+
|
|
1346
|
+
.nav-link {
|
|
1347
|
+
padding: 0.3rem 0.7rem;
|
|
1348
|
+
border-radius: var(--radius-sm);
|
|
1349
|
+
font-size: 0.75rem;
|
|
1350
|
+
font-family: var(--font-mono);
|
|
1351
|
+
color: var(--text-dim);
|
|
1352
|
+
text-decoration: none;
|
|
1353
|
+
cursor: pointer;
|
|
1354
|
+
transition: all var(--transition);
|
|
1355
|
+
border: 1px solid transparent;
|
|
1356
|
+
}
|
|
1357
|
+
|
|
1358
|
+
.nav-link:hover {
|
|
1359
|
+
color: var(--accent);
|
|
1360
|
+
background: var(--accent-glow);
|
|
1361
|
+
border-color: var(--accent);
|
|
1362
|
+
}
|
|
1363
|
+
|
|
1364
|
+
.nav-actions {
|
|
1365
|
+
display: flex;
|
|
1366
|
+
gap: 0.5rem;
|
|
1367
|
+
align-items: center;
|
|
1368
|
+
}
|
|
1369
|
+
|
|
1370
|
+
.nav-btn {
|
|
1371
|
+
background: none;
|
|
1372
|
+
border: 1px solid var(--border);
|
|
1373
|
+
color: var(--text-dim);
|
|
1374
|
+
padding: 0.3rem 0.6rem;
|
|
1375
|
+
border-radius: var(--radius-sm);
|
|
1376
|
+
font-size: 0.8rem;
|
|
1377
|
+
cursor: pointer;
|
|
1378
|
+
transition: all var(--transition);
|
|
1379
|
+
font-family: var(--font-mono);
|
|
1380
|
+
}
|
|
1381
|
+
|
|
1382
|
+
.nav-btn:hover {
|
|
1383
|
+
color: var(--accent);
|
|
1384
|
+
border-color: var(--accent);
|
|
1385
|
+
background: var(--accent-glow);
|
|
1386
|
+
}
|
|
1387
|
+
|
|
1388
|
+
/* \u2500\u2500 Container \u2500\u2500 */
|
|
1389
|
+
.container {
|
|
1390
|
+
max-width: 1200px;
|
|
1391
|
+
margin: 0 auto;
|
|
1392
|
+
padding: 2rem;
|
|
1393
|
+
}
|
|
1394
|
+
|
|
1395
|
+
/* \u2500\u2500 Header \u2500\u2500 */
|
|
1396
|
+
.header {
|
|
1397
|
+
text-align: center;
|
|
1398
|
+
padding: 2.5rem 0 2rem;
|
|
1399
|
+
margin-bottom: 2rem;
|
|
1400
|
+
}
|
|
1401
|
+
|
|
1402
|
+
.header h1 {
|
|
1403
|
+
font-family: var(--font-mono);
|
|
1404
|
+
font-size: 1.8rem;
|
|
1405
|
+
color: var(--accent);
|
|
1406
|
+
margin-bottom: 0.5rem;
|
|
1407
|
+
letter-spacing: -0.02em;
|
|
1408
|
+
}
|
|
1409
|
+
|
|
1410
|
+
.header .subtitle {
|
|
1411
|
+
color: var(--text-dim);
|
|
1412
|
+
font-size: 0.85rem;
|
|
1413
|
+
font-family: var(--font-mono);
|
|
1414
|
+
}
|
|
1415
|
+
|
|
1416
|
+
.badges {
|
|
1417
|
+
display: flex;
|
|
1418
|
+
gap: 0.75rem;
|
|
1419
|
+
justify-content: center;
|
|
1420
|
+
flex-wrap: wrap;
|
|
1421
|
+
margin-top: 1rem;
|
|
1422
|
+
}
|
|
1423
|
+
|
|
1424
|
+
.badge {
|
|
1425
|
+
display: inline-flex;
|
|
1426
|
+
align-items: center;
|
|
1427
|
+
gap: 0.4rem;
|
|
1428
|
+
padding: 0.3rem 0.8rem;
|
|
1429
|
+
border-radius: 20px;
|
|
1430
|
+
font-size: 0.8rem;
|
|
1431
|
+
font-weight: 600;
|
|
1432
|
+
font-family: var(--font-mono);
|
|
1433
|
+
background: var(--bg-card);
|
|
1434
|
+
border: 1px solid var(--border);
|
|
1435
|
+
}
|
|
1436
|
+
|
|
1437
|
+
.badge--accent { border-color: var(--accent); color: var(--accent); }
|
|
1438
|
+
.badge--green { border-color: var(--green); color: var(--green); }
|
|
1439
|
+
.badge--purple { border-color: var(--purple); color: var(--purple); }
|
|
1440
|
+
.badge--orange { border-color: var(--orange); color: var(--orange); }
|
|
1441
|
+
.badge--blue { border-color: var(--blue); color: var(--blue); }
|
|
1442
|
+
.badge--pink { border-color: var(--pink); color: var(--pink); }
|
|
1443
|
+
|
|
1444
|
+
/* \u2500\u2500 Stats Grid \u2500\u2500 */
|
|
1445
|
+
.stats-grid {
|
|
1446
|
+
display: grid;
|
|
1447
|
+
grid-template-columns: repeat(6, 1fr);
|
|
1448
|
+
gap: 1rem;
|
|
1449
|
+
margin: 2rem 0;
|
|
1450
|
+
}
|
|
1451
|
+
|
|
1452
|
+
.stat-card {
|
|
1453
|
+
background: var(--bg-card);
|
|
1454
|
+
border: 1px solid var(--border);
|
|
1455
|
+
border-radius: var(--radius);
|
|
1456
|
+
padding: 1.2rem 1rem;
|
|
1457
|
+
text-align: center;
|
|
1458
|
+
transition: all var(--transition);
|
|
1459
|
+
cursor: default;
|
|
1460
|
+
position: relative;
|
|
1461
|
+
overflow: hidden;
|
|
1462
|
+
}
|
|
1463
|
+
|
|
1464
|
+
.stat-card::before {
|
|
1465
|
+
content: '';
|
|
1466
|
+
position: absolute;
|
|
1467
|
+
top: 0;
|
|
1468
|
+
left: 0;
|
|
1469
|
+
right: 0;
|
|
1470
|
+
height: 3px;
|
|
1471
|
+
background: var(--stat-color, var(--accent));
|
|
1472
|
+
opacity: 0.8;
|
|
1473
|
+
}
|
|
1474
|
+
|
|
1475
|
+
.stat-card:hover {
|
|
1476
|
+
border-color: var(--stat-color, var(--accent));
|
|
1477
|
+
box-shadow: var(--shadow-hover);
|
|
1478
|
+
transform: translateY(-2px);
|
|
1479
|
+
}
|
|
1480
|
+
|
|
1481
|
+
.stat-icon {
|
|
1482
|
+
font-size: 1.5rem;
|
|
1483
|
+
margin-bottom: 0.3rem;
|
|
1484
|
+
display: block;
|
|
1485
|
+
}
|
|
1486
|
+
|
|
1487
|
+
.stat-number {
|
|
1488
|
+
font-size: 2rem;
|
|
1489
|
+
font-weight: 700;
|
|
1490
|
+
font-family: var(--font-mono);
|
|
1491
|
+
color: var(--stat-color, var(--accent));
|
|
1492
|
+
line-height: 1.1;
|
|
1493
|
+
}
|
|
1494
|
+
|
|
1495
|
+
.stat-label {
|
|
1496
|
+
font-size: 0.7rem;
|
|
1497
|
+
color: var(--text-dim);
|
|
1498
|
+
text-transform: uppercase;
|
|
1499
|
+
letter-spacing: 0.08em;
|
|
1500
|
+
font-family: var(--font-mono);
|
|
1501
|
+
margin-top: 0.3rem;
|
|
1502
|
+
}
|
|
1503
|
+
|
|
1504
|
+
/* \u2500\u2500 Ecosystem Map \u2500\u2500 */
|
|
1505
|
+
.ecosystem-map {
|
|
1506
|
+
margin: 2rem 0;
|
|
1507
|
+
border: 1px solid var(--border);
|
|
1508
|
+
border-radius: var(--radius);
|
|
1509
|
+
background: var(--bg-alt);
|
|
1510
|
+
overflow: hidden;
|
|
1511
|
+
position: relative;
|
|
1512
|
+
}
|
|
1513
|
+
|
|
1514
|
+
.ecosystem-svg {
|
|
1515
|
+
width: 100%;
|
|
1516
|
+
height: auto;
|
|
1517
|
+
display: block;
|
|
1518
|
+
}
|
|
1519
|
+
|
|
1520
|
+
.eco-connection {
|
|
1521
|
+
stroke: var(--border);
|
|
1522
|
+
stroke-width: 1.5;
|
|
1523
|
+
fill: none;
|
|
1524
|
+
stroke-dasharray: 6 4;
|
|
1525
|
+
animation: dashFlow 20s linear infinite;
|
|
1526
|
+
}
|
|
1527
|
+
|
|
1528
|
+
.eco-connection--active {
|
|
1529
|
+
stroke: var(--accent);
|
|
1530
|
+
stroke-width: 2;
|
|
1531
|
+
opacity: 0.6;
|
|
1532
|
+
}
|
|
1533
|
+
|
|
1534
|
+
@keyframes dashFlow {
|
|
1535
|
+
to { stroke-dashoffset: -100; }
|
|
1536
|
+
}
|
|
1537
|
+
|
|
1538
|
+
.eco-node {
|
|
1539
|
+
cursor: pointer;
|
|
1540
|
+
transition: transform 0.2s ease;
|
|
1541
|
+
}
|
|
1542
|
+
|
|
1543
|
+
.eco-node:hover {
|
|
1544
|
+
transform: scale(1.1);
|
|
1545
|
+
}
|
|
1546
|
+
|
|
1547
|
+
.eco-node-circle {
|
|
1548
|
+
transition: all 0.2s ease;
|
|
1549
|
+
}
|
|
1550
|
+
|
|
1551
|
+
.eco-node:hover .eco-node-circle {
|
|
1552
|
+
filter: brightness(1.3);
|
|
1553
|
+
}
|
|
1554
|
+
|
|
1555
|
+
.eco-center-circle {
|
|
1556
|
+
filter: drop-shadow(0 0 12px var(--accent-glow));
|
|
1557
|
+
}
|
|
1558
|
+
|
|
1559
|
+
.eco-label {
|
|
1560
|
+
font-family: var(--font-mono);
|
|
1561
|
+
font-size: 11px;
|
|
1562
|
+
fill: var(--text);
|
|
1563
|
+
text-anchor: middle;
|
|
1564
|
+
pointer-events: none;
|
|
1565
|
+
}
|
|
1566
|
+
|
|
1567
|
+
.eco-label--center {
|
|
1568
|
+
font-size: 14px;
|
|
1569
|
+
font-weight: 700;
|
|
1570
|
+
fill: var(--text-bright);
|
|
1571
|
+
}
|
|
1572
|
+
|
|
1573
|
+
.eco-label--count {
|
|
1574
|
+
font-size: 10px;
|
|
1575
|
+
fill: var(--text-dim);
|
|
1576
|
+
}
|
|
1577
|
+
|
|
1578
|
+
.eco-item-circle {
|
|
1579
|
+
opacity: 0.7;
|
|
1580
|
+
transition: opacity 0.2s;
|
|
1581
|
+
}
|
|
1582
|
+
|
|
1583
|
+
.eco-item-circle:hover {
|
|
1584
|
+
opacity: 1;
|
|
1585
|
+
}
|
|
1586
|
+
|
|
1587
|
+
.eco-dimmed {
|
|
1588
|
+
opacity: 0.2;
|
|
1589
|
+
}
|
|
1590
|
+
|
|
1591
|
+
/* \u2500\u2500 Search \u2500\u2500 */
|
|
1592
|
+
.search-bar {
|
|
1593
|
+
position: sticky;
|
|
1594
|
+
top: 45px;
|
|
1595
|
+
z-index: 10;
|
|
1596
|
+
background: color-mix(in srgb, var(--bg) 90%, transparent);
|
|
1597
|
+
backdrop-filter: blur(8px);
|
|
1598
|
+
-webkit-backdrop-filter: blur(8px);
|
|
1599
|
+
padding: 0.75rem 0;
|
|
1600
|
+
margin-bottom: 1.5rem;
|
|
1601
|
+
display: flex;
|
|
1602
|
+
align-items: center;
|
|
1603
|
+
gap: 0.75rem;
|
|
1604
|
+
}
|
|
1605
|
+
|
|
1606
|
+
.search-bar input {
|
|
1607
|
+
flex: 1;
|
|
1608
|
+
padding: 0.65rem 1rem 0.65rem 2.5rem;
|
|
1609
|
+
border: 1px solid var(--border);
|
|
1610
|
+
border-radius: var(--radius);
|
|
1611
|
+
background: var(--bg-card);
|
|
1612
|
+
color: var(--text);
|
|
1613
|
+
font-family: var(--font-mono);
|
|
1614
|
+
font-size: 0.85rem;
|
|
1615
|
+
outline: none;
|
|
1616
|
+
transition: all var(--transition);
|
|
1617
|
+
}
|
|
1618
|
+
|
|
1619
|
+
.search-bar input:focus {
|
|
1620
|
+
border-color: var(--accent);
|
|
1621
|
+
box-shadow: 0 0 0 3px var(--accent-glow);
|
|
1622
|
+
}
|
|
1623
|
+
|
|
1624
|
+
.search-icon {
|
|
1625
|
+
position: absolute;
|
|
1626
|
+
left: 0.85rem;
|
|
1627
|
+
color: var(--text-dim);
|
|
1628
|
+
font-size: 0.9rem;
|
|
1629
|
+
pointer-events: none;
|
|
1630
|
+
}
|
|
1631
|
+
|
|
1632
|
+
.search-bar-inner {
|
|
1633
|
+
position: relative;
|
|
1634
|
+
flex: 1;
|
|
1635
|
+
}
|
|
1636
|
+
|
|
1637
|
+
.search-results-count {
|
|
1638
|
+
font-size: 0.75rem;
|
|
1639
|
+
color: var(--text-dim);
|
|
1640
|
+
font-family: var(--font-mono);
|
|
1641
|
+
white-space: nowrap;
|
|
1642
|
+
}
|
|
1643
|
+
|
|
1644
|
+
.search-kbd {
|
|
1645
|
+
font-size: 0.65rem;
|
|
1646
|
+
color: var(--text-dim);
|
|
1647
|
+
font-family: var(--font-mono);
|
|
1648
|
+
padding: 0.1rem 0.4rem;
|
|
1649
|
+
border: 1px solid var(--border);
|
|
1650
|
+
border-radius: 3px;
|
|
1651
|
+
position: absolute;
|
|
1652
|
+
right: 0.7rem;
|
|
1653
|
+
top: 50%;
|
|
1654
|
+
transform: translateY(-50%);
|
|
1655
|
+
pointer-events: none;
|
|
1656
|
+
opacity: 0.6;
|
|
1657
|
+
}
|
|
1658
|
+
|
|
1659
|
+
/* \u2500\u2500 Sections \u2500\u2500 */
|
|
1660
|
+
.section {
|
|
1661
|
+
margin-bottom: 1.5rem;
|
|
1662
|
+
border: 1px solid var(--border);
|
|
1663
|
+
border-radius: var(--radius);
|
|
1664
|
+
overflow: hidden;
|
|
1665
|
+
box-shadow: var(--shadow);
|
|
1666
|
+
animation: fadeInUp 0.4s ease both;
|
|
1667
|
+
}
|
|
1668
|
+
|
|
1669
|
+
.section-header {
|
|
1670
|
+
display: flex;
|
|
1671
|
+
align-items: center;
|
|
1672
|
+
justify-content: space-between;
|
|
1673
|
+
padding: 0.85rem 1.2rem;
|
|
1674
|
+
background: var(--bg-card);
|
|
1675
|
+
cursor: pointer;
|
|
1676
|
+
user-select: none;
|
|
1677
|
+
transition: background var(--transition);
|
|
1678
|
+
gap: 1rem;
|
|
1679
|
+
}
|
|
1680
|
+
|
|
1681
|
+
.section-header:hover {
|
|
1682
|
+
background: var(--bg-card-hover);
|
|
1683
|
+
}
|
|
1684
|
+
|
|
1685
|
+
.section-header-left {
|
|
1686
|
+
display: flex;
|
|
1687
|
+
align-items: center;
|
|
1688
|
+
gap: 0.6rem;
|
|
1689
|
+
}
|
|
1690
|
+
|
|
1691
|
+
.section-icon {
|
|
1692
|
+
font-size: 1.1rem;
|
|
1693
|
+
}
|
|
1694
|
+
|
|
1695
|
+
.section-header h2 {
|
|
1696
|
+
font-size: 1rem;
|
|
1697
|
+
font-family: var(--font-mono);
|
|
1698
|
+
color: var(--text-bright);
|
|
1699
|
+
}
|
|
1700
|
+
|
|
1701
|
+
.section-header-right {
|
|
1702
|
+
display: flex;
|
|
1703
|
+
align-items: center;
|
|
1704
|
+
gap: 0.75rem;
|
|
1705
|
+
}
|
|
1706
|
+
|
|
1707
|
+
.section-header .count {
|
|
1708
|
+
font-size: 0.8rem;
|
|
1709
|
+
color: var(--text-dim);
|
|
1710
|
+
font-family: var(--font-mono);
|
|
1711
|
+
}
|
|
1712
|
+
|
|
1713
|
+
.section-header .arrow {
|
|
1714
|
+
transition: transform var(--transition);
|
|
1715
|
+
color: var(--text-dim);
|
|
1716
|
+
font-size: 0.7rem;
|
|
1717
|
+
}
|
|
1718
|
+
|
|
1719
|
+
.section-content {
|
|
1720
|
+
max-height: 8000px;
|
|
1721
|
+
overflow: hidden;
|
|
1722
|
+
transition: max-height 0.4s ease, opacity 0.3s ease, padding 0.3s ease;
|
|
1723
|
+
opacity: 1;
|
|
1724
|
+
padding: 1rem 1.2rem;
|
|
1725
|
+
border-top: 1px solid var(--border);
|
|
1726
|
+
}
|
|
1727
|
+
|
|
1728
|
+
.section.collapsed .section-content {
|
|
1729
|
+
max-height: 0;
|
|
1730
|
+
opacity: 0;
|
|
1731
|
+
padding-top: 0;
|
|
1732
|
+
padding-bottom: 0;
|
|
1733
|
+
border-top-color: transparent;
|
|
1734
|
+
}
|
|
1735
|
+
|
|
1736
|
+
.section.collapsed .arrow {
|
|
1737
|
+
transform: rotate(-90deg);
|
|
1738
|
+
}
|
|
1739
|
+
|
|
1740
|
+
/* \u2500\u2500 Cards \u2500\u2500 */
|
|
1741
|
+
.card {
|
|
1742
|
+
background: var(--bg-card);
|
|
1743
|
+
border: 1px solid var(--border);
|
|
1744
|
+
border-radius: var(--radius);
|
|
1745
|
+
padding: 1rem 1.2rem;
|
|
1746
|
+
margin-bottom: 0.75rem;
|
|
1747
|
+
transition: all var(--transition);
|
|
1748
|
+
animation: fadeInUp 0.3s ease both;
|
|
1749
|
+
position: relative;
|
|
1750
|
+
}
|
|
1751
|
+
|
|
1752
|
+
.card:hover {
|
|
1753
|
+
border-color: var(--border-hover);
|
|
1754
|
+
box-shadow: var(--shadow-hover);
|
|
1755
|
+
}
|
|
1756
|
+
|
|
1757
|
+
.card:last-child {
|
|
1758
|
+
margin-bottom: 0;
|
|
1759
|
+
}
|
|
1760
|
+
|
|
1761
|
+
.card-title {
|
|
1762
|
+
font-family: var(--font-mono);
|
|
1763
|
+
font-size: 0.9rem;
|
|
1764
|
+
color: var(--accent);
|
|
1765
|
+
margin-bottom: 0.5rem;
|
|
1766
|
+
display: flex;
|
|
1767
|
+
align-items: center;
|
|
1768
|
+
gap: 0.5rem;
|
|
1769
|
+
flex-wrap: wrap;
|
|
1770
|
+
}
|
|
1771
|
+
|
|
1772
|
+
.card-meta {
|
|
1773
|
+
font-size: 0.75rem;
|
|
1774
|
+
color: var(--text-dim);
|
|
1775
|
+
margin-bottom: 0.5rem;
|
|
1776
|
+
}
|
|
1777
|
+
|
|
1778
|
+
/* \u2500\u2500 Copy Button \u2500\u2500 */
|
|
1779
|
+
.copy-btn {
|
|
1780
|
+
background: none;
|
|
1781
|
+
border: 1px solid var(--border);
|
|
1782
|
+
color: var(--text-dim);
|
|
1783
|
+
font-size: 0.7rem;
|
|
1784
|
+
padding: 0.15rem 0.4rem;
|
|
1785
|
+
border-radius: 3px;
|
|
1786
|
+
cursor: pointer;
|
|
1787
|
+
transition: all var(--transition);
|
|
1788
|
+
font-family: var(--font-mono);
|
|
1789
|
+
opacity: 0;
|
|
1790
|
+
position: absolute;
|
|
1791
|
+
top: 0.75rem;
|
|
1792
|
+
right: 0.75rem;
|
|
1793
|
+
}
|
|
1794
|
+
|
|
1795
|
+
.card:hover .copy-btn {
|
|
1796
|
+
opacity: 1;
|
|
1797
|
+
}
|
|
1798
|
+
|
|
1799
|
+
.copy-btn:hover {
|
|
1800
|
+
color: var(--accent);
|
|
1801
|
+
border-color: var(--accent);
|
|
1802
|
+
}
|
|
1803
|
+
|
|
1804
|
+
.copy-btn--copied {
|
|
1805
|
+
color: var(--green) !important;
|
|
1806
|
+
border-color: var(--green) !important;
|
|
1807
|
+
}
|
|
1808
|
+
|
|
1809
|
+
/* \u2500\u2500 Size Bar \u2500\u2500 */
|
|
1810
|
+
.size-bar {
|
|
1811
|
+
height: 3px;
|
|
1812
|
+
background: var(--border);
|
|
1813
|
+
border-radius: 2px;
|
|
1814
|
+
margin-top: 0.3rem;
|
|
1815
|
+
overflow: hidden;
|
|
1816
|
+
}
|
|
1817
|
+
|
|
1818
|
+
.size-bar-fill {
|
|
1819
|
+
height: 100%;
|
|
1820
|
+
border-radius: 2px;
|
|
1821
|
+
background: linear-gradient(90deg, var(--accent-dim), var(--accent));
|
|
1822
|
+
transition: width 0.6s ease;
|
|
1823
|
+
}
|
|
1824
|
+
|
|
1825
|
+
/* \u2500\u2500 Tool badges \u2500\u2500 */
|
|
1826
|
+
.tool-badge {
|
|
1827
|
+
display: inline-block;
|
|
1828
|
+
padding: 0.15rem 0.5rem;
|
|
1829
|
+
border-radius: 4px;
|
|
1830
|
+
font-size: 0.7rem;
|
|
1831
|
+
font-weight: 600;
|
|
1832
|
+
text-transform: uppercase;
|
|
1833
|
+
letter-spacing: 0.05em;
|
|
1834
|
+
}
|
|
1835
|
+
|
|
1836
|
+
.tool-badge--claude { background: #d4760020; color: #d47600; border: 1px solid #d4760040; }
|
|
1837
|
+
.tool-badge--cursor { background: #00a2ff20; color: #00a2ff; border: 1px solid #00a2ff40; }
|
|
1838
|
+
.tool-badge--windsurf { background: #00c48020; color: #00c480; border: 1px solid #00c48040; }
|
|
1839
|
+
.tool-badge--copilot { background: #8b5cf620; color: #8b5cf6; border: 1px solid #8b5cf640; }
|
|
1840
|
+
.tool-badge--gemini { background: #4285f420; color: #4285f4; border: 1px solid #4285f440; }
|
|
1841
|
+
.tool-badge--codex { background: #10a37f20; color: #10a37f; border: 1px solid #10a37f40; }
|
|
1842
|
+
.tool-badge--aider { background: #ff6b6b20; color: #ff6b6b; border: 1px solid #ff6b6b40; }
|
|
1843
|
+
.tool-badge--cline { background: #e91e6320; color: #e91e63; border: 1px solid #e91e6340; }
|
|
1844
|
+
.tool-badge--continue { background: #ff980020; color: #ff9800; border: 1px solid #ff980040; }
|
|
1845
|
+
.tool-badge--amazonq { background: #ff990020; color: #ff9900; border: 1px solid #ff990040; }
|
|
1846
|
+
.tool-badge--augment { background: #9c27b020; color: #9c27b0; border: 1px solid #9c27b040; }
|
|
1847
|
+
.tool-badge--replit { background: #f2620020; color: #f26200; border: 1px solid #f2620040; }
|
|
1848
|
+
.tool-badge--firebase { background: #ffca2820; color: #ffca28; border: 1px solid #ffca2840; }
|
|
1849
|
+
.tool-badge--opencode { background: #22c55e20; color: #22c55e; border: 1px solid #22c55e40; }
|
|
1850
|
+
.tool-badge--roo { background: #06b6d420; color: #06b6d4; border: 1px solid #06b6d440; }
|
|
1851
|
+
.tool-badge--tabnine { background: #e8596820; color: #e85968; border: 1px solid #e8596840; }
|
|
1852
|
+
.tool-badge--sourcegraph { background: #a112ff20; color: #a112ff; border: 1px solid #a112ff40; }
|
|
1853
|
+
.tool-badge--vscode { background: #007acc20; color: #007acc; border: 1px solid #007acc40; }
|
|
1854
|
+
.tool-badge--universal { background: #78909c20; color: #78909c; border: 1px solid #78909c40; }
|
|
1855
|
+
|
|
1856
|
+
/* \u2500\u2500 Status \u2500\u2500 */
|
|
1857
|
+
.status {
|
|
1858
|
+
display: inline-block;
|
|
1859
|
+
padding: 0.15rem 0.5rem;
|
|
1860
|
+
border-radius: 4px;
|
|
1861
|
+
font-size: 0.7rem;
|
|
1862
|
+
font-weight: 600;
|
|
1863
|
+
}
|
|
1864
|
+
|
|
1865
|
+
.status--ok { background: #00e67620; color: var(--green); }
|
|
1866
|
+
.status--timeout { background: #ffab4020; color: var(--orange); }
|
|
1867
|
+
.status--error { background: #ff525220; color: var(--red); }
|
|
1868
|
+
.status--active { background: #00e67620; color: var(--green); }
|
|
1869
|
+
.status--configured { background: #00d4ff20; color: var(--accent); }
|
|
1870
|
+
.status--detected { background: #b388ff20; color: var(--purple); }
|
|
1871
|
+
|
|
1872
|
+
/* \u2500\u2500 Tool list \u2500\u2500 */
|
|
1873
|
+
.tool-list {
|
|
1874
|
+
list-style: none;
|
|
1875
|
+
margin-top: 0.5rem;
|
|
1876
|
+
}
|
|
1877
|
+
|
|
1878
|
+
.tool-list li {
|
|
1879
|
+
padding: 0.35rem 0;
|
|
1880
|
+
font-size: 0.8rem;
|
|
1881
|
+
border-bottom: 1px solid var(--border);
|
|
1882
|
+
display: flex;
|
|
1883
|
+
gap: 0.5rem;
|
|
1884
|
+
align-items: baseline;
|
|
1885
|
+
}
|
|
1886
|
+
|
|
1887
|
+
.tool-list li:last-child {
|
|
1888
|
+
border-bottom: none;
|
|
1889
|
+
}
|
|
1890
|
+
|
|
1891
|
+
.tool-name {
|
|
1892
|
+
font-family: var(--font-mono);
|
|
1893
|
+
color: var(--accent);
|
|
1894
|
+
white-space: nowrap;
|
|
1895
|
+
font-size: 0.8rem;
|
|
1896
|
+
}
|
|
1897
|
+
|
|
1898
|
+
.tool-desc {
|
|
1899
|
+
color: var(--text-dim);
|
|
1900
|
+
overflow: hidden;
|
|
1901
|
+
text-overflow: ellipsis;
|
|
1902
|
+
font-size: 0.78rem;
|
|
1903
|
+
}
|
|
1904
|
+
|
|
1905
|
+
/* \u2500\u2500 Preview \u2500\u2500 */
|
|
1906
|
+
.preview {
|
|
1907
|
+
background: var(--bg);
|
|
1908
|
+
border: 1px solid var(--border);
|
|
1909
|
+
border-radius: var(--radius-sm);
|
|
1910
|
+
padding: 0.75rem;
|
|
1911
|
+
margin-top: 0.5rem;
|
|
1912
|
+
font-family: var(--font-mono);
|
|
1913
|
+
font-size: 0.73rem;
|
|
1914
|
+
color: var(--text-dim);
|
|
1915
|
+
white-space: pre-wrap;
|
|
1916
|
+
word-break: break-all;
|
|
1917
|
+
max-height: 200px;
|
|
1918
|
+
overflow-y: auto;
|
|
1919
|
+
display: none;
|
|
1920
|
+
line-height: 1.5;
|
|
1921
|
+
}
|
|
1922
|
+
|
|
1923
|
+
.preview.open {
|
|
1924
|
+
display: block;
|
|
1925
|
+
animation: fadeIn 0.2s ease;
|
|
1926
|
+
}
|
|
1927
|
+
|
|
1928
|
+
.preview-toggle {
|
|
1929
|
+
background: none;
|
|
1930
|
+
border: none;
|
|
1931
|
+
color: var(--accent);
|
|
1932
|
+
font-size: 0.75rem;
|
|
1933
|
+
cursor: pointer;
|
|
1934
|
+
font-family: var(--font-mono);
|
|
1935
|
+
padding: 0;
|
|
1936
|
+
transition: color var(--transition);
|
|
1937
|
+
}
|
|
1938
|
+
|
|
1939
|
+
.preview-toggle:hover {
|
|
1940
|
+
color: var(--accent-dim);
|
|
1941
|
+
text-decoration: underline;
|
|
1942
|
+
}
|
|
1943
|
+
|
|
1944
|
+
/* \u2500\u2500 Groups \u2500\u2500 */
|
|
1945
|
+
.tool-group {
|
|
1946
|
+
margin-bottom: 1.2rem;
|
|
1947
|
+
}
|
|
1948
|
+
|
|
1949
|
+
.tool-group:last-child {
|
|
1950
|
+
margin-bottom: 0;
|
|
1951
|
+
}
|
|
1952
|
+
|
|
1953
|
+
.tool-group-header {
|
|
1954
|
+
display: flex;
|
|
1955
|
+
align-items: center;
|
|
1956
|
+
gap: 0.5rem;
|
|
1957
|
+
margin-bottom: 0.6rem;
|
|
1958
|
+
padding-bottom: 0.4rem;
|
|
1959
|
+
border-bottom: 1px solid var(--border);
|
|
1960
|
+
}
|
|
1961
|
+
|
|
1962
|
+
/* \u2500\u2500 Empty state \u2500\u2500 */
|
|
1963
|
+
.empty-state {
|
|
1964
|
+
text-align: center;
|
|
1965
|
+
padding: 4rem 2rem;
|
|
1966
|
+
color: var(--text-dim);
|
|
1967
|
+
}
|
|
1968
|
+
|
|
1969
|
+
.empty-state-icon {
|
|
1970
|
+
font-size: 3rem;
|
|
1971
|
+
margin-bottom: 1rem;
|
|
1972
|
+
display: block;
|
|
1973
|
+
opacity: 0.5;
|
|
1974
|
+
}
|
|
1975
|
+
|
|
1976
|
+
.empty-state h3 {
|
|
1977
|
+
font-size: 1.2rem;
|
|
1978
|
+
margin-bottom: 0.5rem;
|
|
1979
|
+
color: var(--text);
|
|
1980
|
+
font-family: var(--font-mono);
|
|
1981
|
+
}
|
|
1982
|
+
|
|
1983
|
+
/* \u2500\u2500 Scope badge \u2500\u2500 */
|
|
1984
|
+
.scope-badge {
|
|
1985
|
+
font-size: 0.65rem;
|
|
1986
|
+
padding: 0.1rem 0.4rem;
|
|
1987
|
+
border-radius: 3px;
|
|
1988
|
+
font-weight: 600;
|
|
1989
|
+
text-transform: uppercase;
|
|
1990
|
+
letter-spacing: 0.03em;
|
|
1991
|
+
}
|
|
1992
|
+
|
|
1993
|
+
.scope-badge--project { background: var(--accent); color: var(--bg); }
|
|
1994
|
+
.scope-badge--user { background: var(--purple); color: var(--bg); }
|
|
1995
|
+
.scope-badge--vscode { background: #007acc; color: #fff; }
|
|
1996
|
+
.scope-badge--desktop { background: var(--orange); color: var(--bg); }
|
|
1997
|
+
|
|
1998
|
+
/* \u2500\u2500 Warnings \u2500\u2500 */
|
|
1999
|
+
.warning-card {
|
|
2000
|
+
background: var(--bg-card);
|
|
2001
|
+
border: 1px solid var(--orange);
|
|
2002
|
+
border-left: 4px solid var(--orange);
|
|
2003
|
+
border-radius: var(--radius);
|
|
2004
|
+
padding: 0.8rem 1rem;
|
|
2005
|
+
margin-bottom: 0.5rem;
|
|
2006
|
+
font-size: 0.8rem;
|
|
2007
|
+
}
|
|
2008
|
+
|
|
2009
|
+
.warning-card:last-child { margin-bottom: 0; }
|
|
2010
|
+
|
|
2011
|
+
.warning-scanner {
|
|
2012
|
+
font-family: var(--font-mono);
|
|
2013
|
+
font-size: 0.7rem;
|
|
2014
|
+
color: var(--orange);
|
|
2015
|
+
margin-bottom: 0.2rem;
|
|
2016
|
+
}
|
|
2017
|
+
|
|
2018
|
+
/* \u2500\u2500 Footer \u2500\u2500 */
|
|
2019
|
+
.footer {
|
|
2020
|
+
text-align: center;
|
|
2021
|
+
padding: 2rem 0;
|
|
2022
|
+
margin-top: 2rem;
|
|
2023
|
+
border-top: 1px solid var(--border);
|
|
2024
|
+
font-size: 0.75rem;
|
|
2025
|
+
color: var(--text-dim);
|
|
2026
|
+
font-family: var(--font-mono);
|
|
2027
|
+
}
|
|
2028
|
+
|
|
2029
|
+
.footer a {
|
|
2030
|
+
color: var(--accent);
|
|
2031
|
+
text-decoration: none;
|
|
2032
|
+
}
|
|
2033
|
+
|
|
2034
|
+
.footer a:hover {
|
|
2035
|
+
text-decoration: underline;
|
|
2036
|
+
}
|
|
2037
|
+
|
|
2038
|
+
/* \u2500\u2500 Animations \u2500\u2500 */
|
|
2039
|
+
@keyframes fadeInUp {
|
|
2040
|
+
from {
|
|
2041
|
+
opacity: 0;
|
|
2042
|
+
transform: translateY(12px);
|
|
2043
|
+
}
|
|
2044
|
+
to {
|
|
2045
|
+
opacity: 1;
|
|
2046
|
+
transform: translateY(0);
|
|
2047
|
+
}
|
|
2048
|
+
}
|
|
2049
|
+
|
|
2050
|
+
@keyframes fadeIn {
|
|
2051
|
+
from { opacity: 0; }
|
|
2052
|
+
to { opacity: 1; }
|
|
2053
|
+
}
|
|
2054
|
+
|
|
2055
|
+
@keyframes pulse {
|
|
2056
|
+
0%, 100% { opacity: 0.6; }
|
|
2057
|
+
50% { opacity: 1; }
|
|
2058
|
+
}
|
|
2059
|
+
|
|
2060
|
+
/* \u2500\u2500 Responsive \u2500\u2500 */
|
|
2061
|
+
@media (max-width: 1024px) {
|
|
2062
|
+
.stats-grid {
|
|
2063
|
+
grid-template-columns: repeat(3, 1fr);
|
|
2064
|
+
}
|
|
2065
|
+
}
|
|
2066
|
+
|
|
2067
|
+
@media (max-width: 768px) {
|
|
2068
|
+
.container { padding: 1rem; }
|
|
2069
|
+
.nav-bar { padding: 0.5rem 1rem; flex-wrap: wrap; }
|
|
2070
|
+
.nav-links { display: none; }
|
|
2071
|
+
.badges { gap: 0.5rem; }
|
|
2072
|
+
.badge { font-size: 0.7rem; padding: 0.2rem 0.6rem; }
|
|
2073
|
+
.stats-grid { grid-template-columns: repeat(2, 1fr); gap: 0.6rem; }
|
|
2074
|
+
.stat-number { font-size: 1.5rem; }
|
|
2075
|
+
.header h1 { font-size: 1.3rem; }
|
|
2076
|
+
.ecosystem-map { display: none; }
|
|
2077
|
+
.section-content { padding: 0.75rem; }
|
|
2078
|
+
}
|
|
2079
|
+
|
|
2080
|
+
@media (max-width: 480px) {
|
|
2081
|
+
.stats-grid { grid-template-columns: repeat(2, 1fr); }
|
|
2082
|
+
.nav-actions { gap: 0.3rem; }
|
|
2083
|
+
}
|
|
2084
|
+
|
|
2085
|
+
/* \u2500\u2500 Print \u2500\u2500 */
|
|
2086
|
+
@media print {
|
|
2087
|
+
.nav-bar, .search-bar, .ecosystem-map, .nav-btn, .copy-btn, .preview-toggle { display: none !important; }
|
|
2088
|
+
body { background: #fff; color: #000; }
|
|
2089
|
+
.container { max-width: 100%; padding: 1rem; }
|
|
2090
|
+
.card, .section { break-inside: avoid; box-shadow: none; border-color: #ccc; }
|
|
2091
|
+
.section-content { max-height: none !important; opacity: 1 !important; }
|
|
2092
|
+
.header h1 { color: #000; }
|
|
2093
|
+
.stat-card { border-color: #ccc; }
|
|
2094
|
+
.stat-number { color: #333; }
|
|
2095
|
+
}
|
|
2096
|
+
`;
|
|
2097
|
+
|
|
2098
|
+
// src/report/scripts.ts
|
|
2099
|
+
var JS_SCRIPTS = `
|
|
2100
|
+
document.addEventListener('DOMContentLoaded', () => {
|
|
2101
|
+
|
|
2102
|
+
// \u2500\u2500 Animated Counters \u2500\u2500
|
|
2103
|
+
document.querySelectorAll('.stat-number').forEach(el => {
|
|
2104
|
+
const target = parseInt(el.getAttribute('data-target') || '0')
|
|
2105
|
+
if (target === 0) { el.textContent = '0'; return }
|
|
2106
|
+
let current = 0
|
|
2107
|
+
const step = Math.max(1, Math.ceil(target / 25))
|
|
2108
|
+
const interval = setInterval(() => {
|
|
2109
|
+
current = Math.min(current + step, target)
|
|
2110
|
+
el.textContent = current
|
|
2111
|
+
if (current >= target) clearInterval(interval)
|
|
2112
|
+
}, 35)
|
|
2113
|
+
})
|
|
2114
|
+
|
|
2115
|
+
// \u2500\u2500 Section Collapse/Expand \u2500\u2500
|
|
2116
|
+
document.querySelectorAll('.section-header').forEach(header => {
|
|
2117
|
+
header.addEventListener('click', () => {
|
|
2118
|
+
const section = header.closest('.section')
|
|
2119
|
+
if (!section) return
|
|
2120
|
+
const content = section.querySelector('.section-content')
|
|
2121
|
+
if (!content) return
|
|
2122
|
+
|
|
2123
|
+
if (section.classList.contains('collapsed')) {
|
|
2124
|
+
// Expand
|
|
2125
|
+
section.classList.remove('collapsed')
|
|
2126
|
+
content.style.maxHeight = content.scrollHeight + 'px'
|
|
2127
|
+
setTimeout(() => { content.style.maxHeight = '' }, 400)
|
|
2128
|
+
} else {
|
|
2129
|
+
// Collapse
|
|
2130
|
+
content.style.maxHeight = content.scrollHeight + 'px'
|
|
2131
|
+
requestAnimationFrame(() => {
|
|
2132
|
+
content.style.maxHeight = '0px'
|
|
2133
|
+
section.classList.add('collapsed')
|
|
2134
|
+
})
|
|
2135
|
+
}
|
|
2136
|
+
})
|
|
2137
|
+
})
|
|
2138
|
+
|
|
2139
|
+
// \u2500\u2500 Preview Toggle \u2500\u2500
|
|
2140
|
+
document.querySelectorAll('.preview-toggle').forEach(btn => {
|
|
2141
|
+
btn.addEventListener('click', (e) => {
|
|
2142
|
+
e.stopPropagation()
|
|
2143
|
+
const preview = btn.nextElementSibling
|
|
2144
|
+
if (preview && preview.classList.contains('preview')) {
|
|
2145
|
+
preview.classList.toggle('open')
|
|
2146
|
+
btn.textContent = preview.classList.contains('open') ? 'Ocultar' : 'Ver contenido'
|
|
2147
|
+
}
|
|
2148
|
+
})
|
|
2149
|
+
})
|
|
2150
|
+
|
|
2151
|
+
// \u2500\u2500 Global Search \u2500\u2500
|
|
2152
|
+
const searchInput = document.getElementById('search-input')
|
|
2153
|
+
const resultsCount = document.querySelector('.search-results-count')
|
|
2154
|
+
const searchKbd = document.querySelector('.search-kbd')
|
|
2155
|
+
|
|
2156
|
+
if (searchInput) {
|
|
2157
|
+
searchInput.addEventListener('input', (e) => {
|
|
2158
|
+
const query = e.target.value.toLowerCase().trim()
|
|
2159
|
+
let visible = 0
|
|
2160
|
+
let total = 0
|
|
2161
|
+
|
|
2162
|
+
document.querySelectorAll('[data-searchable]').forEach(el => {
|
|
2163
|
+
const text = el.getAttribute('data-searchable').toLowerCase()
|
|
2164
|
+
const match = !query || text.includes(query)
|
|
2165
|
+
el.style.display = match ? '' : 'none'
|
|
2166
|
+
total++
|
|
2167
|
+
if (match) visible++
|
|
2168
|
+
})
|
|
2169
|
+
|
|
2170
|
+
if (resultsCount) {
|
|
2171
|
+
resultsCount.textContent = query ? visible + ' / ' + total : ''
|
|
2172
|
+
}
|
|
2173
|
+
if (searchKbd) {
|
|
2174
|
+
searchKbd.style.display = query ? 'none' : ''
|
|
2175
|
+
}
|
|
2176
|
+
})
|
|
2177
|
+
|
|
2178
|
+
searchInput.addEventListener('focus', () => {
|
|
2179
|
+
if (searchKbd) searchKbd.style.display = 'none'
|
|
2180
|
+
})
|
|
2181
|
+
|
|
2182
|
+
searchInput.addEventListener('blur', () => {
|
|
2183
|
+
if (searchKbd && !searchInput.value) searchKbd.style.display = ''
|
|
2184
|
+
})
|
|
2185
|
+
}
|
|
2186
|
+
|
|
2187
|
+
// \u2500\u2500 Keyboard Shortcuts \u2500\u2500
|
|
2188
|
+
document.addEventListener('keydown', (e) => {
|
|
2189
|
+
// Ignore when typing in input
|
|
2190
|
+
if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') {
|
|
2191
|
+
if (e.key === 'Escape') {
|
|
2192
|
+
e.target.value = ''
|
|
2193
|
+
e.target.dispatchEvent(new Event('input'))
|
|
2194
|
+
e.target.blur()
|
|
2195
|
+
}
|
|
2196
|
+
return
|
|
2197
|
+
}
|
|
2198
|
+
|
|
2199
|
+
if (e.key === '/') {
|
|
2200
|
+
e.preventDefault()
|
|
2201
|
+
if (searchInput) searchInput.focus()
|
|
2202
|
+
}
|
|
2203
|
+
})
|
|
2204
|
+
|
|
2205
|
+
// \u2500\u2500 Nav Links (scroll to section) \u2500\u2500
|
|
2206
|
+
document.querySelectorAll('.nav-link[data-target]').forEach(link => {
|
|
2207
|
+
link.addEventListener('click', (e) => {
|
|
2208
|
+
e.preventDefault()
|
|
2209
|
+
const targetId = link.getAttribute('data-target')
|
|
2210
|
+
const target = document.getElementById(targetId)
|
|
2211
|
+
if (target) {
|
|
2212
|
+
// Expand if collapsed
|
|
2213
|
+
if (target.classList.contains('collapsed')) {
|
|
2214
|
+
target.classList.remove('collapsed')
|
|
2215
|
+
const content = target.querySelector('.section-content')
|
|
2216
|
+
if (content) {
|
|
2217
|
+
content.style.maxHeight = content.scrollHeight + 'px'
|
|
2218
|
+
setTimeout(() => { content.style.maxHeight = '' }, 400)
|
|
2219
|
+
}
|
|
2220
|
+
}
|
|
2221
|
+
target.scrollIntoView({ behavior: 'smooth', block: 'start' })
|
|
2222
|
+
}
|
|
2223
|
+
})
|
|
2224
|
+
})
|
|
2225
|
+
|
|
2226
|
+
// \u2500\u2500 Ecosystem Map (click to scroll) \u2500\u2500
|
|
2227
|
+
document.querySelectorAll('.eco-node[data-section]').forEach(node => {
|
|
2228
|
+
node.addEventListener('click', () => {
|
|
2229
|
+
const targetId = node.getAttribute('data-section')
|
|
2230
|
+
const target = document.getElementById(targetId)
|
|
2231
|
+
if (target) {
|
|
2232
|
+
if (target.classList.contains('collapsed')) {
|
|
2233
|
+
target.classList.remove('collapsed')
|
|
2234
|
+
const content = target.querySelector('.section-content')
|
|
2235
|
+
if (content) {
|
|
2236
|
+
content.style.maxHeight = content.scrollHeight + 'px'
|
|
2237
|
+
setTimeout(() => { content.style.maxHeight = '' }, 400)
|
|
2238
|
+
}
|
|
2239
|
+
}
|
|
2240
|
+
target.scrollIntoView({ behavior: 'smooth', block: 'start' })
|
|
2241
|
+
}
|
|
2242
|
+
})
|
|
2243
|
+
})
|
|
2244
|
+
|
|
2245
|
+
// \u2500\u2500 Theme Toggle \u2500\u2500
|
|
2246
|
+
const themeToggle = document.getElementById('theme-toggle')
|
|
2247
|
+
if (themeToggle) {
|
|
2248
|
+
themeToggle.addEventListener('click', () => {
|
|
2249
|
+
const html = document.documentElement
|
|
2250
|
+
const current = html.getAttribute('data-theme')
|
|
2251
|
+
const next = current === 'light' ? 'dark' : 'light'
|
|
2252
|
+
html.setAttribute('data-theme', next)
|
|
2253
|
+
themeToggle.innerHTML = next === 'light' ? '☀' : '☾'
|
|
2254
|
+
})
|
|
2255
|
+
}
|
|
2256
|
+
|
|
2257
|
+
// \u2500\u2500 Export JSON \u2500\u2500
|
|
2258
|
+
const exportBtn = document.getElementById('export-btn')
|
|
2259
|
+
if (exportBtn) {
|
|
2260
|
+
exportBtn.addEventListener('click', () => {
|
|
2261
|
+
const dataEl = document.getElementById('scan-data')
|
|
2262
|
+
if (!dataEl) return
|
|
2263
|
+
const text = dataEl.textContent || ''
|
|
2264
|
+
const fallback = () => {
|
|
2265
|
+
const ta = document.createElement('textarea')
|
|
2266
|
+
ta.value = text
|
|
2267
|
+
ta.style.position = 'fixed'
|
|
2268
|
+
ta.style.opacity = '0'
|
|
2269
|
+
document.body.appendChild(ta)
|
|
2270
|
+
ta.select()
|
|
2271
|
+
document.execCommand('copy')
|
|
2272
|
+
document.body.removeChild(ta)
|
|
2273
|
+
}
|
|
2274
|
+
if (navigator.clipboard && navigator.clipboard.writeText) {
|
|
2275
|
+
navigator.clipboard.writeText(text).catch(fallback)
|
|
2276
|
+
} else {
|
|
2277
|
+
fallback()
|
|
2278
|
+
}
|
|
2279
|
+
const orig = exportBtn.innerHTML
|
|
2280
|
+
exportBtn.textContent = '\\u2713'
|
|
2281
|
+
setTimeout(() => { exportBtn.innerHTML = orig }, 1500)
|
|
2282
|
+
})
|
|
2283
|
+
}
|
|
2284
|
+
|
|
2285
|
+
// \u2500\u2500 Copy Buttons \u2500\u2500
|
|
2286
|
+
document.querySelectorAll('.copy-btn').forEach(btn => {
|
|
2287
|
+
btn.addEventListener('click', (e) => {
|
|
2288
|
+
e.stopPropagation()
|
|
2289
|
+
const text = btn.getAttribute('data-copy') || ''
|
|
2290
|
+
const fallback = () => {
|
|
2291
|
+
const ta = document.createElement('textarea')
|
|
2292
|
+
ta.value = text
|
|
2293
|
+
ta.style.position = 'fixed'
|
|
2294
|
+
ta.style.opacity = '0'
|
|
2295
|
+
document.body.appendChild(ta)
|
|
2296
|
+
ta.select()
|
|
2297
|
+
document.execCommand('copy')
|
|
2298
|
+
document.body.removeChild(ta)
|
|
2299
|
+
}
|
|
2300
|
+
if (navigator.clipboard && navigator.clipboard.writeText) {
|
|
2301
|
+
navigator.clipboard.writeText(text).catch(fallback)
|
|
2302
|
+
} else {
|
|
2303
|
+
fallback()
|
|
2304
|
+
}
|
|
2305
|
+
btn.classList.add('copy-btn--copied')
|
|
2306
|
+
btn.textContent = '\\u2713'
|
|
2307
|
+
setTimeout(() => {
|
|
2308
|
+
btn.classList.remove('copy-btn--copied')
|
|
2309
|
+
btn.textContent = 'copiar'
|
|
2310
|
+
}, 1500)
|
|
2311
|
+
})
|
|
2312
|
+
})
|
|
2313
|
+
|
|
2314
|
+
})
|
|
2315
|
+
`;
|
|
2316
|
+
|
|
2317
|
+
// src/report/ecosystem-map.ts
|
|
2318
|
+
function esc(str) {
|
|
2319
|
+
return str.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
2320
|
+
}
|
|
2321
|
+
function buildCategories(result, summary) {
|
|
2322
|
+
return [
|
|
2323
|
+
{
|
|
2324
|
+
id: "section-mcp",
|
|
2325
|
+
label: "MCP",
|
|
2326
|
+
icon: "\u2699",
|
|
2327
|
+
count: summary.totalMcpServers,
|
|
2328
|
+
color: "#00d4ff",
|
|
2329
|
+
items: result.mcpServers.slice(0, 8).map((s) => ({
|
|
2330
|
+
name: s.name,
|
|
2331
|
+
detail: `${s.introspection?.tools.length ?? 0} tools`
|
|
2332
|
+
}))
|
|
2333
|
+
},
|
|
2334
|
+
{
|
|
2335
|
+
id: "section-context",
|
|
2336
|
+
label: "Contexto",
|
|
2337
|
+
icon: "\u{1F4C4}",
|
|
2338
|
+
count: summary.totalFiles,
|
|
2339
|
+
color: "#b388ff",
|
|
2340
|
+
items: result.contextFiles.slice(0, 8).map((f) => ({
|
|
2341
|
+
name: f.path,
|
|
2342
|
+
detail: f.tool
|
|
2343
|
+
}))
|
|
2344
|
+
},
|
|
2345
|
+
{
|
|
2346
|
+
id: "section-skills",
|
|
2347
|
+
label: "Skills",
|
|
2348
|
+
icon: "\u26A1",
|
|
2349
|
+
count: summary.totalSkills,
|
|
2350
|
+
color: "#ffab40",
|
|
2351
|
+
items: result.skills.slice(0, 8).map((s) => ({
|
|
2352
|
+
name: s.name
|
|
2353
|
+
}))
|
|
2354
|
+
},
|
|
2355
|
+
{
|
|
2356
|
+
id: "section-agents",
|
|
2357
|
+
label: "Agents",
|
|
2358
|
+
icon: "\u{1F916}",
|
|
2359
|
+
count: summary.totalAgents,
|
|
2360
|
+
color: "#00e676",
|
|
2361
|
+
items: result.agents.slice(0, 8).map((a) => ({
|
|
2362
|
+
name: a.name,
|
|
2363
|
+
detail: a.model
|
|
2364
|
+
}))
|
|
2365
|
+
},
|
|
2366
|
+
{
|
|
2367
|
+
id: "section-memories",
|
|
2368
|
+
label: "Memorias",
|
|
2369
|
+
icon: "\u{1F9E0}",
|
|
2370
|
+
count: summary.totalMemories,
|
|
2371
|
+
color: "#ff80ab",
|
|
2372
|
+
items: result.memories.slice(0, 8).map((m) => ({
|
|
2373
|
+
name: m.type,
|
|
2374
|
+
detail: m.status
|
|
2375
|
+
}))
|
|
2376
|
+
}
|
|
2377
|
+
];
|
|
2378
|
+
}
|
|
2379
|
+
function renderEcosystemMap(result, summary) {
|
|
2380
|
+
const categories = buildCategories(result, summary);
|
|
2381
|
+
const totalItems = summary.totalMcpServers + summary.totalFiles + summary.totalSkills + summary.totalAgents + summary.totalMemories;
|
|
2382
|
+
if (totalItems === 0) return "";
|
|
2383
|
+
const W = 800;
|
|
2384
|
+
const H = 420;
|
|
2385
|
+
const cx = W / 2;
|
|
2386
|
+
const cy = H / 2;
|
|
2387
|
+
const catRadius = 155;
|
|
2388
|
+
const itemRadius = 70;
|
|
2389
|
+
const startAngle = -Math.PI / 2;
|
|
2390
|
+
let svg = "";
|
|
2391
|
+
svg += `<defs>
|
|
2392
|
+
<filter id="glow" x="-50%" y="-50%" width="200%" height="200%">
|
|
2393
|
+
<feGaussianBlur stdDeviation="4" result="blur"/>
|
|
2394
|
+
<feComposite in="SourceGraphic" in2="blur" operator="over"/>
|
|
2395
|
+
</filter>
|
|
2396
|
+
<filter id="glow-strong" x="-50%" y="-50%" width="200%" height="200%">
|
|
2397
|
+
<feGaussianBlur stdDeviation="8" result="blur"/>
|
|
2398
|
+
<feMerge><feMergeNode in="blur"/><feMergeNode in="SourceGraphic"/></feMerge>
|
|
2399
|
+
</filter>
|
|
2400
|
+
</defs>`;
|
|
2401
|
+
svg += `<pattern id="grid" width="30" height="30" patternUnits="userSpaceOnUse">
|
|
2402
|
+
<circle cx="15" cy="15" r="0.5" fill="var(--text-dim)" opacity="0.15"/>
|
|
2403
|
+
</pattern>
|
|
2404
|
+
<rect width="${W}" height="${H}" fill="url(#grid)"/>`;
|
|
2405
|
+
const catPositions = [];
|
|
2406
|
+
categories.forEach((cat, i) => {
|
|
2407
|
+
const angle = startAngle + i / categories.length * Math.PI * 2;
|
|
2408
|
+
const x = cx + Math.cos(angle) * catRadius;
|
|
2409
|
+
const y = cy + Math.sin(angle) * catRadius;
|
|
2410
|
+
catPositions.push({ x, y, cat });
|
|
2411
|
+
const dimmed = cat.count === 0;
|
|
2412
|
+
svg += `<line x1="${cx}" y1="${cy}" x2="${x}" y2="${y}"
|
|
2413
|
+
class="eco-connection ${dimmed ? "eco-dimmed" : "eco-connection--active"}"
|
|
2414
|
+
style="animation-delay: ${i * 0.3}s"/>`;
|
|
2415
|
+
if (!dimmed && cat.items.length > 0) {
|
|
2416
|
+
const maxItems = Math.min(cat.items.length, 8);
|
|
2417
|
+
const arcSpread = Math.min(Math.PI * 0.5, maxItems * 0.2);
|
|
2418
|
+
cat.items.slice(0, maxItems).forEach((item, j) => {
|
|
2419
|
+
const itemAngle = angle - arcSpread / 2 + (maxItems > 1 ? j / (maxItems - 1) * arcSpread : 0);
|
|
2420
|
+
const ix = x + Math.cos(itemAngle) * itemRadius;
|
|
2421
|
+
const iy = y + Math.sin(itemAngle) * itemRadius;
|
|
2422
|
+
svg += `<line x1="${x}" y1="${y}" x2="${ix}" y2="${iy}"
|
|
2423
|
+
stroke="${cat.color}" stroke-width="0.8" opacity="0.25"/>`;
|
|
2424
|
+
svg += `<circle cx="${ix}" cy="${iy}" r="4" fill="${cat.color}"
|
|
2425
|
+
class="eco-item-circle" opacity="0.5">
|
|
2426
|
+
<title>${esc(item.name)}${item.detail ? " (" + esc(item.detail) + ")" : ""}</title>
|
|
2427
|
+
</circle>`;
|
|
2428
|
+
});
|
|
2429
|
+
if (cat.items.length < cat.count) {
|
|
2430
|
+
const extra = cat.count - cat.items.length;
|
|
2431
|
+
const moreAngle = angle + arcSpread / 2 + 0.3;
|
|
2432
|
+
const mx = x + Math.cos(moreAngle) * (itemRadius - 10);
|
|
2433
|
+
const my = y + Math.sin(moreAngle) * (itemRadius - 10);
|
|
2434
|
+
svg += `<text x="${mx}" y="${my}" font-family="var(--font-mono)"
|
|
2435
|
+
font-size="9" fill="${cat.color}" opacity="0.6" text-anchor="middle"
|
|
2436
|
+
dominant-baseline="middle">+${extra}</text>`;
|
|
2437
|
+
}
|
|
2438
|
+
}
|
|
2439
|
+
const nodeR = dimmed ? 22 : 28;
|
|
2440
|
+
svg += `<g class="eco-node ${dimmed ? "eco-dimmed" : ""}" data-section="${cat.id}">
|
|
2441
|
+
<circle cx="${x}" cy="${y}" r="${nodeR}"
|
|
2442
|
+
fill="${cat.color}15" stroke="${cat.color}" stroke-width="2"
|
|
2443
|
+
class="eco-node-circle" ${!dimmed ? 'filter="url(#glow)"' : ""}/>
|
|
2444
|
+
<text x="${x}" y="${y - 3}" class="eco-label"
|
|
2445
|
+
fill="${cat.color}" font-weight="600">${cat.icon} ${cat.label}</text>
|
|
2446
|
+
<text x="${x}" y="${y + 12}" class="eco-label eco-label--count">${cat.count}</text>
|
|
2447
|
+
</g>`;
|
|
2448
|
+
});
|
|
2449
|
+
const projectName = result.project.name.length > 18 ? result.project.name.slice(0, 16) + ".." : result.project.name;
|
|
2450
|
+
svg += `<g class="eco-node">
|
|
2451
|
+
<circle cx="${cx}" cy="${cy}" r="38" fill="var(--accent)" opacity="0.1"
|
|
2452
|
+
stroke="var(--accent)" stroke-width="2" class="eco-center-circle"
|
|
2453
|
+
filter="url(#glow-strong)"/>
|
|
2454
|
+
<circle cx="${cx}" cy="${cy}" r="36" fill="var(--bg-alt)" opacity="0.9"/>
|
|
2455
|
+
<text x="${cx}" y="${cy - 5}" class="eco-label eco-label--center">${esc(projectName)}</text>
|
|
2456
|
+
<text x="${cx}" y="${cy + 12}" class="eco-label eco-label--count">${totalItems} elementos</text>
|
|
2457
|
+
</g>`;
|
|
2458
|
+
return `
|
|
2459
|
+
<div class="ecosystem-map">
|
|
2460
|
+
<svg class="ecosystem-svg" viewBox="0 0 ${W} ${H}" xmlns="http://www.w3.org/2000/svg">
|
|
2461
|
+
${svg}
|
|
2462
|
+
</svg>
|
|
2463
|
+
</div>`;
|
|
2464
|
+
}
|
|
2465
|
+
|
|
2466
|
+
// src/report/sections.ts
|
|
2467
|
+
function esc2(str) {
|
|
2468
|
+
return str.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
2469
|
+
}
|
|
2470
|
+
function formatBytes(bytes) {
|
|
2471
|
+
if (bytes < 1024) return `${bytes} B`;
|
|
2472
|
+
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
|
|
2473
|
+
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
2474
|
+
}
|
|
2475
|
+
function renderNavBar(summary) {
|
|
2476
|
+
const links = [
|
|
2477
|
+
{ id: "section-mcp", label: "MCP", count: summary.totalMcpServers },
|
|
2478
|
+
{ id: "section-context", label: "Contexto", count: summary.totalFiles },
|
|
2479
|
+
{ id: "section-skills", label: "Skills", count: summary.totalSkills },
|
|
2480
|
+
{ id: "section-agents", label: "Agents", count: summary.totalAgents },
|
|
2481
|
+
{ id: "section-memories", label: "Memorias", count: summary.totalMemories }
|
|
2482
|
+
];
|
|
2483
|
+
const navLinks = links.filter((l) => l.count > 0).map(
|
|
2484
|
+
(l) => `<a class="nav-link" data-target="${l.id}">${l.label} (${l.count})</a>`
|
|
2485
|
+
).join("");
|
|
2486
|
+
return `
|
|
2487
|
+
<nav class="nav-bar">
|
|
2488
|
+
<div class="nav-links">
|
|
2489
|
+
<span style="font-family:var(--font-mono);font-size:0.8rem;color:var(--accent);margin-right:0.5rem">>_</span>
|
|
2490
|
+
${navLinks}
|
|
2491
|
+
</div>
|
|
2492
|
+
<div class="nav-actions">
|
|
2493
|
+
<button class="nav-btn" id="theme-toggle" title="Cambiar tema">☾</button>
|
|
2494
|
+
<button class="nav-btn" id="export-btn" title="Exportar JSON">📋</button>
|
|
2495
|
+
</div>
|
|
2496
|
+
</nav>`;
|
|
2497
|
+
}
|
|
2498
|
+
function renderHeader(project, summary, scanDuration) {
|
|
2499
|
+
const date = new Date(project.scannedAt).toLocaleString();
|
|
2500
|
+
return `
|
|
2501
|
+
<header class="header">
|
|
2502
|
+
<h1>> ai-context-inspector</h1>
|
|
2503
|
+
<div class="subtitle">${esc2(project.name)} — ${date} — ${scanDuration}ms</div>
|
|
2504
|
+
<div class="badges">
|
|
2505
|
+
<span class="badge badge--accent">${summary.totalMcpServers} MCPs</span>
|
|
2506
|
+
<span class="badge badge--green">${summary.totalTools} tools</span>
|
|
2507
|
+
<span class="badge badge--purple">${summary.totalFiles} archivos</span>
|
|
2508
|
+
<span class="badge badge--orange">${summary.totalSkills} skills</span>
|
|
2509
|
+
<span class="badge badge--blue">${summary.totalAgents} agents</span>
|
|
2510
|
+
<span class="badge badge--pink">${summary.totalMemories} memorias</span>
|
|
2511
|
+
</div>
|
|
2512
|
+
</header>`;
|
|
2513
|
+
}
|
|
2514
|
+
function renderStatsGrid(summary) {
|
|
2515
|
+
const stats = [
|
|
2516
|
+
{
|
|
2517
|
+
icon: "\u2699\uFE0F",
|
|
2518
|
+
value: summary.totalMcpServers,
|
|
2519
|
+
label: "MCP Servers",
|
|
2520
|
+
color: "#00d4ff"
|
|
2521
|
+
},
|
|
2522
|
+
{
|
|
2523
|
+
icon: "\u{1F6E0}\uFE0F",
|
|
2524
|
+
value: summary.totalTools,
|
|
2525
|
+
label: "MCP Tools",
|
|
2526
|
+
color: "#00e676"
|
|
2527
|
+
},
|
|
2528
|
+
{
|
|
2529
|
+
icon: "\u{1F4C4}",
|
|
2530
|
+
value: summary.totalFiles,
|
|
2531
|
+
label: "Archivos AI",
|
|
2532
|
+
color: "#b388ff"
|
|
2533
|
+
},
|
|
2534
|
+
{
|
|
2535
|
+
icon: "\u26A1",
|
|
2536
|
+
value: summary.totalSkills,
|
|
2537
|
+
label: "Skills",
|
|
2538
|
+
color: "#ffab40"
|
|
2539
|
+
},
|
|
2540
|
+
{
|
|
2541
|
+
icon: "\u{1F916}",
|
|
2542
|
+
value: summary.totalAgents,
|
|
2543
|
+
label: "Agents",
|
|
2544
|
+
color: "#4285f4"
|
|
2545
|
+
},
|
|
2546
|
+
{
|
|
2547
|
+
icon: "\u{1F9E0}",
|
|
2548
|
+
value: summary.totalMemories,
|
|
2549
|
+
label: "Memorias",
|
|
2550
|
+
color: "#ff80ab"
|
|
2551
|
+
}
|
|
2552
|
+
];
|
|
2553
|
+
const cards = stats.map(
|
|
2554
|
+
(s) => `
|
|
2555
|
+
<div class="stat-card" style="--stat-color: ${s.color}">
|
|
2556
|
+
<span class="stat-icon">${s.icon}</span>
|
|
2557
|
+
<span class="stat-number" data-target="${s.value}">0</span>
|
|
2558
|
+
<span class="stat-label">${s.label}</span>
|
|
2559
|
+
</div>`
|
|
2560
|
+
).join("");
|
|
2561
|
+
return `<div class="stats-grid">${cards}</div>`;
|
|
2562
|
+
}
|
|
2563
|
+
function renderMcpServers(servers) {
|
|
2564
|
+
if (servers.length === 0) return "";
|
|
2565
|
+
const cards = servers.map((s, i) => {
|
|
2566
|
+
const intro = s.introspection;
|
|
2567
|
+
const statusClass = intro ? `status--${intro.status}` : "status--configured";
|
|
2568
|
+
const statusText = intro ? intro.status === "ok" ? "OK" : intro.status === "timeout" ? "Timeout" : "Error" : "No introspectado";
|
|
2569
|
+
let toolsHtml = "";
|
|
2570
|
+
if (intro && intro.tools.length > 0) {
|
|
2571
|
+
const items = intro.tools.map(
|
|
2572
|
+
(t) => `<li data-searchable="${esc2(t.name + " " + (t.description ?? ""))}"><span class="tool-name">${esc2(t.name)}</span><span class="tool-desc">${esc2(t.description ?? "")}</span></li>`
|
|
2573
|
+
).join("");
|
|
2574
|
+
toolsHtml = `<div class="card-meta">${intro.tools.length} tools</div><ul class="tool-list">${items}</ul>`;
|
|
2575
|
+
}
|
|
2576
|
+
let resourcesHtml = "";
|
|
2577
|
+
if (intro && intro.resources.length > 0) {
|
|
2578
|
+
resourcesHtml = `<div class="card-meta" style="margin-top:0.5rem">${intro.resources.length} resources</div>`;
|
|
2579
|
+
}
|
|
2580
|
+
let promptsHtml = "";
|
|
2581
|
+
if (intro && intro.prompts.length > 0) {
|
|
2582
|
+
promptsHtml = `<div class="card-meta" style="margin-top:0.5rem">${intro.prompts.length} prompts</div>`;
|
|
2583
|
+
}
|
|
2584
|
+
const serverVersion = intro?.serverInfo ? ` v${esc2(intro.serverInfo.version)}` : "";
|
|
2585
|
+
const errorHtml = intro?.error ? `<div class="card-meta" style="color:var(--red)">${esc2(intro.error)}</div>` : "";
|
|
2586
|
+
const cmdStr = s.config.command ? `${s.config.command} ${(s.config.args ?? []).join(" ")}` : s.config.url ?? "";
|
|
2587
|
+
const configHtml = cmdStr ? `<div class="card-meta">${esc2(cmdStr)}</div>` : "";
|
|
2588
|
+
const copyData = cmdStr || s.name;
|
|
2589
|
+
return `
|
|
2590
|
+
<div class="card" data-searchable="${esc2(s.name + " " + (intro?.serverInfo?.name ?? ""))}"
|
|
2591
|
+
style="animation-delay: ${i * 0.05}s">
|
|
2592
|
+
<button class="copy-btn" data-copy="${esc2(copyData)}">copiar</button>
|
|
2593
|
+
<div class="card-title">
|
|
2594
|
+
<span>${esc2(s.name)}${serverVersion}</span>
|
|
2595
|
+
<span class="status ${statusClass}">${statusText}</span>
|
|
2596
|
+
<span class="scope-badge scope-badge--${s.source}">${s.source}</span>
|
|
2597
|
+
</div>
|
|
2598
|
+
${configHtml}
|
|
2599
|
+
${errorHtml}
|
|
2600
|
+
${toolsHtml}
|
|
2601
|
+
${resourcesHtml}
|
|
2602
|
+
${promptsHtml}
|
|
2603
|
+
</div>`;
|
|
2604
|
+
}).join("");
|
|
2605
|
+
const totalTools = servers.reduce(
|
|
2606
|
+
(sum, s) => sum + (s.introspection?.tools.length ?? 0),
|
|
2607
|
+
0
|
|
2608
|
+
);
|
|
2609
|
+
return `
|
|
2610
|
+
<div class="section" id="section-mcp">
|
|
2611
|
+
<div class="section-header">
|
|
2612
|
+
<div class="section-header-left">
|
|
2613
|
+
<span class="section-icon">\u2699\uFE0F</span>
|
|
2614
|
+
<h2>MCP Servers</h2>
|
|
2615
|
+
</div>
|
|
2616
|
+
<div class="section-header-right">
|
|
2617
|
+
<span class="count">${servers.length} servers · ${totalTools} tools</span>
|
|
2618
|
+
<span class="arrow">▼</span>
|
|
2619
|
+
</div>
|
|
2620
|
+
</div>
|
|
2621
|
+
<div class="section-content">${cards}</div>
|
|
2622
|
+
</div>`;
|
|
2623
|
+
}
|
|
2624
|
+
function renderContextFiles(files) {
|
|
2625
|
+
if (files.length === 0) return "";
|
|
2626
|
+
const maxSize = Math.max(...files.map((f) => f.size), 1);
|
|
2627
|
+
const groups = /* @__PURE__ */ new Map();
|
|
2628
|
+
for (const f of files) {
|
|
2629
|
+
const existing = groups.get(f.tool) ?? [];
|
|
2630
|
+
existing.push(f);
|
|
2631
|
+
groups.set(f.tool, existing);
|
|
2632
|
+
}
|
|
2633
|
+
let groupsHtml = "";
|
|
2634
|
+
for (const [tool, toolFiles] of groups) {
|
|
2635
|
+
const items = toolFiles.map((f, i) => {
|
|
2636
|
+
const previewHtml = f.preview ? `<button class="preview-toggle">Ver contenido</button><div class="preview">${esc2(f.preview)}</div>` : "";
|
|
2637
|
+
const errorHtml = f.error ? `<span class="status status--error">${esc2(f.error)}</span>` : "";
|
|
2638
|
+
const sizeStr = f.size > 0 ? formatBytes(f.size) : "";
|
|
2639
|
+
const typeIcon = f.type === "directory" ? "\u{1F4C1}" : "\u{1F4C4}";
|
|
2640
|
+
const sizePercent = f.size > 0 ? Math.max(3, f.size / maxSize * 100) : 0;
|
|
2641
|
+
const sizeBar = f.size > 0 ? `<div class="size-bar"><div class="size-bar-fill" style="width:${sizePercent}%"></div></div>` : "";
|
|
2642
|
+
const childrenHtml = f.children && f.children.length > 0 ? `<div class="card-meta" style="margin-top:0.3rem">${f.children.length} archivos dentro</div>` : "";
|
|
2643
|
+
return `
|
|
2644
|
+
<div class="card" data-searchable="${esc2(f.path + " " + tool)}"
|
|
2645
|
+
style="animation-delay: ${i * 0.04}s">
|
|
2646
|
+
<button class="copy-btn" data-copy="${esc2(f.path)}">copiar</button>
|
|
2647
|
+
<div class="card-title">${typeIcon} ${esc2(f.path)} ${errorHtml}</div>
|
|
2648
|
+
<div class="card-meta">${sizeStr} · <span class="scope-badge scope-badge--${f.scope}">${f.scope}</span></div>
|
|
2649
|
+
${sizeBar}
|
|
2650
|
+
${childrenHtml}
|
|
2651
|
+
${previewHtml}
|
|
2652
|
+
</div>`;
|
|
2653
|
+
}).join("");
|
|
2654
|
+
groupsHtml += `
|
|
2655
|
+
<div class="tool-group">
|
|
2656
|
+
<div class="tool-group-header">
|
|
2657
|
+
<span class="tool-badge tool-badge--${tool}">${tool}</span>
|
|
2658
|
+
<span class="count">${toolFiles.length} archivos</span>
|
|
2659
|
+
</div>
|
|
2660
|
+
${items}
|
|
2661
|
+
</div>`;
|
|
2662
|
+
}
|
|
2663
|
+
return `
|
|
2664
|
+
<div class="section" id="section-context">
|
|
2665
|
+
<div class="section-header">
|
|
2666
|
+
<div class="section-header-left">
|
|
2667
|
+
<span class="section-icon">\u{1F4C4}</span>
|
|
2668
|
+
<h2>Archivos de Contexto</h2>
|
|
2669
|
+
</div>
|
|
2670
|
+
<div class="section-header-right">
|
|
2671
|
+
<span class="count">${files.length} archivos · ${groups.size} herramientas</span>
|
|
2672
|
+
<span class="arrow">▼</span>
|
|
2673
|
+
</div>
|
|
2674
|
+
</div>
|
|
2675
|
+
<div class="section-content">${groupsHtml}</div>
|
|
2676
|
+
</div>`;
|
|
2677
|
+
}
|
|
2678
|
+
function renderSkills(skills) {
|
|
2679
|
+
if (skills.length === 0) return "";
|
|
2680
|
+
const items = skills.map((s, i) => {
|
|
2681
|
+
const triggersHtml = s.triggers ? `<div class="card-meta">${s.triggers.map((t) => esc2(t)).join(", ")}</div>` : "";
|
|
2682
|
+
return `
|
|
2683
|
+
<div class="card" data-searchable="${esc2(s.name + " " + (s.description ?? ""))}"
|
|
2684
|
+
style="animation-delay: ${i * 0.04}s">
|
|
2685
|
+
<div class="card-title">
|
|
2686
|
+
\u26A1 ${esc2(s.name)}
|
|
2687
|
+
<span class="scope-badge scope-badge--${s.scope}">${s.scope}</span>
|
|
2688
|
+
</div>
|
|
2689
|
+
${s.description ? `<div class="card-meta">${esc2(s.description)}</div>` : ""}
|
|
2690
|
+
${triggersHtml}
|
|
2691
|
+
</div>`;
|
|
2692
|
+
}).join("");
|
|
2693
|
+
return `
|
|
2694
|
+
<div class="section" id="section-skills">
|
|
2695
|
+
<div class="section-header">
|
|
2696
|
+
<div class="section-header-left">
|
|
2697
|
+
<span class="section-icon">\u26A1</span>
|
|
2698
|
+
<h2>Skills</h2>
|
|
2699
|
+
</div>
|
|
2700
|
+
<div class="section-header-right">
|
|
2701
|
+
<span class="count">${skills.length}</span>
|
|
2702
|
+
<span class="arrow">▼</span>
|
|
2703
|
+
</div>
|
|
2704
|
+
</div>
|
|
2705
|
+
<div class="section-content">${items}</div>
|
|
2706
|
+
</div>`;
|
|
2707
|
+
}
|
|
2708
|
+
function renderAgents(agents) {
|
|
2709
|
+
if (agents.length === 0) return "";
|
|
2710
|
+
const items = agents.map((a, i) => {
|
|
2711
|
+
const modelHtml = a.model ? `<span class="badge badge--green" style="font-size:0.65rem;padding:0.1rem 0.5rem">${esc2(a.model)}</span>` : "";
|
|
2712
|
+
const memoryHtml = a.hasMemory ? '<span class="badge badge--pink" style="font-size:0.65rem;padding:0.1rem 0.5rem">\u{1F9E0} memoria</span>' : "";
|
|
2713
|
+
return `
|
|
2714
|
+
<div class="card" data-searchable="${esc2(a.name + " " + (a.description ?? ""))}"
|
|
2715
|
+
style="animation-delay: ${i * 0.04}s">
|
|
2716
|
+
<div class="card-title">
|
|
2717
|
+
\u{1F916} ${esc2(a.name)}
|
|
2718
|
+
${modelHtml}
|
|
2719
|
+
${memoryHtml}
|
|
2720
|
+
<span class="scope-badge scope-badge--${a.scope}">${a.scope}</span>
|
|
2721
|
+
</div>
|
|
2722
|
+
${a.description ? `<div class="card-meta">${esc2(a.description)}</div>` : ""}
|
|
2723
|
+
</div>`;
|
|
2724
|
+
}).join("");
|
|
2725
|
+
return `
|
|
2726
|
+
<div class="section" id="section-agents">
|
|
2727
|
+
<div class="section-header">
|
|
2728
|
+
<div class="section-header-left">
|
|
2729
|
+
<span class="section-icon">\u{1F916}</span>
|
|
2730
|
+
<h2>Agents</h2>
|
|
2731
|
+
</div>
|
|
2732
|
+
<div class="section-header-right">
|
|
2733
|
+
<span class="count">${agents.length}</span>
|
|
2734
|
+
<span class="arrow">▼</span>
|
|
2735
|
+
</div>
|
|
2736
|
+
</div>
|
|
2737
|
+
<div class="section-content">${items}</div>
|
|
2738
|
+
</div>`;
|
|
2739
|
+
}
|
|
2740
|
+
function renderMemories(memories) {
|
|
2741
|
+
if (memories.length === 0) return "";
|
|
2742
|
+
const items = memories.map((m, i) => {
|
|
2743
|
+
const detailEntries = m.details ? Object.entries(m.details).filter(
|
|
2744
|
+
([k]) => k !== "preview"
|
|
2745
|
+
) : [];
|
|
2746
|
+
const detailsHtml = detailEntries.length > 0 ? `<div class="card-meta">${detailEntries.map(([k, v]) => `${esc2(k)}: ${esc2(String(v))}`).join(" · ")}</div>` : "";
|
|
2747
|
+
const previewVal = m.details && typeof m.details.preview === "string" ? m.details.preview : null;
|
|
2748
|
+
const previewHtml = previewVal ? `<button class="preview-toggle">Ver contenido</button><div class="preview">${esc2(previewVal)}</div>` : "";
|
|
2749
|
+
return `
|
|
2750
|
+
<div class="card" data-searchable="${esc2(m.type + " " + (m.path ?? ""))}"
|
|
2751
|
+
style="animation-delay: ${i * 0.04}s">
|
|
2752
|
+
<div class="card-title">
|
|
2753
|
+
\u{1F9E0} ${esc2(m.type)}
|
|
2754
|
+
<span class="status status--${m.status}">${m.status}</span>
|
|
2755
|
+
</div>
|
|
2756
|
+
<div class="card-meta">${m.source}${m.path ? ` · ${esc2(m.path)}` : ""}</div>
|
|
2757
|
+
${detailsHtml}
|
|
2758
|
+
${previewHtml}
|
|
2759
|
+
</div>`;
|
|
2760
|
+
}).join("");
|
|
2761
|
+
return `
|
|
2762
|
+
<div class="section" id="section-memories">
|
|
2763
|
+
<div class="section-header">
|
|
2764
|
+
<div class="section-header-left">
|
|
2765
|
+
<span class="section-icon">\u{1F9E0}</span>
|
|
2766
|
+
<h2>Memorias</h2>
|
|
2767
|
+
</div>
|
|
2768
|
+
<div class="section-header-right">
|
|
2769
|
+
<span class="count">${memories.length}</span>
|
|
2770
|
+
<span class="arrow">▼</span>
|
|
2771
|
+
</div>
|
|
2772
|
+
</div>
|
|
2773
|
+
<div class="section-content">${items}</div>
|
|
2774
|
+
</div>`;
|
|
2775
|
+
}
|
|
2776
|
+
function renderWarnings(warnings) {
|
|
2777
|
+
if (warnings.length === 0) return "";
|
|
2778
|
+
const items = warnings.map(
|
|
2779
|
+
(w) => `
|
|
2780
|
+
<div class="warning-card">
|
|
2781
|
+
<div class="warning-scanner">${esc2(w.scanner)}${w.path ? ` — ${esc2(w.path)}` : ""}</div>
|
|
2782
|
+
<div>${esc2(w.message)}</div>
|
|
2783
|
+
</div>`
|
|
2784
|
+
).join("");
|
|
2785
|
+
return `
|
|
2786
|
+
<div class="section" id="section-warnings">
|
|
2787
|
+
<div class="section-header">
|
|
2788
|
+
<div class="section-header-left">
|
|
2789
|
+
<span class="section-icon">\u26A0\uFE0F</span>
|
|
2790
|
+
<h2>Advertencias</h2>
|
|
2791
|
+
</div>
|
|
2792
|
+
<div class="section-header-right">
|
|
2793
|
+
<span class="count">${warnings.length}</span>
|
|
2794
|
+
<span class="arrow">▼</span>
|
|
2795
|
+
</div>
|
|
2796
|
+
</div>
|
|
2797
|
+
<div class="section-content">${items}</div>
|
|
2798
|
+
</div>`;
|
|
2799
|
+
}
|
|
2800
|
+
function renderEmptyState() {
|
|
2801
|
+
return `
|
|
2802
|
+
<div class="empty-state">
|
|
2803
|
+
<span class="empty-state-icon">\u{1F50D}</span>
|
|
2804
|
+
<h3>No se encontr\xF3 configuraci\xF3n AI en este proyecto</h3>
|
|
2805
|
+
<p>Este proyecto no tiene archivos de configuraci\xF3n de herramientas AI.</p>
|
|
2806
|
+
<p style="margin-top:1rem;font-size:0.85rem;color:var(--text-dim)">
|
|
2807
|
+
Herramientas soportadas: Claude, Cursor, Windsurf, Copilot, Gemini,
|
|
2808
|
+
Codex, OpenCode, Aider, Cline, Roo, Continue, Amazon Q, Augment,
|
|
2809
|
+
Replit, Firebase Studio, Tabnine, Sourcegraph
|
|
2810
|
+
</p>
|
|
2811
|
+
</div>`;
|
|
2812
|
+
}
|
|
2813
|
+
|
|
2814
|
+
// src/report/generator.ts
|
|
2815
|
+
function computeSummary(result) {
|
|
2816
|
+
return {
|
|
2817
|
+
totalMcpServers: result.mcpServers.length,
|
|
2818
|
+
totalTools: result.mcpServers.reduce(
|
|
2819
|
+
(sum, s) => sum + (s.introspection?.tools.length ?? 0),
|
|
2820
|
+
0
|
|
2821
|
+
),
|
|
2822
|
+
totalFiles: result.contextFiles.length,
|
|
2823
|
+
totalSkills: result.skills.length,
|
|
2824
|
+
totalAgents: result.agents.length,
|
|
2825
|
+
totalMemories: result.memories.length
|
|
2826
|
+
};
|
|
2827
|
+
}
|
|
2828
|
+
function generateHtml(result) {
|
|
2829
|
+
const summary = computeSummary(result);
|
|
2830
|
+
const isEmpty = summary.totalMcpServers === 0 && summary.totalFiles === 0 && summary.totalSkills === 0 && summary.totalAgents === 0 && summary.totalMemories === 0;
|
|
2831
|
+
const navBar = renderNavBar(summary);
|
|
2832
|
+
const header = renderHeader(result.project, summary, result.scanDuration);
|
|
2833
|
+
const statsGrid = renderStatsGrid(summary);
|
|
2834
|
+
const ecosystemMap = renderEcosystemMap(result, summary);
|
|
2835
|
+
const content = isEmpty ? renderEmptyState() : [
|
|
2836
|
+
renderMcpServers(result.mcpServers),
|
|
2837
|
+
renderContextFiles(result.contextFiles),
|
|
2838
|
+
renderSkills(result.skills),
|
|
2839
|
+
renderAgents(result.agents),
|
|
2840
|
+
renderMemories(result.memories),
|
|
2841
|
+
renderWarnings(result.warnings)
|
|
2842
|
+
].join("");
|
|
2843
|
+
const exportData = {
|
|
2844
|
+
project: result.project.name,
|
|
2845
|
+
scannedAt: result.project.scannedAt,
|
|
2846
|
+
scanDuration: result.scanDuration,
|
|
2847
|
+
summary,
|
|
2848
|
+
mcpServers: result.mcpServers.map((s) => ({
|
|
2849
|
+
name: s.name,
|
|
2850
|
+
source: s.source,
|
|
2851
|
+
transport: s.config.transport,
|
|
2852
|
+
tools: s.introspection?.tools.length ?? 0
|
|
2853
|
+
})),
|
|
2854
|
+
contextFiles: result.contextFiles.map((f) => ({
|
|
2855
|
+
path: f.path,
|
|
2856
|
+
tool: f.tool,
|
|
2857
|
+
scope: f.scope
|
|
2858
|
+
})),
|
|
2859
|
+
skills: result.skills.map((s) => ({ name: s.name, scope: s.scope })),
|
|
2860
|
+
agents: result.agents.map((a) => ({
|
|
2861
|
+
name: a.name,
|
|
2862
|
+
scope: a.scope,
|
|
2863
|
+
model: a.model
|
|
2864
|
+
})),
|
|
2865
|
+
memories: result.memories.map((m) => ({
|
|
2866
|
+
type: m.type,
|
|
2867
|
+
source: m.source,
|
|
2868
|
+
status: m.status
|
|
2869
|
+
}))
|
|
2870
|
+
};
|
|
2871
|
+
return `<!DOCTYPE html>
|
|
2872
|
+
<html lang="es">
|
|
2873
|
+
<head>
|
|
2874
|
+
<meta charset="UTF-8">
|
|
2875
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
2876
|
+
<title>AI Context Inspector \u2014 ${escHtml(result.project.name)}</title>
|
|
2877
|
+
<style>${CSS_STYLES}</style>
|
|
2878
|
+
</head>
|
|
2879
|
+
<body>
|
|
2880
|
+
${navBar}
|
|
2881
|
+
<div class="container">
|
|
2882
|
+
${header}
|
|
2883
|
+
${statsGrid}
|
|
2884
|
+
${ecosystemMap}
|
|
2885
|
+
<div class="search-bar">
|
|
2886
|
+
<div class="search-bar-inner">
|
|
2887
|
+
<span class="search-icon">\u{1F50D}</span>
|
|
2888
|
+
<input type="text" id="search-input" placeholder="Buscar tools, archivos, skills, agents..." />
|
|
2889
|
+
<span class="search-kbd">/</span>
|
|
2890
|
+
</div>
|
|
2891
|
+
<span class="search-results-count"></span>
|
|
2892
|
+
</div>
|
|
2893
|
+
${content}
|
|
2894
|
+
<footer class="footer">
|
|
2895
|
+
Generado por ai-context-inspector — ${new Date(result.project.scannedAt).toLocaleString()}
|
|
2896
|
+
</footer>
|
|
2897
|
+
</div>
|
|
2898
|
+
<script type="application/json" id="scan-data">${JSON.stringify(exportData)}</script>
|
|
2899
|
+
<script>${JS_SCRIPTS}</script>
|
|
2900
|
+
</body>
|
|
2901
|
+
</html>`;
|
|
2902
|
+
}
|
|
2903
|
+
function escHtml(str) {
|
|
2904
|
+
return str.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
2905
|
+
}
|
|
2906
|
+
|
|
2907
|
+
export {
|
|
2908
|
+
scanMcpConfigs,
|
|
2909
|
+
introspectServers,
|
|
2910
|
+
runAllScanners,
|
|
2911
|
+
generateHtml
|
|
2912
|
+
};
|