shakers-cli 0.0.0-stage → 0.14.1

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,109 @@
1
+ 'use strict';
2
+
3
+ const { getCatalog, categoryLabel } = require('./i18n');
4
+ const { sanitizeRenderText } = require('./sanitize-network-text');
5
+ const { DEFAULT_REPORT_GATE } = require('./report-gating');
6
+ const { c, bar, sep, wrap } = require('./terminal-format');
7
+ const { detectedTools } = require('./detected-tools');
8
+ const { printAgents, printActivity } = require('./terminal-agents');
9
+ const {
10
+ printMcpServices,
11
+ printTierAnalysis,
12
+ printLadder,
13
+ printRoadmap,
14
+ } = require('./terminal-sections');
15
+
16
+ // talents-ai-score: terminal parity with the HTML report.
17
+
18
+ // `lang` ('es'|'en', see src/i18n.js) decides the text catalog.
19
+ function renderTerminal(report, maturity, lang, opts = {}) {
20
+ const t = getCatalog(lang);
21
+ const lines = [];
22
+ const p = (s = '') => lines.push(s);
23
+
24
+ // issue 123 / ADR-044: the ONE identity gate.
25
+ const gate = opts.gate || DEFAULT_REPORT_GATE;
26
+
27
+ // ADR-016: Setup Level (S1-S3 / Not certified) replaces the 0-4 band label.
28
+ const setupKey = (maturity.setupLevel && maturity.setupLevel.key) || 'none';
29
+ const setupLabel = (t.setupLevels && t.setupLevels[setupKey] && t.setupLevels[setupKey].label) || setupKey;
30
+
31
+ // A compact brand header in BOTH modes for orientation.
32
+ p();
33
+ p(`${c.bold}${c.primary} SHAKERS${c.reset}${c.muted} · ${t.terminal.brandSub}${c.reset}`);
34
+ p(`${c.muted} ${new Date(report.generatedAt).toLocaleString()} · ${t.terminal.toolsDetected(detectedTools(report.tools).length, report.tools.length)}${c.reset}`);
35
+
36
+ // `--roadmap` (opts.showRoadmap): render ONLY the next-steps/roadmap section (header + steps + the copyable implementation prompt) — NOT the rest of the report.
37
+ if (opts.showRoadmap) {
38
+ sep(p);
39
+ if (gate.showRoadmap) {
40
+ printRoadmap(report, maturity, t, lang, p);
41
+ }
42
+ return lines.join('\n');
43
+ }
44
+
45
+ // Default report: score -> why -> tools -> technologies -> agents, then a dim hint pointing at --roadmap (so the next-steps section is discoverable).
46
+
47
+ sep(p);
48
+ const tierName = (maturity.tierKey && t.tierNames[maturity.tierKey]) || maturity.tierName || '';
49
+ const tierBit = maturity.tierKey ? t.terminal.tierInline(maturity.tierKey, tierName) : '';
50
+ // This report's ONE headline (dueño, 2026-08-11): the Setup Level IS the evaluation's main result, so it gets `accent` (lime) — and nothing else on this screen does.
51
+ const setupColor = setupKey === 'none' ? c.white : c.accent;
52
+ p(` ${c.bold}${setupColor}${t.terminal.setupLevel(setupLabel)}${c.reset}${c.muted}${tierBit}${c.reset}`);
53
+ p(` ${c.primary}${bar(maturity.score)}${c.reset} ${c.dim}${maturity.score}/100${c.reset}`);
54
+
55
+ // 2. WHY that score/tier — the rationale, right after the number.
56
+ sep(p);
57
+ printTierAnalysis(report, t, p);
58
+
59
+ // 2b. Progression ladder — what each level 0-4 and tier T0-T7 means, and the
60
+ // ✓/●/○ progression with unlock criteria (report req 1, full in terminal).
61
+ sep(p);
62
+ printLadder(report, t, p);
63
+
64
+ // 3. Detected tools — omitted entirely if none detected.
65
+ const detected = detectedTools(report.tools);
66
+ if (detected.length) {
67
+ sep(p);
68
+ p(` ${c.bold}${t.terminal.detectedHeading}${c.reset}`);
69
+ for (const tool of detected) {
70
+ const depthBits = Object.entries(tool.depth)
71
+ .filter(([, v]) => v > 0)
72
+ .map(([k, v]) => `${v} ${k}`)
73
+ .join(', ');
74
+ const extra = depthBits ? `${c.dim} — ${depthBits}${c.reset}` : '';
75
+ const version = tool.version ? `${c.dim} v${sanitizeRenderText(tool.version)}${c.reset}` : '';
76
+ p(` ${c.success}●${c.reset} ${sanitizeRenderText(tool.name)}${version} ${c.muted}(${categoryLabel(lang, tool.category)})${c.reset}${extra}`);
77
+ }
78
+ }
79
+
80
+ // 4b. MCP services (issue 110) — ALWAYS printed, empty state included, because
81
+ // its absence is a tier criterion the talent needs to know about.
82
+ sep(p);
83
+ printMcpServices(report, t, p);
84
+
85
+ // 5. Agents — one line per agent (+ summarized description, ↓ nesting,
86
+ // compact score + usage). Omitted entirely when there are no agents at all.
87
+ const hasAgents = Array.isArray(report.agents) && report.agents.length > 0;
88
+ if (hasAgents) {
89
+ sep(p);
90
+ printAgents(report, t, p, gate);
91
+ }
92
+
93
+ const activityLines = [];
94
+ if (printActivity(report, t, (s = '') => activityLines.push(s))) {
95
+ sep(p);
96
+ for (const line of activityLines) p(line);
97
+ }
98
+
99
+ // Where the roadmap pointer went.
100
+ if (gate.showRoadmap) {
101
+ p(` ${c.dim}${t.terminal.roadmapHint}${c.reset}`);
102
+ }
103
+ p();
104
+
105
+ return lines.join('\n');
106
+ }
107
+
108
+ // `wrap` is re-exported for its own unit test (issue 087): it is the one piece of the formatting with edge cases worth pinning directly.
109
+ module.exports = { renderTerminal, wrap, printAgents };
@@ -0,0 +1,155 @@
1
+ 'use strict';
2
+
3
+ const { detectedTools } = require('./detected-tools');
4
+
5
+ const LABELS = {
6
+ es: {
7
+ title: 'Informe de uso de IA (almacenado)',
8
+ platform: 'Plataforma',
9
+ level: 'Nivel',
10
+ score: 'Puntuación',
11
+ tier: 'Tier',
12
+ detected: 'Detectados',
13
+ categories: 'Categorías',
14
+ tools: 'Herramientas',
15
+ technologies: 'Tecnologías',
16
+ agents: 'Agentes',
17
+ mcp: 'Servidores MCP',
18
+ activity: 'Actividad',
19
+ sessions: 'Sesiones',
20
+ subagentRuns: 'Ejecuciones de subagentes',
21
+ activeHours: 'Horas activas',
22
+ crossToolLinks: 'Enlaces entre herramientas',
23
+ filesTouched: 'Ficheros tocados',
24
+ bashCommands: 'Comandos de shell',
25
+ planningSignals: 'Señales de planificación',
26
+ commits: 'Commits propios',
27
+ steering: 'Correcciones/steering',
28
+ decisions: 'Decisiones deliberadas',
29
+ fluency: 'Nivel de fluidez con IA',
30
+ interaction: 'Nivel de interacción (prompting)',
31
+ veracity: 'veracidad',
32
+ generatedAt: 'Generado',
33
+ none: '(ninguno)',
34
+ notEvaluated: '(sin evaluar todavía)',
35
+ },
36
+ en: {
37
+ title: 'AI usage report (stored)',
38
+ platform: 'Platform',
39
+ level: 'Level',
40
+ score: 'Score',
41
+ tier: 'Tier',
42
+ detected: 'Detected',
43
+ categories: 'Categories',
44
+ tools: 'Tools',
45
+ technologies: 'Technologies',
46
+ agents: 'Agents',
47
+ mcp: 'MCP servers',
48
+ activity: 'Activity',
49
+ sessions: 'Sessions',
50
+ subagentRuns: 'Subagent runs',
51
+ activeHours: 'Active hours',
52
+ crossToolLinks: 'Cross-tool links',
53
+ filesTouched: 'Files touched',
54
+ bashCommands: 'Shell commands',
55
+ planningSignals: 'Planning signals',
56
+ commits: 'Your commits',
57
+ steering: 'Steering/corrections',
58
+ decisions: 'Deliberate decisions',
59
+ fluency: 'AI fluency level',
60
+ interaction: 'Interaction (prompting) level',
61
+ veracity: 'veracity',
62
+ generatedAt: 'Generated',
63
+ none: '(none)',
64
+ notEvaluated: '(not evaluated yet)',
65
+ },
66
+ };
67
+
68
+ function line(out, label, value) {
69
+ out.push(` ${label}: ${value}`);
70
+ }
71
+
72
+ function agentName(a) {
73
+ if (!a || typeof a !== 'object') return null;
74
+ const name = typeof a.name === 'string' ? a.name : null;
75
+ if (!name) return null;
76
+ const role = typeof a.role === 'string' && a.role ? ` — ${a.role}` : '';
77
+ return `${name}${role}`;
78
+ }
79
+
80
+ function evaluationText(evaluation, L) {
81
+ if (!evaluation || typeof evaluation !== 'object' || !evaluation.level) {
82
+ return L.notEvaluated;
83
+ }
84
+ const veracity = evaluation.veracity ? ` (${L.veracity}: ${evaluation.veracity})` : '';
85
+ return `${evaluation.level}${veracity}`;
86
+ }
87
+
88
+ function renderUsageReportText(view, { lang = 'en' } = {}) {
89
+ const L = LABELS[lang] || LABELS.en;
90
+ const report = view && view.report ? view.report : null;
91
+ const out = [];
92
+ out.push('');
93
+ out.push(` ${L.title}`);
94
+ out.push('');
95
+
96
+ if (report) {
97
+ const levelName = typeof report.levelName === 'string' ? report.levelName : '';
98
+ line(out, L.platform, report.platform || '');
99
+ line(out, L.level, levelName ? `${levelName} (${report.level})` : `${report.level}`);
100
+ line(out, L.score, `${report.score}`);
101
+ line(out, L.tier, report.tierKey ? `${report.tierKey}` : `${report.tier ?? '-'}`);
102
+ line(out, L.detected, `${report.totalDetected}`);
103
+ line(out, L.categories, Array.isArray(report.categories) && report.categories.length ? report.categories.join(', ') : L.none);
104
+
105
+ const toolCount = detectedTools(report.tools).length;
106
+ line(out, L.tools, `${toolCount}`);
107
+
108
+ const techs = Array.isArray(report.technologies) ? report.technologies : [];
109
+ line(out, L.technologies, techs.length ? techs.join(', ') : L.none);
110
+
111
+ const mcpCount = report.mcp && typeof report.mcp.count === 'number' ? report.mcp.count : 0;
112
+ line(out, L.mcp, `${mcpCount}`);
113
+
114
+ out.push('');
115
+ out.push(` ${L.agents}:`);
116
+ const agents = Array.isArray(report.agents) ? report.agents.map(agentName).filter(Boolean) : [];
117
+ if (agents.length) {
118
+ for (const a of agents) out.push(` - ${a}`);
119
+ } else {
120
+ out.push(` ${L.none}`);
121
+ }
122
+
123
+ out.push('');
124
+ out.push(` ${L.activity}:`);
125
+ const s = report.sessions;
126
+ if (s && typeof s === 'object') {
127
+ line(out, ` ${L.sessions}`, `${s.sessionCount}`);
128
+ line(out, ` ${L.subagentRuns}`, `${s.subagentRunCount ?? 0}`);
129
+ line(out, ` ${L.activeHours}`, `${s.activeHours ?? 0}`);
130
+ line(out, ` ${L.crossToolLinks}`, `${s.crossToolLinks}`);
131
+ line(out, ` ${L.filesTouched}`, `${s.filesTouchedCount}`);
132
+ line(out, ` ${L.bashCommands}`, `${s.bashCommandsCount}`);
133
+ line(out, ` ${L.planningSignals}`, `${s.planningSignalCount ?? 0}`);
134
+ }
135
+ const git = report.gitActivity;
136
+ if (git && typeof git === 'object') {
137
+ line(out, ` ${L.commits}`, `${git.authoredCommitCount ?? 0}`);
138
+ }
139
+ line(out, ` ${L.steering}`, `${report.steeringTraceCount ?? 0}`);
140
+ line(out, ` ${L.decisions}`, `${report.decisionExchangeCount ?? 0}`);
141
+ }
142
+
143
+ out.push('');
144
+ line(out, L.fluency, evaluationText(view && view.fluency, L));
145
+ line(out, L.interaction, evaluationText(view && view.interactionQuality, L));
146
+
147
+ if (report && report.generatedAt) {
148
+ out.push('');
149
+ line(out, L.generatedAt, report.generatedAt);
150
+ }
151
+ out.push('');
152
+ return `${out.join('\n')}\n`;
153
+ }
154
+
155
+ module.exports = { renderUsageReportText };
@@ -0,0 +1,215 @@
1
+ 'use strict';
2
+
3
+ // repo-context.js — collect a COMPACT, content-free structural context of a codebase for the LLM analysis that builds the LOCAL report's graph (`map`).
4
+
5
+ const fs = require('fs');
6
+ const path = require('path');
7
+ const { scrubString } = require('./graph-generator');
8
+ const { modelNode } = require('./graph-scan'); // exact-model resolver (aliases → exact ids)
9
+
10
+ const SKIP_DIRS = new Set(['node_modules', '.git', 'dist', 'build', 'coverage', '.next', '.turbo', 'vendor', '__snapshots__', '__tests__', '__mocks__', 'test', 'tests', 'e2e']);
11
+ const CODE_EXT = new Set(['.ts', '.js', '.tsx', '.jsx', '.mjs', '.cts', '.mts']);
12
+ const SKIP_FILE_RE = /\.(spec|test|e2e-spec|d)\.[tj]sx?$/;
13
+ const MAX_FILES = 6000;
14
+ const MAX_READ = 700;
15
+ const MAX_BYTES_PER = 200 * 1024;
16
+ const LINE_MAX = 160;
17
+ const CAP = { entrypoints: 12, crons: 18, agents: 30, integrations: 20, stores: 12, services: 40, tech: 60 };
18
+
19
+ const AI_RE = /generateText|streamText|generateObject|streamObject|embedMany|tool\s*\(|@ai-sdk\/|from\s+['"]ai['"]|new\s+OpenAI|new\s+Anthropic|@anthropic-ai\/sdk|GoogleGenerativeAI|@google\/(?:genai|generative-ai)|Gemini\w*Service|Anthropic\w*Service|ShakersAi\w*|\.sendMessage\(|\.messages\.create\(|\.chat\.completions\.create\(|\.generateContent\(|\.responses\.create\(|ChatOpenAI|langchain|llamaindex|createAgent|AgentExecutor/;
20
+ const CRON_RE = /@Cron\b|CronExpression|new\s+CronJob|EVERY_(MINUTE|HOUR|DAY)/;
21
+ const CRON_PATH_RE = /(\.cron\.[tj]s$|(^|\/)cron-jobs?\/|(^|\/)crons?\/)/i;
22
+ const ENTRY_PATH_RE = /(controller|\.route|router|webhook|gateway|resolver|main\.[tj]s|(^|\/)cli\/|(^|\/)commands?\/)/i;
23
+ const IMPORT_RE = /^\s*(import\b|const\b.*=\s*require\()/;
24
+
25
+ // npm import substring -> integration/provider label
26
+ const INTEG = [
27
+ [/stripe/i, 'Stripe'], [/hubspot/i, 'HubSpot'], [/(datadog|dd-trace)/i, 'Datadog'],
28
+ [/twilio/i, 'Twilio'], [/(sendgrid|@sendgrid)/i, 'SendGrid'], [/(@slack|slack-)/i, 'Slack'],
29
+ [/(bigquery|@google-cloud\/bigquery)/i, 'BigQuery'], [/holded/i, 'Holded'], [/adobe/i, 'Adobe PDF'],
30
+ [/n8n/i, 'n8n'], [/cloudfront/i, 'CloudFront'], [/(mongoose|mongodb)/i, 'MongoDB'],
31
+ [/(ioredis|['"]redis['"])/i, 'Redis'], [/@aws-sdk|aws-sdk/i, 'AWS'],
32
+ ];
33
+ // integrations/<dir> -> label (a dir under any `integrations/` path = a wired service)
34
+ const INTEG_DIR = {
35
+ hubspot: 'HubSpot', holded: 'Holded', slack: 'Slack', datadog: 'Datadog', bigquery: 'BigQuery',
36
+ adobe: 'Adobe PDF', 'adobe-pdf': 'Adobe PDF', n8n: 'n8n', cloudfront: 'CloudFront', s3: 'AWS S3',
37
+ stripe: 'Stripe', match: 'calc-match', 'shakers-ai': 'shakers-ai-api', 'sh-ai': 'shakers-ai-api',
38
+ twilio: 'Twilio', sendgrid: 'SendGrid',
39
+ };
40
+ const MODEL_DIRS = new Set(['gemini', 'anthropic', 'openai']); // integration dirs that are MODELS, not externals
41
+
42
+ function slug(s) { return String(s || '').toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '').slice(0, 48) || 'x'; }
43
+ function clip(s) { s = String(s || '').trim(); return s.length > LINE_MAX ? s.slice(0, LINE_MAX) + '…' : s; }
44
+ function titleize(s) { return String(s || '').replace(/[-_]+/g, ' ').replace(/\.(service|controller|cron|module)$/i, '').trim().replace(/\b\w/g, (c) => c.toUpperCase()).slice(0, 40); }
45
+ function readSafe(p) { try { const st = fs.statSync(p); if (st.size > MAX_BYTES_PER) return null; return fs.readFileSync(p, 'utf8'); } catch { return null; } }
46
+
47
+ function providerFromLine(line) {
48
+ const l = String(line || '').toLowerCase();
49
+ if (/gemini|generatecontent|google.*genai|generativeai/.test(l)) return { label: 'Gemini', domain: 'gemini.google.com' };
50
+ if (/anthropic|claude|messages\.create/.test(l)) return { label: 'Claude', domain: 'claude.ai' };
51
+ if (/openai|chat\.completions|responses\.create|\bgpt\b/.test(l)) return { label: 'OpenAI', domain: 'openai.com' };
52
+ if (/shakersai|sh_ai|shakers-ai|chatgpt/.test(l)) return { label: 'shakers-ai-api', domain: null };
53
+ return null;
54
+ }
55
+
56
+ function walk(root) {
57
+ const files = [];
58
+ const stack = [root];
59
+ while (stack.length && files.length < MAX_FILES) {
60
+ const dir = stack.pop();
61
+ let entries;
62
+ try { entries = fs.readdirSync(dir, { withFileTypes: true }); } catch { continue; }
63
+ for (const e of entries) {
64
+ if (files.length >= MAX_FILES) break;
65
+ if (e.name.startsWith('.') && e.name !== '.claude') continue;
66
+ const full = path.join(dir, e.name);
67
+ if (e.isDirectory()) { if (!SKIP_DIRS.has(e.name)) stack.push(full); }
68
+ else if (CODE_EXT.has(path.extname(e.name)) && !SKIP_FILE_RE.test(e.name)) files.push(full);
69
+ }
70
+ }
71
+ return files;
72
+ }
73
+
74
+ function entryGroup(rel) {
75
+ const p = rel.toLowerCase();
76
+ const app = (p.match(/(?:^|\/)apps\/([^/]+)\//) || [])[1];
77
+ if (app) return { key: 'app:' + app, label: titleize(app) + ' API', sub: `apps/${app}` };
78
+ const integ = (p.match(/(?:^|\/)integrations\/([^/]+)\/.*(webhook|controller)/) || [])[1];
79
+ if (integ) return { key: 'wh:' + integ, label: titleize(integ) + ' webhook', sub: 'inbound webhook' };
80
+ if (/gateway|websocket|\bws\b/.test(p)) return { key: 'ws', label: 'WebSocket gateway', sub: 'realtime' };
81
+ if (/(^|\/)cli\/|(^|\/)commands?\//.test(p)) return { key: 'cli', label: 'CLI', sub: 'commands' };
82
+ if (/main\.[tj]s$/.test(p)) return { key: 'http', label: 'HTTP server', sub: 'main' };
83
+ if (/webhook/.test(p)) return { key: 'wh', label: 'Webhook', sub: 'inbound' };
84
+ return { key: 'ctrl', label: 'HTTP API', sub: 'controllers' };
85
+ }
86
+
87
+ function collectRepoContext(root, { readFileImpl = readSafe } = {}) {
88
+ const abs = path.resolve(root || process.cwd());
89
+ const rel = (p) => path.relative(abs, p) || path.basename(p);
90
+
91
+ const ctx = {
92
+ project: { name: scrubString(path.basename(abs) || 'project'), slug: slug(path.basename(abs)), date: new Date().toISOString().slice(0, 10) },
93
+ entrypoints: [], crons: [], agents: [], services: [], models: [], integrations: [], stores: [], technologies: [],
94
+ };
95
+ const integrationSet = new Set();
96
+ const modelSet = new Map();
97
+ const storeSet = new Set();
98
+ const serviceSet = new Set();
99
+ const entryGroups = new Map();
100
+ const cronFiles = new Set();
101
+ const aiFiles = new Map(); // path -> { provider, line }
102
+
103
+ const pkgRaw = readFileImpl(path.join(abs, 'package.json'));
104
+ if (pkgRaw) {
105
+ try {
106
+ const pkg = JSON.parse(pkgRaw);
107
+ if (pkg.name) { const nm = pkg.name.replace(/^@[^/]+\//, ''); ctx.project.name = scrubString(nm); ctx.project.slug = slug(nm); }
108
+ if (pkg.description) ctx.project.tagline = scrubString(clip(pkg.description));
109
+ ctx.technologies = Object.keys({ ...(pkg.dependencies || {}), ...(pkg.devDependencies || {}) }).slice(0, CAP.tech).map(scrubString);
110
+ } catch { /* ignore */ }
111
+ }
112
+
113
+ // Prisma stores (datasource provider). schema.prisma OR prisma/schema/ dir.
114
+ const eatPrisma = (raw) => {
115
+ if (!raw) return;
116
+ const provider = (raw.match(/datasource\s+\w+\s*\{[^}]*provider\s*=\s*"([^"]+)"/m) || [])[1];
117
+ if (provider) storeSet.add(provider === 'postgresql' ? 'PostgreSQL' : titleize(provider));
118
+ };
119
+ for (const cand of ['prisma/schema.prisma', 'schema.prisma']) { const r = readFileImpl(path.join(abs, cand)); if (r) { eatPrisma(r); break; } }
120
+ for (const dir of ['prisma/schema', 'prisma']) {
121
+ const d = path.join(abs, dir);
122
+ try { if (fs.statSync(d).isDirectory()) for (const f of fs.readdirSync(d)) if (f.endsWith('.prisma')) eatPrisma(readFileImpl(path.join(d, f))); } catch { /* none */ }
123
+ }
124
+
125
+ try {
126
+ for (const f of fs.readdirSync(path.join(abs, '.claude', 'agents'))) {
127
+ if (!f.endsWith('.md')) continue;
128
+ const raw = readFileImpl(path.join(abs, '.claude', 'agents', f)) || '';
129
+ const mm = raw.match(/^\s*model\s*:\s*["']?([^"'\n]+?)["']?\s*$/im);
130
+ const mn = mm ? modelNode(mm[1].trim()) : null;
131
+ const provider = mn ? mn.label : null;
132
+ if (mn) modelSet.set(mn.label, { label: mn.label, ...(mn.domain ? { domain: mn.domain } : {}) });
133
+ ctx.agents.push({ label: titleize(path.basename(f, '.md')), provider, path: scrubString(`.claude/agents/${f}`) });
134
+ }
135
+ } catch { /* none */ }
136
+
137
+ // scan code files — AI/flow-likely first
138
+ const score = (p) => {
139
+ if (/(gemini|openai|anthropic|claude|ai-footprint|\bai\b|\/ai[-/]|llm|agent|orchestrat|generate-|chat|embed|personaliz|translat|synthes|certif|recomm|summar|extract|classif|briefing|showcase)/i.test(p)) return 0;
140
+ if (/(\.service\.|controller|\.cron|worker|queue|webhook|gateway|resolver|handler|integrations\/)/i.test(p)) return 1;
141
+ return 2;
142
+ };
143
+ const files = walk(abs).sort((a, b) => score(rel(a)) - score(rel(b)));
144
+ let readCount = 0;
145
+ for (const f of files) {
146
+ const r = rel(f);
147
+ // integration dirs → externals or models (KNOWN dirs only, to avoid noise
148
+ // from generic subdirs like ai/chat/http/config)
149
+ const idir = (r.match(/(?:^|\/)integrations\/([^/]+)\//) || [])[1];
150
+ if (idir) {
151
+ const key = idir.toLowerCase();
152
+ if (MODEL_DIRS.has(key)) { const pv = providerFromLine(key); if (pv) modelSet.set(pv.label, pv); }
153
+ else if (INTEG_DIR[key]) integrationSet.add(INTEG_DIR[key]);
154
+ }
155
+ // module/service dirs
156
+ const mod = (r.match(/(?:^|\/)modules\/([^/]+)\//) || [])[1];
157
+ if (mod && !/^(prisma|mongo|base-http|response|counter|shared|common|health)$/i.test(mod)) serviceSet.add(mod);
158
+ // datastore hints from path/libs
159
+ if (/aws-sdk\/client-s3|(^|\/)s3\b/i.test(r)) storeSet.add('AWS S3');
160
+ if (/cloudfront/i.test(r)) storeSet.add('CloudFront KVS');
161
+ // entrypoints (grouped)
162
+ if (ENTRY_PATH_RE.test(r) && !CRON_PATH_RE.test(r)) { const g = entryGroup(r); if (!entryGroups.has(g.key)) entryGroups.set(g.key, g); }
163
+ // crons (per file, by path)
164
+ if (CRON_PATH_RE.test(r)) cronFiles.add(r);
165
+
166
+ if (readCount >= MAX_READ) continue;
167
+ const src = readFileImpl(f);
168
+ if (src == null) continue;
169
+ readCount++;
170
+ const lines = src.split('\n');
171
+ let firstAi = null;
172
+ for (const ln of lines) {
173
+ if (!firstAi && AI_RE.test(ln)) firstAi = ln;
174
+ if (CRON_RE.test(ln)) cronFiles.add(r);
175
+ if (IMPORT_RE.test(ln)) {
176
+ for (const [re, label] of INTEG) if (re.test(ln)) {
177
+ if (/mongo/i.test(label)) storeSet.add('MongoDB');
178
+ else if (/redis/i.test(label)) storeSet.add('Redis');
179
+ else if (label === 'AWS') storeSet.add('AWS S3');
180
+ else integrationSet.add(label);
181
+ }
182
+ const pv = providerFromLine(ln);
183
+ if (pv && pv.domain) modelSet.set(pv.label, pv);
184
+ }
185
+ }
186
+ if (firstAi) aiFiles.set(r, { provider: providerFromLine(firstAi), line: clip(firstAi) });
187
+ }
188
+
189
+ const AGENT_SKIP_DIR = /^(gemini|anthropic|openai|http|base-http|event|config|util|shared|common|prisma|mongo|health|logger|integrations)$/i;
190
+ for (const [p, info] of aiFiles) {
191
+ if (ctx.agents.length >= CAP.agents) break;
192
+ if (/(?:^|\/)integrations\//.test(p)) { // the provider client, not an agent
193
+ if (info.provider && info.provider.domain) modelSet.set(info.provider.label, info.provider);
194
+ continue;
195
+ }
196
+ const dir = p.split('/').slice(-2, -1)[0] || path.basename(p);
197
+ if (AGENT_SKIP_DIR.test(dir)) continue;
198
+ ctx.agents.push({ label: titleize(dir), provider: info.provider ? info.provider.label : null, path: scrubString(p) });
199
+ if (info.provider && info.provider.domain) modelSet.set(info.provider.label, info.provider);
200
+ else if (info.provider) integrationSet.add(info.provider.label); // shakers-ai-api = external tool
201
+ }
202
+ const hasApp = [...entryGroups.keys()].some((k) => k.startsWith('app:'));
203
+ const hasNamedWebhook = [...entryGroups.keys()].some((k) => k.startsWith('wh:'));
204
+ if (hasApp) { entryGroups.delete('ctrl'); entryGroups.delete('http'); }
205
+ if (hasNamedWebhook) entryGroups.delete('wh');
206
+ ctx.entrypoints = Array.from(entryGroups.values()).slice(0, CAP.entrypoints).map((g) => ({ label: g.label, sub: g.sub }));
207
+ ctx.crons = Array.from(cronFiles).slice(0, CAP.crons).map((p) => ({ label: titleize(path.basename(p).replace(/\.(cron\.)?[tj]s$/, '')), path: scrubString(p) }));
208
+ ctx.services = Array.from(serviceSet).slice(0, CAP.services).map((m) => titleize(m));
209
+ ctx.models = Array.from(modelSet.values());
210
+ ctx.integrations = Array.from(integrationSet).slice(0, CAP.integrations);
211
+ ctx.stores = Array.from(storeSet).slice(0, CAP.stores);
212
+ return ctx;
213
+ }
214
+
215
+ module.exports = { collectRepoContext, slug };
@@ -0,0 +1,49 @@
1
+ 'use strict';
2
+
3
+ const { loadAuthSession, sessionStatus, isPlatformTalent } = require('./auth-session-store');
4
+ const { getProfile } = require('./config');
5
+
6
+ // THE ONE LAYER THAT DECIDES WHAT THE REPORT SHOWS ACCORDING TO IDENTITY (talents-ai-score, issue 123 / ADR-044).
7
+
8
+ // Pure: identity in, policy out.
9
+ function resolveReportGate({ loggedIn = false, status = null, email = null, userType = null, profile = 'talent' } = {}) {
10
+ const showEverything = !loggedIn;
11
+ const resolvedProfile = profile === 'external' ? 'external' : 'talent';
12
+ return Object.freeze({
13
+ loggedIn: !!loggedIn,
14
+ profile: resolvedProfile,
15
+ sessionStatus: status,
16
+ email: loggedIn && typeof email === 'string' && email ? email : null,
17
+ // --roadmap AND its server-side computation/egress (see bin/report.js): a
18
+ // logged-in Talent's roadmap is not merely hidden, it is not requested.
19
+ showRoadmap: showEverything,
20
+ // Per-agent "how to improve this agent" tips in the footprint report.
21
+ showAgentSuggestions: showEverything,
22
+ // The copyable remediation prompt in a `certify` verdict.
23
+ showCertifyRemediation: showEverything,
24
+ showFrameworkIntro: resolvedProfile === 'talent' && loggedIn && isPlatformTalent({ userType }),
25
+ });
26
+ }
27
+
28
+ // The general-user policy, used when a renderer is called without a gate. Frozen
29
+ // and shared so callers cannot accidentally mutate the default for everyone.
30
+ const DEFAULT_REPORT_GATE = resolveReportGate({ loggedIn: false });
31
+
32
+ // Reads the auth session and resolves the policy.
33
+ function reportGateForIdentity(env = process.env, now = Date.now()) {
34
+ const session = loadAuthSession(env);
35
+ const status = sessionStatus(session, now);
36
+ return resolveReportGate({
37
+ loggedIn: status === 'active',
38
+ status,
39
+ email: session ? session.email : null,
40
+ userType: session ? session.userType : null,
41
+ profile: getProfile(env),
42
+ });
43
+ }
44
+
45
+ module.exports = {
46
+ resolveReportGate,
47
+ reportGateForIdentity,
48
+ DEFAULT_REPORT_GATE,
49
+ };