@cocaxcode/ai-context-inspector 0.1.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.
@@ -0,0 +1,1565 @@
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 readFile4 } from "fs/promises";
233
+ import { join as join5, basename } 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
+ // ── Aider ──
385
+ {
386
+ path: ".aider.conf.yml",
387
+ tool: "aider",
388
+ type: "file",
389
+ scope: "project",
390
+ description: "Configuraci\xF3n YAML de Aider"
391
+ },
392
+ {
393
+ path: ".aiderignore",
394
+ tool: "aider",
395
+ type: "file",
396
+ scope: "project",
397
+ description: "Archivos ignorados por Aider"
398
+ },
399
+ {
400
+ path: ".aider.model.settings.yml",
401
+ tool: "aider",
402
+ type: "file",
403
+ scope: "project",
404
+ description: "Configuraci\xF3n de modelo de Aider"
405
+ },
406
+ {
407
+ path: ".aider.model.metadata.json",
408
+ tool: "aider",
409
+ type: "file",
410
+ scope: "project",
411
+ description: "Metadatos de modelo de Aider"
412
+ },
413
+ // ── Cline ──
414
+ {
415
+ path: ".clinerules",
416
+ tool: "cline",
417
+ type: "file",
418
+ scope: "project",
419
+ description: "Reglas de Cline (archivo \xFAnico)"
420
+ },
421
+ {
422
+ path: ".clinerules",
423
+ tool: "cline",
424
+ type: "directory",
425
+ scope: "project",
426
+ description: "Directorio de reglas de Cline"
427
+ },
428
+ {
429
+ path: ".clineignore",
430
+ tool: "cline",
431
+ type: "file",
432
+ scope: "project",
433
+ description: "Archivos excluidos de Cline"
434
+ },
435
+ // ── Amazon Q ──
436
+ {
437
+ path: ".amazonq/rules",
438
+ tool: "amazonq",
439
+ type: "directory",
440
+ scope: "project",
441
+ description: "Directorio de reglas de Amazon Q Developer"
442
+ },
443
+ // ── Augment ──
444
+ {
445
+ path: ".augment/rules",
446
+ tool: "augment",
447
+ type: "directory",
448
+ scope: "project",
449
+ description: "Directorio de reglas de Augment Code"
450
+ },
451
+ {
452
+ path: ".augment-guidelines",
453
+ tool: "augment",
454
+ type: "file",
455
+ scope: "project",
456
+ description: "Directrices de Augment (legacy)"
457
+ },
458
+ // ── Replit ──
459
+ {
460
+ path: ".replit.md",
461
+ tool: "replit",
462
+ type: "file",
463
+ scope: "project",
464
+ description: "Instrucciones de Replit Agent"
465
+ },
466
+ // ── Firebase Studio ──
467
+ {
468
+ path: ".idx/airules.md",
469
+ tool: "firebase",
470
+ type: "file",
471
+ scope: "project",
472
+ description: "Reglas AI de Firebase Studio"
473
+ },
474
+ // ── VS Code ──
475
+ {
476
+ path: ".vscode/mcp.json",
477
+ tool: "vscode",
478
+ type: "file",
479
+ scope: "project",
480
+ description: "Configuraci\xF3n MCP de VS Code"
481
+ },
482
+ // ── Universal ──
483
+ {
484
+ path: "CONVENTIONS.md",
485
+ tool: "universal",
486
+ type: "file",
487
+ scope: "project",
488
+ description: "Convenciones de c\xF3digo (multi-tool)"
489
+ },
490
+ // ── User-level entries ──
491
+ {
492
+ path: "~/.claude",
493
+ tool: "claude",
494
+ type: "directory",
495
+ scope: "user",
496
+ description: "Configuraci\xF3n global de Claude Code"
497
+ },
498
+ {
499
+ path: "~/.claude.json",
500
+ tool: "claude",
501
+ type: "file",
502
+ scope: "user",
503
+ description: "Servidores MCP a nivel de usuario"
504
+ },
505
+ {
506
+ path: "~/.claude/CLAUDE.md",
507
+ tool: "claude",
508
+ type: "file",
509
+ scope: "user",
510
+ description: "Instrucciones globales de Claude Code"
511
+ },
512
+ {
513
+ path: "~/.gemini",
514
+ tool: "gemini",
515
+ type: "directory",
516
+ scope: "user",
517
+ description: "Configuraci\xF3n global de Gemini CLI"
518
+ },
519
+ {
520
+ path: "~/.gemini/GEMINI.md",
521
+ tool: "gemini",
522
+ type: "file",
523
+ scope: "user",
524
+ description: "Instrucciones globales de Gemini CLI"
525
+ },
526
+ {
527
+ path: "~/.codex",
528
+ tool: "codex",
529
+ type: "directory",
530
+ scope: "user",
531
+ description: "Configuraci\xF3n global de Codex CLI"
532
+ },
533
+ {
534
+ path: "~/.continue",
535
+ tool: "continue",
536
+ type: "directory",
537
+ scope: "user",
538
+ description: "Configuraci\xF3n global de Continue.dev"
539
+ },
540
+ {
541
+ path: "~/.aider.conf.yml",
542
+ tool: "aider",
543
+ type: "file",
544
+ scope: "user",
545
+ description: "Configuraci\xF3n global de Aider"
546
+ },
547
+ {
548
+ path: "~/.augment/rules",
549
+ tool: "augment",
550
+ type: "directory",
551
+ scope: "user",
552
+ description: "Reglas globales de Augment"
553
+ },
554
+ {
555
+ path: "~/.github/agents",
556
+ tool: "copilot",
557
+ type: "directory",
558
+ scope: "user",
559
+ description: "Agentes globales de Copilot"
560
+ },
561
+ {
562
+ path: "~/.codeium",
563
+ tool: "windsurf",
564
+ type: "directory",
565
+ scope: "user",
566
+ description: "Configuraci\xF3n global de Codeium/Windsurf"
567
+ }
568
+ ];
569
+
570
+ // src/scanner/context-files.ts
571
+ var PREVIEW_MAX_CHARS = 500;
572
+ function resolveHome(p) {
573
+ return p.startsWith("~/") ? join2(homedir2(), p.slice(2)) : p;
574
+ }
575
+ async function scanDirectory(dirPath, projectDir, scope) {
576
+ const children = [];
577
+ try {
578
+ const entries = await readdir(dirPath, { withFileTypes: true });
579
+ for (const entry of entries) {
580
+ const fullPath = join2(dirPath, entry.name);
581
+ try {
582
+ const s = await stat(fullPath);
583
+ let preview = null;
584
+ if (entry.isFile()) {
585
+ try {
586
+ const content = await readFile2(fullPath, "utf-8");
587
+ preview = content.slice(0, PREVIEW_MAX_CHARS);
588
+ } catch {
589
+ preview = null;
590
+ }
591
+ }
592
+ children.push({
593
+ path: relative(projectDir, fullPath),
594
+ absolutePath: fullPath,
595
+ tool: "claude",
596
+ alsoUsedBy: [],
597
+ type: entry.isDirectory() ? "directory" : "file",
598
+ scope,
599
+ size: s.size,
600
+ preview
601
+ });
602
+ } catch {
603
+ }
604
+ }
605
+ } catch {
606
+ }
607
+ return children;
608
+ }
609
+ async function scanContextFiles(config) {
610
+ const files = [];
611
+ const warnings = [];
612
+ for (const entry of AI_FILE_CATALOG) {
613
+ if (entry.scope === "user" && !config.includeUser) continue;
614
+ const resolvedPath = entry.scope === "user" ? resolveHome(entry.path) : join2(config.dir, entry.path);
615
+ try {
616
+ const s = await stat(resolvedPath);
617
+ const isDir = s.isDirectory();
618
+ if (entry.tool === "cline" && entry.path === ".clinerules") {
619
+ if (entry.type === "file" && isDir || entry.type === "directory" && !isDir) {
620
+ continue;
621
+ }
622
+ } else if (entry.type === "file" && isDir) {
623
+ continue;
624
+ } else if (entry.type === "directory" && !isDir) {
625
+ continue;
626
+ }
627
+ let preview = null;
628
+ let children;
629
+ if (isDir) {
630
+ children = await scanDirectory(resolvedPath, config.dir, entry.scope);
631
+ } else {
632
+ try {
633
+ const content = await readFile2(resolvedPath, "utf-8");
634
+ preview = content.slice(0, PREVIEW_MAX_CHARS);
635
+ } catch {
636
+ preview = null;
637
+ }
638
+ }
639
+ const result = {
640
+ path: entry.scope === "user" ? entry.path : relative(config.dir, resolvedPath),
641
+ absolutePath: resolvedPath,
642
+ tool: entry.tool,
643
+ alsoUsedBy: entry.alsoUsedBy ?? [],
644
+ type: isDir ? "directory" : "file",
645
+ scope: entry.scope,
646
+ size: s.size,
647
+ preview,
648
+ children
649
+ };
650
+ files.push(result);
651
+ } catch (err) {
652
+ const code = err.code;
653
+ if (code === "EACCES") {
654
+ warnings.push({
655
+ scanner: "context-files",
656
+ message: `Permiso denegado: ${resolvedPath}`,
657
+ path: resolvedPath
658
+ });
659
+ files.push({
660
+ path: entry.scope === "user" ? entry.path : relative(config.dir, resolvedPath),
661
+ absolutePath: resolvedPath,
662
+ tool: entry.tool,
663
+ alsoUsedBy: entry.alsoUsedBy ?? [],
664
+ type: entry.type,
665
+ scope: entry.scope,
666
+ size: 0,
667
+ preview: null,
668
+ error: "EACCES"
669
+ });
670
+ }
671
+ }
672
+ }
673
+ return { files, warnings };
674
+ }
675
+
676
+ // src/scanner/skills.ts
677
+ import { readFile as readFile3, readdir as readdir2, stat as stat2 } from "fs/promises";
678
+ import { join as join3 } from "path";
679
+ import { homedir as homedir3 } from "os";
680
+ var SKILL_DIRS_PROJECT = [".claude/skills"];
681
+ var SKILL_DIRS_USER = ["~/.claude/skills"];
682
+ async function scanSkillDir(dirPath, scope) {
683
+ const skills = [];
684
+ try {
685
+ const entries = await readdir2(dirPath, { withFileTypes: true });
686
+ for (const entry of entries) {
687
+ if (!entry.isDirectory()) continue;
688
+ if (entry.name.startsWith("_") || entry.name.startsWith("sdd-")) continue;
689
+ const skillMdPath = join3(dirPath, entry.name, "SKILL.md");
690
+ try {
691
+ await stat2(skillMdPath);
692
+ const content = await readFile3(skillMdPath, "utf-8");
693
+ let description;
694
+ const purposeMatch = content.match(/##\s*Purpose\s*\n+(.+)/i);
695
+ if (purposeMatch) {
696
+ description = purposeMatch[1].trim().slice(0, 200);
697
+ }
698
+ const triggers = [];
699
+ const triggerMatch = content.match(
700
+ /(?:trigger|triggers?):\s*(.+)/gi
701
+ );
702
+ if (triggerMatch) {
703
+ for (const match of triggerMatch) {
704
+ const value = match.replace(/(?:trigger|triggers?):\s*/i, "").trim();
705
+ if (value) triggers.push(value);
706
+ }
707
+ }
708
+ skills.push({
709
+ name: entry.name,
710
+ path: skillMdPath,
711
+ scope,
712
+ description,
713
+ triggers: triggers.length > 0 ? triggers : void 0
714
+ });
715
+ } catch {
716
+ }
717
+ }
718
+ } catch {
719
+ }
720
+ return skills;
721
+ }
722
+ async function parseSkillRegistry(registryPath) {
723
+ const skills = [];
724
+ try {
725
+ const content = await readFile3(registryPath, "utf-8");
726
+ const lines = content.split("\n");
727
+ let inTable = false;
728
+ for (const line of lines) {
729
+ if (line.includes("| Skill") && line.includes("Trigger")) {
730
+ inTable = true;
731
+ continue;
732
+ }
733
+ if (inTable && line.startsWith("|---")) continue;
734
+ if (inTable && line.startsWith("|")) {
735
+ const cells = line.split("|").map((c) => c.trim()).filter(Boolean);
736
+ if (cells.length >= 2) {
737
+ skills.push({
738
+ name: cells[0],
739
+ path: cells[2] ?? registryPath,
740
+ scope: "project",
741
+ triggers: cells[1] ? [cells[1]] : void 0
742
+ });
743
+ }
744
+ } else if (inTable) {
745
+ inTable = false;
746
+ }
747
+ }
748
+ } catch {
749
+ }
750
+ return skills;
751
+ }
752
+ async function scanSkills(config) {
753
+ const allSkills = [];
754
+ const warnings = [];
755
+ for (const dir of SKILL_DIRS_PROJECT) {
756
+ const fullPath = join3(config.dir, dir);
757
+ const found = await scanSkillDir(fullPath, "project");
758
+ allSkills.push(...found);
759
+ }
760
+ if (config.includeUser) {
761
+ for (const dir of SKILL_DIRS_USER) {
762
+ const fullPath = dir.startsWith("~/") ? join3(homedir3(), dir.slice(2)) : dir;
763
+ const found = await scanSkillDir(fullPath, "user");
764
+ allSkills.push(...found);
765
+ }
766
+ }
767
+ const registryPath = join3(config.dir, ".atl", "skill-registry.md");
768
+ const registrySkills = await parseSkillRegistry(registryPath);
769
+ const seen = new Set(allSkills.map((s) => s.name));
770
+ for (const rs of registrySkills) {
771
+ if (!seen.has(rs.name)) {
772
+ allSkills.push(rs);
773
+ seen.add(rs.name);
774
+ }
775
+ }
776
+ return { skills: allSkills, warnings };
777
+ }
778
+
779
+ // src/scanner/memories.ts
780
+ import { readdir as readdir3, stat as stat3 } from "fs/promises";
781
+ import { join as join4 } from "path";
782
+ async function detectOpenspec(dir) {
783
+ const openspecDir = join4(dir, "openspec");
784
+ try {
785
+ const s = await stat3(openspecDir);
786
+ if (!s.isDirectory()) return null;
787
+ let specsCount = 0;
788
+ let changesCount = 0;
789
+ try {
790
+ const specsDir = join4(openspecDir, "specs");
791
+ const specs = await readdir3(specsDir);
792
+ specsCount = specs.filter((f) => !f.startsWith(".")).length;
793
+ } catch {
794
+ }
795
+ try {
796
+ const changesDir = join4(openspecDir, "changes");
797
+ const changes = await readdir3(changesDir);
798
+ changesCount = changes.filter(
799
+ (f) => !f.startsWith(".") && f !== "archive"
800
+ ).length;
801
+ } catch {
802
+ }
803
+ return {
804
+ type: "openspec",
805
+ path: "openspec/",
806
+ source: "filesystem",
807
+ status: "active",
808
+ details: { specs: specsCount, changes: changesCount }
809
+ };
810
+ } catch {
811
+ return null;
812
+ }
813
+ }
814
+ async function detectAtl(dir) {
815
+ const atlDir = join4(dir, ".atl");
816
+ try {
817
+ const s = await stat3(atlDir);
818
+ if (!s.isDirectory()) return null;
819
+ let files = [];
820
+ try {
821
+ files = await readdir3(atlDir);
822
+ } catch {
823
+ }
824
+ return {
825
+ type: "atl",
826
+ path: ".atl/",
827
+ source: "filesystem",
828
+ status: "active",
829
+ details: { files: files.length }
830
+ };
831
+ } catch {
832
+ return null;
833
+ }
834
+ }
835
+ function detectEngram(mcpServers) {
836
+ const engram = mcpServers.find(
837
+ (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"))
838
+ );
839
+ if (!engram) return null;
840
+ return {
841
+ type: "engram",
842
+ source: "mcp",
843
+ status: "configured",
844
+ details: { server: engram.name }
845
+ };
846
+ }
847
+ async function scanMemories(config, mcpServers) {
848
+ const memories = [];
849
+ const warnings = [];
850
+ const openspec = await detectOpenspec(config.dir);
851
+ if (openspec) memories.push(openspec);
852
+ const atl = await detectAtl(config.dir);
853
+ if (atl) memories.push(atl);
854
+ const engram = detectEngram(mcpServers);
855
+ if (engram) memories.push(engram);
856
+ return { memories, warnings };
857
+ }
858
+
859
+ // src/scanner/index.ts
860
+ async function detectProjectName(dir) {
861
+ try {
862
+ const pkg = await readFile4(join5(dir, "package.json"), "utf-8");
863
+ const parsed = JSON.parse(pkg);
864
+ if (parsed.name) return parsed.name;
865
+ } catch {
866
+ }
867
+ return basename(dir);
868
+ }
869
+ async function runAllScanners(config) {
870
+ const start = performance.now();
871
+ const warnings = [];
872
+ const absDir = resolve(config.dir);
873
+ const name = await detectProjectName(absDir);
874
+ const project = {
875
+ name,
876
+ path: absDir,
877
+ scannedAt: (/* @__PURE__ */ new Date()).toISOString()
878
+ };
879
+ const [contextResult, mcpResult, skillsResult] = await Promise.all([
880
+ scanContextFiles({ ...config, dir: absDir }),
881
+ scanMcpConfigs({ dir: absDir, includeUser: config.includeUser }),
882
+ scanSkills({ ...config, dir: absDir })
883
+ ]);
884
+ warnings.push(...contextResult.warnings);
885
+ warnings.push(...mcpResult.warnings);
886
+ warnings.push(...skillsResult.warnings);
887
+ if (config.introspect && mcpResult.servers.length > 0) {
888
+ await introspectServers(mcpResult.servers, config.timeout);
889
+ }
890
+ const memoriesResult = await scanMemories(
891
+ { ...config, dir: absDir },
892
+ mcpResult.servers
893
+ );
894
+ warnings.push(...memoriesResult.warnings);
895
+ const scanDuration = Math.round(performance.now() - start);
896
+ return {
897
+ project,
898
+ contextFiles: contextResult.files,
899
+ mcpServers: mcpResult.servers,
900
+ skills: skillsResult.skills,
901
+ memories: memoriesResult.memories,
902
+ warnings,
903
+ scanDuration
904
+ };
905
+ }
906
+
907
+ // src/report/styles.ts
908
+ var CSS_STYLES = `
909
+ :root {
910
+ --bg: #0a0a0f;
911
+ --bg-card: #12121a;
912
+ --bg-card-hover: #1a1a25;
913
+ --border: #2a2a3a;
914
+ --text: #e0e0e8;
915
+ --text-dim: #8888a0;
916
+ --text-bright: #ffffff;
917
+ --accent: #00d4ff;
918
+ --accent-dim: #0099bb;
919
+ --green: #00e676;
920
+ --red: #ff5252;
921
+ --orange: #ffab40;
922
+ --purple: #b388ff;
923
+ --font-mono: 'JetBrains Mono', 'Fira Code', 'Cascadia Code', 'Consolas', monospace;
924
+ --font-sans: system-ui, -apple-system, sans-serif;
925
+ --radius: 8px;
926
+ }
927
+
928
+ @media (prefers-color-scheme: light) {
929
+ :root {
930
+ --bg: #f5f5f8;
931
+ --bg-card: #ffffff;
932
+ --bg-card-hover: #f0f0f5;
933
+ --border: #d0d0dd;
934
+ --text: #2a2a3a;
935
+ --text-dim: #666680;
936
+ --text-bright: #000000;
937
+ --accent: #0088cc;
938
+ --accent-dim: #006699;
939
+ }
940
+ }
941
+
942
+ * { margin: 0; padding: 0; box-sizing: border-box; }
943
+
944
+ body {
945
+ background: var(--bg);
946
+ color: var(--text);
947
+ font-family: var(--font-sans);
948
+ line-height: 1.6;
949
+ min-height: 100vh;
950
+ }
951
+
952
+ .container {
953
+ max-width: 1200px;
954
+ margin: 0 auto;
955
+ padding: 2rem;
956
+ }
957
+
958
+ /* \u2500\u2500 Header \u2500\u2500 */
959
+ .header {
960
+ text-align: center;
961
+ padding: 2rem 0 1.5rem;
962
+ border-bottom: 1px solid var(--border);
963
+ margin-bottom: 1.5rem;
964
+ }
965
+
966
+ .header h1 {
967
+ font-family: var(--font-mono);
968
+ font-size: 1.5rem;
969
+ color: var(--accent);
970
+ margin-bottom: 0.5rem;
971
+ }
972
+
973
+ .header .subtitle {
974
+ color: var(--text-dim);
975
+ font-size: 0.85rem;
976
+ }
977
+
978
+ .badges {
979
+ display: flex;
980
+ gap: 0.75rem;
981
+ justify-content: center;
982
+ flex-wrap: wrap;
983
+ margin-top: 1rem;
984
+ }
985
+
986
+ .badge {
987
+ display: inline-flex;
988
+ align-items: center;
989
+ gap: 0.4rem;
990
+ padding: 0.3rem 0.8rem;
991
+ border-radius: 20px;
992
+ font-size: 0.8rem;
993
+ font-weight: 600;
994
+ font-family: var(--font-mono);
995
+ background: var(--bg-card);
996
+ border: 1px solid var(--border);
997
+ }
998
+
999
+ .badge--accent { border-color: var(--accent); color: var(--accent); }
1000
+ .badge--green { border-color: var(--green); color: var(--green); }
1001
+ .badge--purple { border-color: var(--purple); color: var(--purple); }
1002
+ .badge--orange { border-color: var(--orange); color: var(--orange); }
1003
+
1004
+ /* \u2500\u2500 Search \u2500\u2500 */
1005
+ .search-bar {
1006
+ position: sticky;
1007
+ top: 0;
1008
+ z-index: 10;
1009
+ background: var(--bg);
1010
+ padding: 0.75rem 0;
1011
+ margin-bottom: 1rem;
1012
+ }
1013
+
1014
+ .search-bar input {
1015
+ width: 100%;
1016
+ padding: 0.6rem 1rem;
1017
+ border: 1px solid var(--border);
1018
+ border-radius: var(--radius);
1019
+ background: var(--bg-card);
1020
+ color: var(--text);
1021
+ font-family: var(--font-mono);
1022
+ font-size: 0.85rem;
1023
+ outline: none;
1024
+ transition: border-color 0.2s;
1025
+ }
1026
+
1027
+ .search-bar input:focus {
1028
+ border-color: var(--accent);
1029
+ }
1030
+
1031
+ /* \u2500\u2500 Sections \u2500\u2500 */
1032
+ .section {
1033
+ margin-bottom: 1.5rem;
1034
+ border: 1px solid var(--border);
1035
+ border-radius: var(--radius);
1036
+ overflow: hidden;
1037
+ }
1038
+
1039
+ .section-header {
1040
+ display: flex;
1041
+ align-items: center;
1042
+ justify-content: space-between;
1043
+ padding: 0.75rem 1rem;
1044
+ background: var(--bg-card);
1045
+ cursor: pointer;
1046
+ user-select: none;
1047
+ transition: background 0.2s;
1048
+ }
1049
+
1050
+ .section-header:hover {
1051
+ background: var(--bg-card-hover);
1052
+ }
1053
+
1054
+ .section-header h2 {
1055
+ font-size: 1rem;
1056
+ font-family: var(--font-mono);
1057
+ color: var(--text-bright);
1058
+ }
1059
+
1060
+ .section-header .count {
1061
+ font-size: 0.8rem;
1062
+ color: var(--text-dim);
1063
+ font-family: var(--font-mono);
1064
+ }
1065
+
1066
+ .section-header .arrow {
1067
+ transition: transform 0.2s;
1068
+ color: var(--text-dim);
1069
+ }
1070
+
1071
+ .section.collapsed .section-content {
1072
+ display: none;
1073
+ }
1074
+
1075
+ .section.collapsed .arrow {
1076
+ transform: rotate(-90deg);
1077
+ }
1078
+
1079
+ .section-content {
1080
+ padding: 1rem;
1081
+ border-top: 1px solid var(--border);
1082
+ }
1083
+
1084
+ /* \u2500\u2500 Cards \u2500\u2500 */
1085
+ .card {
1086
+ background: var(--bg-card);
1087
+ border: 1px solid var(--border);
1088
+ border-radius: var(--radius);
1089
+ padding: 1rem;
1090
+ margin-bottom: 0.75rem;
1091
+ transition: border-color 0.2s;
1092
+ }
1093
+
1094
+ .card:hover {
1095
+ border-color: var(--accent-dim);
1096
+ }
1097
+
1098
+ .card:last-child {
1099
+ margin-bottom: 0;
1100
+ }
1101
+
1102
+ .card-title {
1103
+ font-family: var(--font-mono);
1104
+ font-size: 0.9rem;
1105
+ color: var(--accent);
1106
+ margin-bottom: 0.5rem;
1107
+ display: flex;
1108
+ align-items: center;
1109
+ gap: 0.5rem;
1110
+ }
1111
+
1112
+ .card-meta {
1113
+ font-size: 0.75rem;
1114
+ color: var(--text-dim);
1115
+ margin-bottom: 0.5rem;
1116
+ }
1117
+
1118
+ /* \u2500\u2500 Tool badges \u2500\u2500 */
1119
+ .tool-badge {
1120
+ display: inline-block;
1121
+ padding: 0.15rem 0.5rem;
1122
+ border-radius: 4px;
1123
+ font-size: 0.7rem;
1124
+ font-weight: 600;
1125
+ text-transform: uppercase;
1126
+ letter-spacing: 0.05em;
1127
+ }
1128
+
1129
+ .tool-badge--claude { background: #d4760020; color: #d47600; border: 1px solid #d4760040; }
1130
+ .tool-badge--cursor { background: #00a2ff20; color: #00a2ff; border: 1px solid #00a2ff40; }
1131
+ .tool-badge--windsurf { background: #00c48020; color: #00c480; border: 1px solid #00c48040; }
1132
+ .tool-badge--copilot { background: #8b5cf620; color: #8b5cf6; border: 1px solid #8b5cf640; }
1133
+ .tool-badge--gemini { background: #4285f420; color: #4285f4; border: 1px solid #4285f440; }
1134
+ .tool-badge--codex { background: #10a37f20; color: #10a37f; border: 1px solid #10a37f40; }
1135
+ .tool-badge--aider { background: #ff6b6b20; color: #ff6b6b; border: 1px solid #ff6b6b40; }
1136
+ .tool-badge--cline { background: #e91e6320; color: #e91e63; border: 1px solid #e91e6340; }
1137
+ .tool-badge--continue { background: #ff980020; color: #ff9800; border: 1px solid #ff980040; }
1138
+ .tool-badge--amazonq { background: #ff990020; color: #ff9900; border: 1px solid #ff990040; }
1139
+ .tool-badge--augment { background: #9c27b020; color: #9c27b0; border: 1px solid #9c27b040; }
1140
+ .tool-badge--replit { background: #f2620020; color: #f26200; border: 1px solid #f2620040; }
1141
+ .tool-badge--firebase { background: #ffca2820; color: #ffca28; border: 1px solid #ffca2840; }
1142
+ .tool-badge--vscode { background: #007acc20; color: #007acc; border: 1px solid #007acc40; }
1143
+ .tool-badge--universal { background: #78909c20; color: #78909c; border: 1px solid #78909c40; }
1144
+
1145
+ /* \u2500\u2500 Status \u2500\u2500 */
1146
+ .status {
1147
+ display: inline-block;
1148
+ padding: 0.15rem 0.5rem;
1149
+ border-radius: 4px;
1150
+ font-size: 0.7rem;
1151
+ font-weight: 600;
1152
+ }
1153
+
1154
+ .status--ok { background: #00e67620; color: var(--green); }
1155
+ .status--timeout { background: #ffab4020; color: var(--orange); }
1156
+ .status--error { background: #ff525220; color: var(--red); }
1157
+ .status--active { background: #00e67620; color: var(--green); }
1158
+ .status--configured { background: #00d4ff20; color: var(--accent); }
1159
+
1160
+ /* \u2500\u2500 Tool list \u2500\u2500 */
1161
+ .tool-list {
1162
+ list-style: none;
1163
+ margin-top: 0.5rem;
1164
+ }
1165
+
1166
+ .tool-list li {
1167
+ padding: 0.3rem 0;
1168
+ font-size: 0.8rem;
1169
+ border-bottom: 1px solid var(--border);
1170
+ display: flex;
1171
+ gap: 0.5rem;
1172
+ }
1173
+
1174
+ .tool-list li:last-child {
1175
+ border-bottom: none;
1176
+ }
1177
+
1178
+ .tool-name {
1179
+ font-family: var(--font-mono);
1180
+ color: var(--accent);
1181
+ white-space: nowrap;
1182
+ }
1183
+
1184
+ .tool-desc {
1185
+ color: var(--text-dim);
1186
+ overflow: hidden;
1187
+ text-overflow: ellipsis;
1188
+ }
1189
+
1190
+ /* \u2500\u2500 Preview \u2500\u2500 */
1191
+ .preview {
1192
+ background: var(--bg);
1193
+ border: 1px solid var(--border);
1194
+ border-radius: 4px;
1195
+ padding: 0.5rem;
1196
+ margin-top: 0.5rem;
1197
+ font-family: var(--font-mono);
1198
+ font-size: 0.75rem;
1199
+ color: var(--text-dim);
1200
+ white-space: pre-wrap;
1201
+ word-break: break-all;
1202
+ max-height: 150px;
1203
+ overflow-y: auto;
1204
+ display: none;
1205
+ }
1206
+
1207
+ .preview.open {
1208
+ display: block;
1209
+ }
1210
+
1211
+ .preview-toggle {
1212
+ background: none;
1213
+ border: none;
1214
+ color: var(--accent);
1215
+ font-size: 0.75rem;
1216
+ cursor: pointer;
1217
+ font-family: var(--font-mono);
1218
+ padding: 0;
1219
+ }
1220
+
1221
+ .preview-toggle:hover {
1222
+ text-decoration: underline;
1223
+ }
1224
+
1225
+ /* \u2500\u2500 Groups \u2500\u2500 */
1226
+ .tool-group {
1227
+ margin-bottom: 1rem;
1228
+ }
1229
+
1230
+ .tool-group:last-child {
1231
+ margin-bottom: 0;
1232
+ }
1233
+
1234
+ .tool-group-header {
1235
+ display: flex;
1236
+ align-items: center;
1237
+ gap: 0.5rem;
1238
+ margin-bottom: 0.5rem;
1239
+ padding-bottom: 0.3rem;
1240
+ border-bottom: 1px solid var(--border);
1241
+ }
1242
+
1243
+ /* \u2500\u2500 Empty state \u2500\u2500 */
1244
+ .empty-state {
1245
+ text-align: center;
1246
+ padding: 3rem;
1247
+ color: var(--text-dim);
1248
+ }
1249
+
1250
+ .empty-state h3 {
1251
+ font-size: 1.1rem;
1252
+ margin-bottom: 0.5rem;
1253
+ color: var(--text);
1254
+ }
1255
+
1256
+ /* \u2500\u2500 Scope badge \u2500\u2500 */
1257
+ .scope-badge {
1258
+ font-size: 0.65rem;
1259
+ padding: 0.1rem 0.4rem;
1260
+ border-radius: 3px;
1261
+ font-weight: 600;
1262
+ text-transform: uppercase;
1263
+ }
1264
+
1265
+ .scope-badge--project { background: var(--accent); color: var(--bg); }
1266
+ .scope-badge--user { background: var(--purple); color: var(--bg); }
1267
+
1268
+ /* \u2500\u2500 Footer \u2500\u2500 */
1269
+ .footer {
1270
+ text-align: center;
1271
+ padding: 1.5rem 0;
1272
+ margin-top: 1rem;
1273
+ border-top: 1px solid var(--border);
1274
+ font-size: 0.75rem;
1275
+ color: var(--text-dim);
1276
+ font-family: var(--font-mono);
1277
+ }
1278
+
1279
+ /* \u2500\u2500 Responsive \u2500\u2500 */
1280
+ @media (max-width: 768px) {
1281
+ .container { padding: 1rem; }
1282
+ .badges { gap: 0.5rem; }
1283
+ .badge { font-size: 0.7rem; padding: 0.2rem 0.6rem; }
1284
+ }
1285
+ `;
1286
+
1287
+ // src/report/scripts.ts
1288
+ var JS_SCRIPTS = `
1289
+ document.addEventListener('DOMContentLoaded', () => {
1290
+ // Section collapse/expand
1291
+ document.querySelectorAll('.section-header').forEach(header => {
1292
+ header.addEventListener('click', () => {
1293
+ header.closest('.section').classList.toggle('collapsed')
1294
+ })
1295
+ })
1296
+
1297
+ // Preview toggle
1298
+ document.querySelectorAll('.preview-toggle').forEach(btn => {
1299
+ btn.addEventListener('click', (e) => {
1300
+ e.stopPropagation()
1301
+ const preview = btn.nextElementSibling
1302
+ if (preview && preview.classList.contains('preview')) {
1303
+ preview.classList.toggle('open')
1304
+ btn.textContent = preview.classList.contains('open') ? 'Ocultar' : 'Ver contenido'
1305
+ }
1306
+ })
1307
+ })
1308
+
1309
+ // Global search
1310
+ const searchInput = document.getElementById('search-input')
1311
+ if (searchInput) {
1312
+ searchInput.addEventListener('input', (e) => {
1313
+ const query = e.target.value.toLowerCase().trim()
1314
+ document.querySelectorAll('[data-searchable]').forEach(el => {
1315
+ const text = el.getAttribute('data-searchable').toLowerCase()
1316
+ el.style.display = (!query || text.includes(query)) ? '' : 'none'
1317
+ })
1318
+ })
1319
+ }
1320
+ })
1321
+ `;
1322
+
1323
+ // src/report/sections.ts
1324
+ function esc(str) {
1325
+ return str.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
1326
+ }
1327
+ function formatBytes(bytes) {
1328
+ if (bytes < 1024) return `${bytes} B`;
1329
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
1330
+ return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
1331
+ }
1332
+ function renderHeader(project, summary, scanDuration) {
1333
+ const date = new Date(project.scannedAt).toLocaleString();
1334
+ return `
1335
+ <header class="header">
1336
+ <h1>&gt; ai-context-inspector</h1>
1337
+ <div class="subtitle">${esc(project.name)} &mdash; ${date} &mdash; ${scanDuration}ms</div>
1338
+ <div class="badges">
1339
+ <span class="badge badge--accent">${summary.totalMcpServers} MCPs</span>
1340
+ <span class="badge badge--green">${summary.totalTools} tools</span>
1341
+ <span class="badge badge--purple">${summary.totalFiles} archivos</span>
1342
+ <span class="badge badge--orange">${summary.totalSkills} skills</span>
1343
+ <span class="badge badge--accent">${summary.totalMemories} memorias</span>
1344
+ </div>
1345
+ </header>`;
1346
+ }
1347
+ function renderMcpServers(servers) {
1348
+ if (servers.length === 0) return "";
1349
+ const cards = servers.map((s) => {
1350
+ const intro = s.introspection;
1351
+ const statusClass = intro ? `status--${intro.status}` : "status--configured";
1352
+ const statusText = intro ? intro.status === "ok" ? "OK" : intro.status === "timeout" ? "Timeout" : "Error" : "No introspectado";
1353
+ let toolsHtml = "";
1354
+ if (intro && intro.tools.length > 0) {
1355
+ const items = intro.tools.map(
1356
+ (t) => `<li data-searchable="${esc(t.name + " " + (t.description ?? ""))}"><span class="tool-name">${esc(t.name)}</span><span class="tool-desc">${esc(t.description ?? "")}</span></li>`
1357
+ ).join("");
1358
+ toolsHtml = `<div class="card-meta">${intro.tools.length} tools</div><ul class="tool-list">${items}</ul>`;
1359
+ }
1360
+ let resourcesHtml = "";
1361
+ if (intro && intro.resources.length > 0) {
1362
+ resourcesHtml = `<div class="card-meta" style="margin-top:0.5rem">${intro.resources.length} resources</div>`;
1363
+ }
1364
+ let promptsHtml = "";
1365
+ if (intro && intro.prompts.length > 0) {
1366
+ promptsHtml = `<div class="card-meta" style="margin-top:0.5rem">${intro.prompts.length} prompts</div>`;
1367
+ }
1368
+ const serverVersion = intro?.serverInfo ? ` v${esc(intro.serverInfo.version)}` : "";
1369
+ const errorHtml = intro?.error ? `<div class="card-meta" style="color:var(--red)">${esc(intro.error)}</div>` : "";
1370
+ const configHtml = s.config.command ? `<div class="card-meta">${esc(s.config.command)} ${esc((s.config.args ?? []).join(" "))}</div>` : s.config.url ? `<div class="card-meta">${esc(s.config.url)}</div>` : "";
1371
+ return `
1372
+ <div class="card" data-searchable="${esc(s.name + " " + (intro?.serverInfo?.name ?? ""))}">
1373
+ <div class="card-title">
1374
+ <span>${esc(s.name)}${serverVersion}</span>
1375
+ <span class="status ${statusClass}">${statusText}</span>
1376
+ <span class="scope-badge scope-badge--${s.source}">${s.source}</span>
1377
+ </div>
1378
+ ${configHtml}
1379
+ ${errorHtml}
1380
+ ${toolsHtml}
1381
+ ${resourcesHtml}
1382
+ ${promptsHtml}
1383
+ </div>`;
1384
+ }).join("");
1385
+ const totalTools = servers.reduce(
1386
+ (sum, s) => sum + (s.introspection?.tools.length ?? 0),
1387
+ 0
1388
+ );
1389
+ return `
1390
+ <div class="section">
1391
+ <div class="section-header">
1392
+ <h2>MCP Servers</h2>
1393
+ <div>
1394
+ <span class="count">${servers.length} servers &middot; ${totalTools} tools</span>
1395
+ <span class="arrow">&#9660;</span>
1396
+ </div>
1397
+ </div>
1398
+ <div class="section-content">${cards}</div>
1399
+ </div>`;
1400
+ }
1401
+ function renderContextFiles(files) {
1402
+ if (files.length === 0) return "";
1403
+ const groups = /* @__PURE__ */ new Map();
1404
+ for (const f of files) {
1405
+ const existing = groups.get(f.tool) ?? [];
1406
+ existing.push(f);
1407
+ groups.set(f.tool, existing);
1408
+ }
1409
+ let groupsHtml = "";
1410
+ for (const [tool, toolFiles] of groups) {
1411
+ const items = toolFiles.map((f) => {
1412
+ const previewHtml = f.preview ? `<button class="preview-toggle">Ver contenido</button><div class="preview">${esc(f.preview)}</div>` : "";
1413
+ const errorHtml = f.error ? `<span class="status status--error">${f.error}</span>` : "";
1414
+ const sizeStr = f.size > 0 ? formatBytes(f.size) : "";
1415
+ const typeIcon = f.type === "directory" ? "&#128193;" : "&#128196;";
1416
+ const childrenHtml = f.children && f.children.length > 0 ? `<div class="card-meta" style="margin-top:0.3rem">${f.children.length} archivos dentro</div>` : "";
1417
+ return `
1418
+ <div class="card" data-searchable="${esc(f.path + " " + tool)}">
1419
+ <div class="card-title">${typeIcon} ${esc(f.path)} ${errorHtml}</div>
1420
+ <div class="card-meta">${sizeStr} &middot; <span class="scope-badge scope-badge--${f.scope}">${f.scope}</span></div>
1421
+ ${childrenHtml}
1422
+ ${previewHtml}
1423
+ </div>`;
1424
+ }).join("");
1425
+ groupsHtml += `
1426
+ <div class="tool-group">
1427
+ <div class="tool-group-header">
1428
+ <span class="tool-badge tool-badge--${tool}">${tool}</span>
1429
+ <span class="count">${toolFiles.length} archivos</span>
1430
+ </div>
1431
+ ${items}
1432
+ </div>`;
1433
+ }
1434
+ return `
1435
+ <div class="section">
1436
+ <div class="section-header">
1437
+ <h2>Archivos de Contexto</h2>
1438
+ <div>
1439
+ <span class="count">${files.length} archivos &middot; ${groups.size} herramientas</span>
1440
+ <span class="arrow">&#9660;</span>
1441
+ </div>
1442
+ </div>
1443
+ <div class="section-content">${groupsHtml}</div>
1444
+ </div>`;
1445
+ }
1446
+ function renderSkills(skills) {
1447
+ if (skills.length === 0) return "";
1448
+ const items = skills.map((s) => {
1449
+ const triggersHtml = s.triggers ? `<div class="card-meta">${s.triggers.map((t) => esc(t)).join(", ")}</div>` : "";
1450
+ return `
1451
+ <div class="card" data-searchable="${esc(s.name + " " + (s.description ?? ""))}">
1452
+ <div class="card-title">
1453
+ ${esc(s.name)}
1454
+ <span class="scope-badge scope-badge--${s.scope}">${s.scope}</span>
1455
+ </div>
1456
+ ${s.description ? `<div class="card-meta">${esc(s.description)}</div>` : ""}
1457
+ ${triggersHtml}
1458
+ </div>`;
1459
+ }).join("");
1460
+ return `
1461
+ <div class="section">
1462
+ <div class="section-header">
1463
+ <h2>Skills</h2>
1464
+ <div>
1465
+ <span class="count">${skills.length}</span>
1466
+ <span class="arrow">&#9660;</span>
1467
+ </div>
1468
+ </div>
1469
+ <div class="section-content">${items}</div>
1470
+ </div>`;
1471
+ }
1472
+ function renderMemories(memories) {
1473
+ if (memories.length === 0) return "";
1474
+ const items = memories.map((m) => {
1475
+ const detailsHtml = m.details ? `<div class="card-meta">${Object.entries(m.details).map(([k, v]) => `${esc(k)}: ${esc(String(v))}`).join(" &middot; ")}</div>` : "";
1476
+ return `
1477
+ <div class="card" data-searchable="${esc(m.type)}">
1478
+ <div class="card-title">
1479
+ ${esc(m.type)}
1480
+ <span class="status status--${m.status}">${m.status}</span>
1481
+ </div>
1482
+ <div class="card-meta">${m.source}${m.path ? ` &middot; ${esc(m.path)}` : ""}</div>
1483
+ ${detailsHtml}
1484
+ </div>`;
1485
+ }).join("");
1486
+ return `
1487
+ <div class="section">
1488
+ <div class="section-header">
1489
+ <h2>Memorias</h2>
1490
+ <div>
1491
+ <span class="count">${memories.length}</span>
1492
+ <span class="arrow">&#9660;</span>
1493
+ </div>
1494
+ </div>
1495
+ <div class="section-content">${items}</div>
1496
+ </div>`;
1497
+ }
1498
+ function renderEmptyState() {
1499
+ return `
1500
+ <div class="empty-state">
1501
+ <h3>No se encontr\xF3 configuraci\xF3n AI en este proyecto</h3>
1502
+ <p>Este proyecto no tiene archivos de configuraci\xF3n de herramientas AI.</p>
1503
+ <p style="margin-top:1rem;font-size:0.85rem">
1504
+ Herramientas soportadas: Claude, Cursor, Windsurf, Copilot, Gemini,
1505
+ Codex, Aider, Cline, Continue, Amazon Q, Augment, Replit, Firebase Studio
1506
+ </p>
1507
+ </div>`;
1508
+ }
1509
+
1510
+ // src/report/generator.ts
1511
+ function computeSummary(result) {
1512
+ return {
1513
+ totalMcpServers: result.mcpServers.length,
1514
+ totalTools: result.mcpServers.reduce(
1515
+ (sum, s) => sum + (s.introspection?.tools.length ?? 0),
1516
+ 0
1517
+ ),
1518
+ totalFiles: result.contextFiles.length,
1519
+ totalSkills: result.skills.length,
1520
+ totalMemories: result.memories.length
1521
+ };
1522
+ }
1523
+ function generateHtml(result) {
1524
+ const summary = computeSummary(result);
1525
+ const isEmpty = summary.totalMcpServers === 0 && summary.totalFiles === 0 && summary.totalSkills === 0 && summary.totalMemories === 0;
1526
+ const header = renderHeader(result.project, summary, result.scanDuration);
1527
+ const content = isEmpty ? renderEmptyState() : [
1528
+ renderMcpServers(result.mcpServers),
1529
+ renderContextFiles(result.contextFiles),
1530
+ renderSkills(result.skills),
1531
+ renderMemories(result.memories)
1532
+ ].join("");
1533
+ return `<!DOCTYPE html>
1534
+ <html lang="es">
1535
+ <head>
1536
+ <meta charset="UTF-8">
1537
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
1538
+ <title>AI Context Inspector \u2014 ${escHtml(result.project.name)}</title>
1539
+ <style>${CSS_STYLES}</style>
1540
+ </head>
1541
+ <body>
1542
+ <div class="container">
1543
+ ${header}
1544
+ <div class="search-bar">
1545
+ <input type="text" id="search-input" placeholder="Buscar tools, archivos, skills..." />
1546
+ </div>
1547
+ ${content}
1548
+ <footer class="footer">
1549
+ Generado por ai-context-inspector &mdash; ${new Date(result.project.scannedAt).toLocaleString()}
1550
+ </footer>
1551
+ </div>
1552
+ <script>${JS_SCRIPTS}</script>
1553
+ </body>
1554
+ </html>`;
1555
+ }
1556
+ function escHtml(str) {
1557
+ return str.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
1558
+ }
1559
+
1560
+ export {
1561
+ scanMcpConfigs,
1562
+ introspectServers,
1563
+ runAllScanners,
1564
+ generateHtml
1565
+ };