shakers-cli 0.0.0-stage → 0.14.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.
Files changed (237) hide show
  1. package/LICENSE +59 -0
  2. package/README.md +1233 -2
  3. package/bin/add-agent.js +88 -0
  4. package/bin/add-project.js +88 -0
  5. package/bin/add-role.js +61 -0
  6. package/bin/add-skill.js +88 -0
  7. package/bin/ai-usage.js +99 -0
  8. package/bin/alma.js +99 -0
  9. package/bin/applications.js +69 -0
  10. package/bin/availability.js +70 -0
  11. package/bin/certifications.js +69 -0
  12. package/bin/certify.js +73 -0
  13. package/bin/change-role.js +61 -0
  14. package/bin/config.js +124 -0
  15. package/bin/experiences.js +69 -0
  16. package/bin/find-projects.js +89 -0
  17. package/bin/invitations.js +69 -0
  18. package/bin/lang.js +70 -0
  19. package/bin/login.js +216 -0
  20. package/bin/mcp.js +124 -0
  21. package/bin/mcpb.js +5 -0
  22. package/bin/me.js +69 -0
  23. package/bin/onboarding.js +134 -0
  24. package/bin/portfolios.js +69 -0
  25. package/bin/rate.js +70 -0
  26. package/bin/register.js +337 -0
  27. package/bin/report-html.js +134 -0
  28. package/bin/save.js +77 -0
  29. package/bin/shakers.js +363 -0
  30. package/bin/share.js +72 -0
  31. package/bin/show-project.js +72 -0
  32. package/bin/socials.js +70 -0
  33. package/bin/superadmin.js +351 -0
  34. package/bin/update-check-worker.js +70 -0
  35. package/bin/update.js +111 -0
  36. package/package.json +40 -4
  37. package/src/agent-category.js +13 -0
  38. package/src/agent-evaluation.js +177 -0
  39. package/src/agent-org-chart.js +355 -0
  40. package/src/agent-synthesis.js +132 -0
  41. package/src/agent-usage.js +118 -0
  42. package/src/agents-client.js +165 -0
  43. package/src/ai-fluency-cell.js +72 -0
  44. package/src/ai-profile-client.js +45 -0
  45. package/src/ai-profile-preview.js +154 -0
  46. package/src/ai.js +66 -0
  47. package/src/alma-client.js +123 -0
  48. package/src/alma-flow.js +32 -0
  49. package/src/alma-repo-context.js +47 -0
  50. package/src/alma-shell.js +161 -0
  51. package/src/ansi.js +62 -0
  52. package/src/applications-client.js +79 -0
  53. package/src/applications-flow.js +58 -0
  54. package/src/auth-client.js +144 -0
  55. package/src/auth-device-client.js +118 -0
  56. package/src/auth-method-picker.js +44 -0
  57. package/src/auth-methods.js +35 -0
  58. package/src/auth-session-store.js +215 -0
  59. package/src/authorship-mismatch.js +62 -0
  60. package/src/authorship.js +204 -0
  61. package/src/automations-detector.js +170 -0
  62. package/src/availability-client.js +93 -0
  63. package/src/availability-flow.js +124 -0
  64. package/src/backend-request.js +191 -0
  65. package/src/blind-approval-extract.js +48 -0
  66. package/src/brand-ansi.js +14 -0
  67. package/src/brand-svg.js +9 -0
  68. package/src/browser-tools-detector.js +21 -0
  69. package/src/build-next-level.js +56 -0
  70. package/src/certifications-client.js +89 -0
  71. package/src/certifications-flow.js +75 -0
  72. package/src/certify-dimension-client.js +163 -0
  73. package/src/certify-dimension-flow.js +135 -0
  74. package/src/certify-disclaimer.js +51 -0
  75. package/src/certify-remediation-prompt.js +23 -0
  76. package/src/cli-args.js +83 -0
  77. package/src/command-audit.js +105 -0
  78. package/src/config-dir.js +120 -0
  79. package/src/config.js +877 -0
  80. package/src/consent-flow.js +115 -0
  81. package/src/consent-skip.js +27 -0
  82. package/src/cv-file.js +62 -0
  83. package/src/cv-text.js +92 -0
  84. package/src/decisions-extract.js +43 -0
  85. package/src/detect-repos.js +145 -0
  86. package/src/detected-tools.js +15 -0
  87. package/src/detection-evidence.js +105 -0
  88. package/src/detection-noise.js +132 -0
  89. package/src/detectors.js +221 -0
  90. package/src/device-auth-store.js +100 -0
  91. package/src/device-login.js +120 -0
  92. package/src/email-verification.js +152 -0
  93. package/src/env-paths.js +30 -0
  94. package/src/find-projects-cache.js +89 -0
  95. package/src/find-projects-client.js +148 -0
  96. package/src/find-projects-flow.js +164 -0
  97. package/src/git-activity.js +124 -0
  98. package/src/git-log-prefetch.js +53 -0
  99. package/src/graph-certs.js +58 -0
  100. package/src/graph-generator.js +230 -0
  101. package/src/graph-scan.js +239 -0
  102. package/src/i18n.js +3341 -0
  103. package/src/inter-fonts.js +6 -0
  104. package/src/interactive-select.js +183 -0
  105. package/src/interview-client.js +226 -0
  106. package/src/interview-transcript.js +56 -0
  107. package/src/inventory-client.js +90 -0
  108. package/src/invitations-client.js +66 -0
  109. package/src/invitations-flow.js +50 -0
  110. package/src/lang-client.js +147 -0
  111. package/src/lang-codes.js +14 -0
  112. package/src/lang-flow.js +85 -0
  113. package/src/legal-notice.js +53 -0
  114. package/src/locale.js +72 -0
  115. package/src/markdown-ansi.js +50 -0
  116. package/src/maturity.js +111 -0
  117. package/src/mcp-ai-usage-result-tool.js +111 -0
  118. package/src/mcp-ai-usage-tool.js +401 -0
  119. package/src/mcp-alma-tools.js +95 -0
  120. package/src/mcp-applications-tools.js +44 -0
  121. package/src/mcp-auth-form.js +480 -0
  122. package/src/mcp-availability-tools.js +45 -0
  123. package/src/mcp-certifications-tools.js +44 -0
  124. package/src/mcp-certify-dimension-tools.js +66 -0
  125. package/src/mcp-choice.js +95 -0
  126. package/src/mcp-detector.js +223 -0
  127. package/src/mcp-device-login-tools.js +177 -0
  128. package/src/mcp-find-projects-tools.js +69 -0
  129. package/src/mcp-install.js +275 -0
  130. package/src/mcp-instructions.js +27 -0
  131. package/src/mcp-invitations-tools.js +44 -0
  132. package/src/mcp-lang-tools.js +41 -0
  133. package/src/mcp-onboarding-tools.js +379 -0
  134. package/src/mcp-portfolios-tools.js +50 -0
  135. package/src/mcp-profile-tools.js +48 -0
  136. package/src/mcp-prompts.js +73 -0
  137. package/src/mcp-rate-tools.js +44 -0
  138. package/src/mcp-register-tools.js +441 -0
  139. package/src/mcp-report-tools.js +105 -0
  140. package/src/mcp-role-tools.js +372 -0
  141. package/src/mcp-saved-positions-tools.js +59 -0
  142. package/src/mcp-server.js +290 -0
  143. package/src/mcp-show-project-tools.js +52 -0
  144. package/src/mcp-signup-client.js +118 -0
  145. package/src/mcp-signup-tools.js +1171 -0
  146. package/src/mcp-socials-tools.js +41 -0
  147. package/src/memory-structure-detector.js +108 -0
  148. package/src/model-call-record.js +198 -0
  149. package/src/onboarding-availability.js +26 -0
  150. package/src/onboarding-client.js +374 -0
  151. package/src/onboarding-flow.js +1162 -0
  152. package/src/onboarding-funnel.js +272 -0
  153. package/src/onboarding-livekit-client.js +102 -0
  154. package/src/onboarding-status-client.js +57 -0
  155. package/src/open-file.js +32 -0
  156. package/src/osc-link.js +16 -0
  157. package/src/portfolio-client.js +164 -0
  158. package/src/portfolio-git-info.js +61 -0
  159. package/src/portfolios-client.js +85 -0
  160. package/src/portfolios-flow.js +79 -0
  161. package/src/profile-client.js +133 -0
  162. package/src/profile-flow.js +115 -0
  163. package/src/profile-switch.js +46 -0
  164. package/src/prompt-select.js +66 -0
  165. package/src/rate-client.js +81 -0
  166. package/src/rate-flow.js +137 -0
  167. package/src/register-context.js +145 -0
  168. package/src/render-ai-fluency.js +266 -0
  169. package/src/render-certification.js +177 -0
  170. package/src/render-html.js +140 -0
  171. package/src/render-report-text.js +27 -0
  172. package/src/render-sheet.js +261 -0
  173. package/src/render-terminal.js +109 -0
  174. package/src/render-usage-report-text.js +155 -0
  175. package/src/repo-context.js +215 -0
  176. package/src/report-gating.js +49 -0
  177. package/src/report-store.js +226 -0
  178. package/src/report-theme.js +177 -0
  179. package/src/report-view-client.js +50 -0
  180. package/src/reveal-output.js +29 -0
  181. package/src/roadmap-content.js +453 -0
  182. package/src/roadmap-personalization.js +132 -0
  183. package/src/roadmap-prompt.js +154 -0
  184. package/src/roles-client.js +112 -0
  185. package/src/roles-flow.js +192 -0
  186. package/src/sanitize-network-text.js +36 -0
  187. package/src/saved-positions-client.js +71 -0
  188. package/src/saved-positions-flow.js +50 -0
  189. package/src/scan-exclusions.js +50 -0
  190. package/src/scanner.js +624 -0
  191. package/src/scope-signals.js +148 -0
  192. package/src/secret-prompt.js +72 -0
  193. package/src/session-gate.js +30 -0
  194. package/src/session-normalizers.js +174 -0
  195. package/src/session-refresh.js +51 -0
  196. package/src/session-scan.js +475 -0
  197. package/src/session-signals.js +36 -0
  198. package/src/shakers-wordmark.js +17 -0
  199. package/src/share-card.js +362 -0
  200. package/src/share-certification.js +111 -0
  201. package/src/share-consent-state.js +157 -0
  202. package/src/share-payload.js +300 -0
  203. package/src/share.js +185 -0
  204. package/src/show-project-client.js +58 -0
  205. package/src/show-project-flow.js +119 -0
  206. package/src/signup-client.js +103 -0
  207. package/src/signup-copy.js +450 -0
  208. package/src/signup-language.js +21 -0
  209. package/src/skills-client.js +102 -0
  210. package/src/socials-client.js +91 -0
  211. package/src/socials-flow.js +83 -0
  212. package/src/specificity-extract.js +41 -0
  213. package/src/start-add-agents.js +306 -0
  214. package/src/start-add-portfolio.js +388 -0
  215. package/src/start-add-skills.js +260 -0
  216. package/src/stdin-ask.js +129 -0
  217. package/src/steering-extract.js +43 -0
  218. package/src/strip-internal-comments.js +45 -0
  219. package/src/tech-detector.js +295 -0
  220. package/src/templates/report-sheet.html +572 -0
  221. package/src/terminal-agents.js +207 -0
  222. package/src/terminal-format.js +85 -0
  223. package/src/terminal-progress.js +106 -0
  224. package/src/terminal-sections.js +219 -0
  225. package/src/tier-analysis.js +126 -0
  226. package/src/tier-engine.js +114 -0
  227. package/src/tier-projection.js +117 -0
  228. package/src/transcript-turns.js +49 -0
  229. package/src/translations-client.js +84 -0
  230. package/src/typewriter.js +154 -0
  231. package/src/update-flow.js +223 -0
  232. package/src/update-notifier.js +113 -0
  233. package/src/usage-model-calls.js +119 -0
  234. package/src/usage-oneshot-commands.js +180 -0
  235. package/src/usage-repo-scope.js +162 -0
  236. package/src/usage-run-steps.js +354 -0
  237. package/src/work-streams.js +79 -0
@@ -0,0 +1,239 @@
1
+ 'use strict';
2
+
3
+ // graph-scan.js — the DETECTOR → scan adapter for the LOCAL report (`map`).
4
+
5
+ const path = require('path');
6
+ const { scan } = require('./scanner');
7
+ const { classify } = require('./maturity');
8
+ const { setupLevelForTier } = require('./tier-engine');
9
+ const { sanitizeRenderText } = require('./sanitize-network-text');
10
+ // The ONE agent-shaping layer, shared with the terminal report (issue 089).
11
+ const { buildAgentCardTree } = require('./render-html');
12
+
13
+ function slug(s) {
14
+ return String(s || '')
15
+ .toLowerCase()
16
+ .replace(/[^a-z0-9]+/g, '-')
17
+ .replace(/^-+|-+$/g, '')
18
+ .slice(0, 48) || 'x';
19
+ }
20
+
21
+ // Bare Claude aliases → the current EXACT model id (Claude 5 family + Opus 4.8 + Haiku 4.5).
22
+ const CLAUDE_ALIASES = {
23
+ opus: 'claude-opus-4-8',
24
+ sonnet: 'claude-sonnet-5',
25
+ haiku: 'claude-haiku-4-5-20251001',
26
+ };
27
+
28
+ // Normalize a raw agent `model` string to a graph model node keyed by the EXACT model id (one node per distinct exact id — never collapsed to a vendor family).
29
+ function modelNode(raw) {
30
+ const original = String(raw || '').trim();
31
+ const m = original.toLowerCase();
32
+ if (!m) return null;
33
+ // `inherit` (or the session default): be honest — no specific id invented.
34
+ if (m === 'inherit' || m === 'default') return { id: 'inherit', label: 'inherit', domain: null };
35
+ // Bare Claude alias → exact id.
36
+ if (CLAUDE_ALIASES[m]) return { id: CLAUDE_ALIASES[m], label: CLAUDE_ALIASES[m], domain: 'claude.ai' };
37
+ // Already an exact/qualified id — keep it verbatim, key by it.
38
+ if (/claude/.test(m)) return { id: m, label: original, domain: 'claude.ai' };
39
+ if (/gemini/.test(m)) return { id: m, label: original, domain: 'gemini.google.com' };
40
+ if (/gpt|o1|o3|openai/.test(m)) return { id: m, label: original, domain: 'openai.com' };
41
+ // unknown but present: keep it as a node so the agent has a call target
42
+ return { id: slug(m), label: original, domain: null };
43
+ }
44
+
45
+ // Light, deterministic store hints from technologies (fed to the LLM pass only
46
+ // as hints; never emitted as authoritative store nodes here).
47
+ function storeHints(technologies) {
48
+ const t = (technologies || []).map((x) => String(x).toLowerCase());
49
+ const hints = [];
50
+ if (t.some((x) => /prisma|postgres|pg\b/.test(x))) hints.push('postgresql');
51
+ if (t.some((x) => /mongo/.test(x))) hints.push('mongodb');
52
+ if (t.some((x) => /redis/.test(x))) hints.push('redis');
53
+ if (t.some((x) => /s3|aws-sdk/.test(x))) hints.push('aws-s3');
54
+ return hints;
55
+ }
56
+
57
+ function buildGraphScan(root, { scanFn = scan, classifyFn = classify } = {}) {
58
+ const abs = path.resolve(root || process.cwd());
59
+ const report = scanFn({ root: abs });
60
+ const maturity = classifyFn(report);
61
+
62
+ const agentsRaw = Array.isArray(report.agents) ? report.agents : [];
63
+ const technologies = Array.isArray(report.technologies) ? report.technologies : [];
64
+
65
+ // agents (+ their models), deduped by slug(name)
66
+ const seenAgent = new Set();
67
+ const agents = [];
68
+ const modelsById = new Map();
69
+ const nameToId = new Map();
70
+ for (const a of agentsRaw) {
71
+ const id = slug(a.name);
72
+ if (!a.name || seenAgent.has(id)) continue;
73
+ seenAgent.add(id);
74
+ nameToId.set(a.name, id);
75
+ const mn = modelNode(a.model);
76
+ if (mn && !modelsById.has(mn.id)) modelsById.set(mn.id, mn);
77
+ agents.push({
78
+ id,
79
+ label: a.name,
80
+ _parentName: a.parent || null,
81
+ ...(mn ? { model: mn.id } : {}),
82
+ ...(a.aiProduct ? { group: a.aiProduct } : {}),
83
+ // Sub-label = the EXACT resolved model id (not the raw alias like "opus").
84
+ ...(mn ? { sub: mn.label } : a.aiProduct ? { sub: a.aiProduct } : {}),
85
+ });
86
+ }
87
+ // second pass: resolve parent NAME -> parent agent id (orchestrator hierarchy)
88
+ for (const ag of agents) {
89
+ const pid = ag._parentName ? nameToId.get(ag._parentName) : null;
90
+ if (pid && pid !== ag.id) ag.parent = pid;
91
+ delete ag._parentName;
92
+ }
93
+
94
+ const scanOut = {
95
+ project: {
96
+ name: path.basename(abs) || 'project',
97
+ slug: slug(path.basename(abs)),
98
+ date: new Date().toISOString().slice(0, 10),
99
+ },
100
+ agents,
101
+ models: Array.from(modelsById.values()),
102
+ tools: [],
103
+ integrations: [],
104
+ technologies,
105
+ // hints for the LLM enrichment pass (structural only)
106
+ entrypoints: [],
107
+ stores: storeHints(technologies),
108
+ };
109
+
110
+ return {
111
+ scan: scanOut,
112
+ footprint: buildFootprintDrawer(report, maturity),
113
+ report,
114
+ maturity,
115
+ };
116
+ }
117
+
118
+ // Setup-level ladder for the drawer (ADR-016): the 3 Setup Levels + "Not certified", replacing the retired 0-4 maturity ladder.
119
+ const SETUP_LADDER = [
120
+ { rank: 0, key: 'none', label: 'Sin certificar' },
121
+ { rank: 1, key: 'S1', label: 'S1 · Asistido' },
122
+ { rank: 2, key: 'S2', label: 'S2 · Extendido' },
123
+ { rank: 3, key: 'S3', label: 'S3 · Orquestado' },
124
+ ];
125
+
126
+ // #3 footprint drawer payload from the live scan (defensive). Detected agents =
127
+ // a FLAT list with a depth number, not a tree object (issue 089).
128
+ function drawerAgents(report, t) {
129
+ let tree;
130
+ try {
131
+ tree = buildAgentCardTree(report, t);
132
+ } catch {
133
+ return [];
134
+ }
135
+ // WHICH agents an evaluation actually covered.
136
+ const evaluatedNames = new Set(
137
+ report && report.agentEvaluation && Array.isArray(report.agentEvaluation.evaluations)
138
+ ? report.agentEvaluation.evaluations
139
+ .filter((e) => e && typeof e.name === 'string')
140
+ .map((e) => sanitizeRenderText(e.name))
141
+ : [],
142
+ );
143
+ const rows = [];
144
+ const walk = (card, depth, seen) => {
145
+ if (seen.has(card.name)) return; // malformed parent chain: never loop
146
+ const nextSeen = new Set(seen);
147
+ nextSeen.add(card.name);
148
+ const children = tree.childrenByParent.get(card.name) || [];
149
+ const cls = card.classification && card.classification.catalogId ? card.classification : null;
150
+ rows.push({
151
+ name: sanitizeRenderText(card.name),
152
+ model: card.model ? sanitizeRenderText(card.model) : (card.aiProduct ? sanitizeRenderText(card.aiProduct) : null),
153
+ // `whatItDoes` is already the merged description (synthesis > frontmatter >
154
+ // humanised-name fallback) and already sanitised by the shaping layer.
155
+ whatItDoes: card.whatItDoes || null,
156
+ // The category is a REQUIREMENT of the design, and it is the agent → catalog-seed association, not the Talent's tier.
157
+ category: cls ? cls.category : null,
158
+ // THREE STATES, NOT TWO (issue 106).
159
+ evaluated: evaluatedNames.has(card.name),
160
+ // COPIED from the card, never recomputed (2026-08-03).
161
+ evaluationState: card.evaluationState || (evaluatedNames.has(card.name) ? (cls ? 'classified' : 'unclassified') : 'not-evaluated'),
162
+ role: cls && cls.role ? sanitizeRenderText(cls.role) : null,
163
+ depth,
164
+ hasChildren: children.length > 0,
165
+ });
166
+ for (const child of children) walk(child, depth + 1, nextSeen);
167
+ };
168
+ for (const root of tree.roots) walk(root, 0, new Set());
169
+ return rows;
170
+ }
171
+
172
+ function buildFootprintDrawer(report, maturity, t) {
173
+ const score = maturity && typeof maturity.score === 'number' ? maturity.score : 0;
174
+ const tierKey = (maturity && (maturity.tierKey || maturity.key)) || '';
175
+ const tierNum = maturity && typeof maturity.tier === 'number'
176
+ ? maturity.tier
177
+ : (/^T([0-7])$/.test(tierKey) ? Number(tierKey.slice(1)) : null);
178
+ const tierName = (maturity && maturity.tierName) || '—';
179
+ // Setup Level (ADR-016) replaces the 0-4 band as the drawer's headline rollup.
180
+ const setup = (maturity && maturity.setupLevel)
181
+ || (tierNum != null ? setupLevelForTier(tierNum) : { key: 'none', code: null, rank: 0, emoji: '○' });
182
+ const setupLabel = (SETUP_LADDER.find((s) => s.key === setup.key) || SETUP_LADDER[0]).label;
183
+ // Issue 055: this drawer payload feeds the shareable sheet AND the graph report, and both only `esc()`.
184
+ const technologies = (Array.isArray(report.technologies) ? report.technologies : [])
185
+ .map((x) => (typeof x === 'string' ? sanitizeRenderText(x) : x));
186
+ // Installed AI dev tools (assistants).
187
+ const toolList = Array.isArray(report.tools)
188
+ ? report.tools
189
+ : report.tools && typeof report.tools === 'object'
190
+ ? Object.entries(report.tools).map(([id, v]) => ({ id, ...(v && typeof v === 'object' ? v : {}) }))
191
+ : [];
192
+ const tools = [];
193
+ for (const t of toolList) {
194
+ if (!t || !(t.detected || t.installed || t.present)) continue;
195
+ const nm = (typeof t.name === 'string' && t.name) || prettyTool(t.id || t.key || '');
196
+ if (nm) tools.push(sanitizeRenderText(nm));
197
+ }
198
+
199
+ return {
200
+ score,
201
+ // `setup` is the ADR-016 rollup shown in the drawer hero; `tier` keeps the tierKey · tierName chip.
202
+ setup: { key: setup.key, code: setup.code, rank: setup.rank, emoji: setup.emoji, label: setupLabel },
203
+ tier: { key: tierKey || 'none', name: tierName, label: tierKey ? `${tierKey} · ${tierName}` : tierName },
204
+ ladder: SETUP_LADDER,
205
+ // "Lectura": a deterministic, human-readable one-liner from the scan (no LLM,
206
+ // no fabricated content). Previously hardcoded '' → an empty box in the drawer.
207
+ summary: buildReading({ setupLabel, tierKey, tierName, score, nTools: tools.length, nTech: technologies.length }),
208
+ tools,
209
+ technologies,
210
+ // Issue 089: ALL detected agents, always.
211
+ agents: drawerAgents(report, t),
212
+ // Issue 110: the SERVICES behind the MCP servers, shaped ONCE here for both consumers of this drawer (the shareable sheet and the graph report), the same way `agents` is.
213
+ mcp: {
214
+ services: (report.mcp && Array.isArray(report.mcp.services) ? report.mcp.services : [])
215
+ .map((x) => ({ label: sanitizeRenderText(x.label), count: x.count })),
216
+ unidentified: (report.mcp && typeof report.mcp.unidentified === 'number') ? report.mcp.unidentified : 0,
217
+ total: (report.mcp && typeof report.mcp.total === 'number') ? report.mcp.total : 0,
218
+ },
219
+ };
220
+ }
221
+
222
+ // Deterministic Spanish reading of the footprint (the drawer is es), composed ONLY from real scan numbers — never invented prose.
223
+ function buildReading({ setupLabel, tierKey, tierName, score, nTools, nTech }) {
224
+ const tier = tierKey ? `${tierKey} · ${tierName}` : tierName;
225
+ const toolsPart = nTools > 0 ? `${nTools} herramienta${nTools === 1 ? '' : 's'} de IA detectada${nTools === 1 ? '' : 's'}` : 'sin herramientas de IA detectadas';
226
+ const techPart = nTech > 0 ? `${nTech} tecnología${nTech === 1 ? '' : 's'} en el proyecto` : 'sin tecnologías detectadas';
227
+ return `Nivel de setup ${setupLabel} (${tier}), score ${score}/100. ${toolsPart[0].toUpperCase()}${toolsPart.slice(1)} y ${techPart}.`;
228
+ }
229
+
230
+ function prettyTool(key) {
231
+ const map = {
232
+ 'claude-code': 'Claude Code', cursor: 'Cursor', 'github-copilot': 'GitHub Copilot',
233
+ windsurf: 'Windsurf', aider: 'Aider', continue: 'Continue', 'gemini-cli': 'Gemini CLI',
234
+ 'codex-cli': 'Codex CLI', trae: 'Trae',
235
+ };
236
+ return map[key] || key;
237
+ }
238
+
239
+ module.exports = { buildGraphScan, buildFootprintDrawer, modelNode, slug };