shakers-cli 0.15.0 → 0.17.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 (132) hide show
  1. package/README.md +56 -1203
  2. package/bin/ai-usage.js +32 -58
  3. package/bin/login.js +10 -10
  4. package/bin/mcp.js +17 -12
  5. package/bin/mcpb.js +1 -1
  6. package/bin/onboarding.js +3 -2
  7. package/bin/register.js +19 -5
  8. package/bin/report-html.js +2 -2
  9. package/bin/shakers-cli.js +352 -0
  10. package/bin/shakers.js +20 -344
  11. package/bin/share.js +1 -1
  12. package/bin/superadmin.js +5 -5
  13. package/bin/version.js +3 -0
  14. package/package.json +12 -3
  15. package/src/account-marker.js +40 -0
  16. package/src/account-trace.js +23 -0
  17. package/src/agent-category.js +1 -1
  18. package/src/agent-evaluation.js +3 -3
  19. package/src/agent-org-chart.js +5 -5
  20. package/src/agent-synthesis.js +3 -3
  21. package/src/agent-usage.js +1 -1
  22. package/src/agents-client.js +4 -4
  23. package/src/ai-usage-review.js +101 -0
  24. package/src/ai.js +1 -1
  25. package/src/ansi.js +1 -1
  26. package/src/auth-client.js +18 -4
  27. package/src/auth-method-picker.js +1 -1
  28. package/src/auth-methods.js +1 -1
  29. package/src/auth-session-store.js +18 -8
  30. package/src/authorship-mismatch.js +1 -1
  31. package/src/authorship.js +2 -2
  32. package/src/automations-detector.js +1 -1
  33. package/src/backend-request.js +5 -3
  34. package/src/brand-ansi.js +1 -1
  35. package/src/browser-tools-detector.js +1 -1
  36. package/src/certify-disclaimer.js +1 -1
  37. package/src/certify-remediation-prompt.js +1 -1
  38. package/src/cli-args.js +2 -2
  39. package/src/command-audit.js +8 -5
  40. package/src/config-dir.js +1 -1
  41. package/src/config.js +114 -48
  42. package/src/consent-flow.js +7 -6
  43. package/src/consent-skip.js +1 -1
  44. package/src/detection-evidence.js +2 -2
  45. package/src/detection-noise.js +1 -1
  46. package/src/detectors.js +1 -1
  47. package/src/device-login.js +12 -1
  48. package/src/email-verification.js +3 -3
  49. package/src/env-paths.js +1 -1
  50. package/src/find-projects-client.js +12 -0
  51. package/src/find-projects-flow.js +1 -13
  52. package/src/git-activity.js +1 -1
  53. package/src/graph-certs.js +3 -3
  54. package/src/graph-scan.js +9 -9
  55. package/src/i18n.js +148 -138
  56. package/src/interactive-select.js +1 -1
  57. package/src/interview-client.js +6 -1
  58. package/src/interview-transcript.js +1 -1
  59. package/src/legal-notice.js +1 -1
  60. package/src/maturity.js +6 -6
  61. package/src/mcp-ai-usage-result-tool.js +34 -12
  62. package/src/mcp-ai-usage-tool.js +282 -163
  63. package/src/mcp-auth-form.js +7 -3
  64. package/src/mcp-detector.js +3 -3
  65. package/src/mcp-device-login-tools.js +2 -1
  66. package/src/mcp-find-projects-tools.js +1 -2
  67. package/src/mcp-install.js +279 -44
  68. package/src/mcp-instructions.js +1 -1
  69. package/src/mcp-onboarding-tools.js +4 -4
  70. package/src/mcp-prompts.js +4 -4
  71. package/src/mcp-register-tools.js +1 -0
  72. package/src/mcp-role-tools.js +24 -1
  73. package/src/mcp-server.js +5 -1
  74. package/src/mcp-signup-client.js +2 -2
  75. package/src/mcp-signup-tools.js +39 -9
  76. package/src/memory-structure-detector.js +1 -1
  77. package/src/model-call-record.js +2 -2
  78. package/src/onboarding-flow.js +63 -42
  79. package/src/onboarding-funnel.js +153 -20
  80. package/src/open-file.js +1 -1
  81. package/src/osc-link.js +1 -1
  82. package/src/portfolio-client.js +11 -1
  83. package/src/portfolio-git-info.js +1 -1
  84. package/src/profile-flow.js +1 -1
  85. package/src/profile-switch.js +5 -12
  86. package/src/render-certification.js +10 -10
  87. package/src/render-html.js +6 -6
  88. package/src/render-report-text.js +1 -1
  89. package/src/render-sheet.js +10 -10
  90. package/src/render-terminal.js +6 -6
  91. package/src/repo-scope.js +132 -0
  92. package/src/report-gating.js +5 -3
  93. package/src/report-store.js +13 -8
  94. package/src/roadmap-content.js +5 -5
  95. package/src/roadmap-personalization.js +3 -3
  96. package/src/roadmap-prompt.js +2 -2
  97. package/src/roles-flow.js +8 -5
  98. package/src/scan-exclusions.js +1 -1
  99. package/src/scanner.js +14 -14
  100. package/src/secret-prompt.js +1 -1
  101. package/src/session-gate.js +1 -1
  102. package/src/session-refresh.js +33 -3
  103. package/src/session-scan.js +1 -1
  104. package/src/shakers-wordmark.js +1 -1
  105. package/src/share-card.js +2 -2
  106. package/src/share-certification.js +4 -4
  107. package/src/share-consent-state.js +9 -9
  108. package/src/share-payload.js +7 -7
  109. package/src/share.js +7 -7
  110. package/src/signup-client.js +1 -1
  111. package/src/signup-copy.js +177 -2
  112. package/src/skills-client.js +1 -1
  113. package/src/start-add-agents.js +2 -2
  114. package/src/start-add-portfolio.js +3 -13
  115. package/src/start-add-skills.js +2 -2
  116. package/src/stdin-ask.js +16 -4
  117. package/src/strip-internal-comments.js +1 -1
  118. package/src/tech-detector.js +2 -2
  119. package/src/templates/report-sheet.html +11 -13
  120. package/src/terminal-agents.js +8 -8
  121. package/src/terminal-format.js +3 -3
  122. package/src/terminal-sections.js +11 -11
  123. package/src/tier-engine.js +3 -3
  124. package/src/tier-projection.js +2 -2
  125. package/src/transcript-turns.js +1 -1
  126. package/src/typewriter.js +1 -1
  127. package/src/usage-model-calls.js +6 -6
  128. package/src/usage-oneshot-commands.js +5 -5
  129. package/src/usage-repo-scope.js +11 -129
  130. package/src/usage-run-steps.js +82 -181
  131. package/src/usage-scan.js +235 -0
  132. package/src/work-streams.js +1 -1
package/bin/shakers.js CHANGED
@@ -10,354 +10,30 @@ if (_nodeMajor < 18) {
10
10
  process.exit(1);
11
11
  }
12
12
 
13
- const { detectReportLang, getCatalog } = require('../src/i18n');
14
- const { promptSelect } = require('../src/prompt-select');
15
- const { createStdinAsk } = require('../src/stdin-ask');
16
- const { migrateLegacyConfigDir } = require('../src/config-dir');
17
- const { sanitizeStaleBakedEndpoints } = require('../src/config');
18
- const { ensureFreshSession } = require('../src/session-refresh');
19
- const { hasUsableSession } = require('../src/session-gate');
20
- const { run: runAiUsage } = require('./ai-usage');
21
- const { run: runCertify } = require('./certify');
22
- const { run: runShare } = require('./share');
23
- const { run: runReport } = require('./report-html');
24
- const { run: runSuperadmin } = require('./superadmin');
25
- const { run: runLogin, runLogout } = require('./login');
26
- const { run: runAddSkill } = require('./add-skill');
27
- const { run: runAddAgent } = require('./add-agent');
28
- const { run: runAddProject } = require('./add-project');
29
- const { run: runAddRole } = require('./add-role');
30
- const { run: runChangeRole } = require('./change-role');
31
- const { run: runFindProjects } = require('./find-projects');
32
- const { run: runShowProject } = require('./show-project');
33
- const { run: runSave, runUnsave } = require('./save');
34
- const { run: runInvitations } = require('./invitations');
35
- const { run: runApplications } = require('./applications');
36
- const { run: runAvailability } = require('./availability');
37
- const { run: runCertifications } = require('./certifications');
38
- const { run: runMe } = require('./me');
39
- const { run: runRate } = require('./rate');
40
- const { run: runLang } = require('./lang');
41
- const { run: runSocials } = require('./socials');
42
- const { run: runExperiences } = require('./experiences');
43
- const { run: runPortfolios } = require('./portfolios');
44
- const { run: runAlma } = require('./alma');
45
- const { run: runRegister } = require('./register');
46
- const { run: runOnboarding } = require('./onboarding');
47
- const { run: runConfig } = require('./config');
48
- const { run: runMcp } = require('./mcp');
49
- const { run: runUpdate } = require('./update');
50
- const { checkOnboardingCompleted } = require('../src/onboarding-availability');
51
- const { emitCommandAudit } = require('../src/command-audit');
52
- const { readPackageInfo } = require('../src/update-flow');
53
- const { isSuppressed, maybeNotify, triggerBackgroundCheck } = require('../src/update-notifier');
54
-
55
- let VERSION = '';
56
- try {
57
- VERSION = require('../package.json').version || '';
58
- } catch {
59
- VERSION = '';
60
- }
61
-
62
- function parseLang(argv) {
63
- for (let i = 0; i < argv.length; i++) {
64
- const a = argv[i];
65
- if (a === '--lang' && (argv[i + 1] === 'es' || argv[i + 1] === 'en')) return argv[i + 1];
66
- if (a === '--lang=es') return 'es';
67
- if (a === '--lang=en') return 'en';
68
- }
69
- return null;
13
+ if (['--version', '-v', 'version'].includes(String(process.argv[2]).toLowerCase())) {
14
+ require('./version');
15
+ process.exit(0);
70
16
  }
71
17
 
72
- const HANDLERS = {
73
- 'ai-usage': runAiUsage,
74
- certify: runCertify,
75
- share: runShare,
76
- report: runReport,
77
- sheet: runReport,
78
- 'add-skill': runAddSkill,
79
- 'add-agent': runAddAgent,
80
- 'add-project': runAddProject,
81
- 'add-role': runAddRole,
82
- 'change-role': runChangeRole,
83
- 'find-projects': runFindProjects,
84
- 'show-project': runShowProject,
85
- 'save-project': runSave,
86
- 'unsave-project': runUnsave,
87
- invitations: runInvitations,
88
- applications: runApplications,
89
- availability: runAvailability,
90
- certifications: runCertifications,
91
- me: runMe,
92
- profile: runMe,
93
- rate: runRate,
94
- lang: runLang,
95
- socials: runSocials,
96
- experiences: runExperiences,
97
- portfolios: runPortfolios,
98
- alma: runAlma,
99
- register: runRegister,
100
- onboarding: runOnboarding,
101
- config: runConfig,
102
- update: runUpdate,
103
- login: runLogin,
104
- logout: runLogout,
105
- superadmin: runSuperadmin,
106
- mcp: runMcp,
107
- };
108
-
109
- const SESSION_EXEMPT = new Set(['ai-usage', 'register', 'config', 'login', 'logout', 'superadmin', 'mcp', 'share', 'alma', 'update']);
110
-
111
- // Command FAMILIES — a navigation layer over the flat commands (which all stay
112
- // valid as aliases). `shakers <family> <sub>` resolves to the flat handler; every
113
- // sub is just a friendlier name for an existing command. Additive, never breaking.
114
- const FAMILIES = {
115
- add: { skill: 'add-skill', agent: 'add-agent', project: 'add-project', role: 'add-role' },
116
- role: { add: 'add-role', change: 'change-role', main: 'change-role' },
117
- profile: {
118
- show: 'me', rate: 'rate', languages: 'lang', lang: 'lang', socials: 'socials',
119
- experiences: 'experiences', portfolios: 'portfolios', availability: 'availability',
120
- certifications: 'certifications',
121
- },
122
- projects: {
123
- find: 'find-projects', show: 'show-project', save: 'save-project', unsave: 'unsave-project',
124
- invitations: 'invitations', applications: 'applications',
125
- },
126
- usage: { run: 'ai-usage', report: 'report', share: 'share' },
127
- account: { login: 'login', logout: 'logout', register: 'register', config: 'config', update: 'update' },
128
- certs: { list: 'certifications', certify: 'certify' },
129
- };
130
-
131
- // `shakers <family>` with no sub: obvious default, else the family help.
132
- const FAMILY_DEFAULTS = { usage: 'ai-usage', profile: 'me', certs: 'certifications' };
133
-
134
- function familyHelpText(family, lang) {
135
- const map = FAMILIES[family] || {};
136
- const subs = Object.keys(map);
137
- // Show `shakers <family> <sub>` next to the flat alias it maps to, so both forms are discoverable.
138
- const lines = subs.map((s) => ` shakers ${family} ${s} (alias: shakers ${map[s]})`).join('\n');
139
- if (lang === 'es') {
140
- return `\n Familia "${family}" — subcomandos (los comandos planos entre paréntesis siguen valiendo):\n${lines}\n\n Ejecuta \`shakers ${family} <subcomando> --help\` para las opciones.\n\n`;
141
- }
142
- return `\n "${family}" family — subcommands (the flat commands in parentheses still work):\n${lines}\n\n Run \`shakers ${family} <subcommand> --help\` for the options.\n\n`;
143
- }
18
+ const config = require('../src/config');
144
19
 
145
- // Resolves a family + sub into a flat command, mutating nothing the flat path relies on.
146
- function resolveFamily(command, args) {
147
- const family = FAMILIES[command];
148
- if (!family) return { command, args };
149
- const subToken = args.find((a) => !a.startsWith('-'));
150
- const sub = subToken ? subToken.toLowerCase() : null;
151
- if (sub && family[sub]) {
152
- return { command: family[sub], args: args.filter((_, i) => i !== args.indexOf(subToken)) };
153
- }
154
- if (!sub && FAMILY_DEFAULTS[command]) return { command: FAMILY_DEFAULTS[command], args };
155
- // A pure family (not also a flat handler) with no valid sub → signal "show family help".
156
- if (!HANDLERS[command]) return { command, args, familyHelp: true };
157
- return { command, args };
158
- }
159
-
160
- async function remindAccountForAiUsage(lang, args) {
161
- if (!process.stdin.isTTY || args.includes('--json')) return;
162
- const r = getCatalog(lang).cli.accountReminder;
163
- const ask = createStdinAsk();
164
- let choice;
165
- try {
166
- choice = await promptSelect({
167
- ask,
168
- stdinIsTTY: true,
169
- out: (line) => process.stdout.write(` ${line}\n`),
170
- header: r.question,
171
- hint: r.hint,
172
- items: [{ id: 'yes', label: r.yes }, { id: 'no', label: r.no }],
173
- labelFor: (it) => it.label,
174
- });
175
- } finally {
176
- ask.close();
177
- }
178
- if (choice && choice.id === 'yes') {
179
- await runLogin(['--lang', lang]);
180
- }
181
- }
182
-
183
- function usageText(lang, { hideOnboarding = false } = {}) {
184
- const title = VERSION ? `shakers v${VERSION}` : 'shakers';
185
- if (lang === 'es') {
186
- const onboardingLine = hideOnboarding ? '' : ` onboarding La entrevista de onboarding (por voz/texto, LiveKit)\n`;
187
- return `\n ${title} — evaluación de uso de IA y skills\n\n`
188
- + ` Uso: shakers <comando> [opciones]\n\n`
189
- + ` Público (sin sesión):\n`
190
- + ` ai-usage Escanea este proyecto + tu máquina y puntúa tu setup de IA\n`
191
- + ` register Registro completo: perfil, precio, uso de IA y entrevista\n`
192
- + ` mcp Servidor MCP local (stdio) para apps de IA de escritorio\n\n`
193
- + ` Requieren sesión (ejecuta antes: shakers login):\n`
194
- + onboardingLine
195
- + ` certify Certifica una dimensión de tu rol (entrevista por LiveKit)\n`
196
- + ` add-skill Declara Skills detectadas en tu perfil\n`
197
- + ` add-agent Declara un agente detectado en tu perfil\n`
198
- + ` add-project Añade este proyecto a tu portfolio\n`
199
- + ` add-role Añade un rol a tu perfil (rol de crecimiento)\n`
200
- + ` change-role Cambia tu rol principal por uno de los que ya tienes\n`
201
- + ` find-projects Lista los proyectos/posiciones disponibles para ti\n`
202
- + ` show-project Detalle de un proyecto/posición por id\n`
203
- + ` save-project/unsave-project Guarda o quita un proyecto de tus guardados\n`
204
- + ` invitations Tus invitaciones a proyectos\n`
205
- + ` applications Proyectos a los que has aplicado + estado\n`
206
- + ` availability Ver o fijar tu disponibilidad\n`
207
- + ` certifications Tus dimensiones certificadas/sin certificar\n`
208
- + ` me Tu resumen de perfil\n`
209
- + ` rate Ver o fijar tu tarifa por proyecto\n`
210
- + ` lang Ver o fijar tus idiomas y nivel\n`
211
- + ` socials Ver o fijar tus enlaces sociales\n`
212
- + ` experiences Tus experiencias laborales\n`
213
- + ` portfolios Tus piezas de portfolio\n`
214
- + ` alma Mini-shell interactivo para conversar con Alma\n`
215
- + ` report Abre el informe HTML completo y compartible\n`
216
- + ` share Tarjeta de uso con marca para LinkedIn\n\n`
217
- + ` Sesión:\n`
218
- + ` login Inicia sesión en tu cuenta de Shakers\n`
219
- + ` logout Cierra la sesión\n`
220
- + ` config Lee/escribe la configuración (bases de hub/certs)\n`
221
- + ` update Actualiza el CLI a la última versión publicada (--check solo informa)\n\n`
222
- + ` Familias (agrupan los comandos de arriba; los nombres planos siguen valiendo como alias):\n`
223
- + ` add <skill|agent|project|role> · role <add|change>\n`
224
- + ` profile <show|rate|languages|socials|experiences|portfolios|availability|certifications>\n`
225
- + ` projects <find|show|save|unsave|invitations|applications>\n`
226
- + ` usage <run|report|share> · certs <list|certify> · account <login|logout|register|config|update>\n`
227
- + ` Ejecuta \`shakers <familia>\` (sin subcomando) para ver sus subcomandos.\n\n`
228
- + ` Ejecuta shakers <comando> --help para las opciones de cada comando.\n\n`;
229
- }
230
- const onboardingLine = hideOnboarding ? '' : ` onboarding The onboarding interview (over voice/text, LiveKit)\n`;
231
- return `\n ${title} — AI usage & skills evaluator\n\n`
232
- + ` Usage: shakers <command> [options]\n\n`
233
- + ` Public (no session):\n`
234
- + ` ai-usage Scan this project + machine and score your AI setup\n`
235
- + ` register Full registration: profile, pricing, AI usage and interview\n`
236
- + ` mcp Local MCP server (stdio) for desktop AI apps\n\n`
237
- + ` Require a session (run \`shakers login\` first):\n`
238
- + onboardingLine
239
- + ` certify Certify a dimension of your role (LiveKit interview)\n`
240
- + ` add-skill Declare detected Skills on your profile\n`
241
- + ` add-agent Declare a detected agent on your profile\n`
242
- + ` add-project Add this project to your portfolio\n`
243
- + ` add-role Add a role to your profile (a growth role)\n`
244
- + ` change-role Switch your main role to one you already have\n`
245
- + ` find-projects List the Projects/Positions available to you\n`
246
- + ` show-project Detail of a project/position by id\n`
247
- + ` save-project/unsave-project Save or remove a project from your saved list\n`
248
- + ` invitations Your project invitations\n`
249
- + ` applications Projects you applied to + status\n`
250
- + ` availability View or set your availability\n`
251
- + ` certifications Your certified/uncertified dimensions\n`
252
- + ` me Your profile summary\n`
253
- + ` rate View or set your per-project rate\n`
254
- + ` lang View or set your languages and level\n`
255
- + ` socials View or set your social links\n`
256
- + ` experiences Your work experiences\n`
257
- + ` portfolios Your portfolio pieces\n`
258
- + ` alma Interactive mini-shell to converse with Alma\n`
259
- + ` report Open the full shareable HTML report\n`
260
- + ` share Branded usage card for LinkedIn\n\n`
261
- + ` Session:\n`
262
- + ` login Sign in to your Shakers account\n`
263
- + ` logout Sign out\n`
264
- + ` config Read/write configuration (hub/certs bases)\n`
265
- + ` update Update the CLI to the latest published version (--check only reports)\n\n`
266
- + ` Families (group the commands above; the flat names still work as aliases):\n`
267
- + ` add <skill|agent|project|role> · role <add|change>\n`
268
- + ` profile <show|rate|languages|socials|experiences|portfolios|availability|certifications>\n`
269
- + ` projects <find|show|save|unsave|invitations|applications>\n`
270
- + ` usage <run|report|share> · certs <list|certify> · account <login|logout|register|config|update>\n`
271
- + ` Run \`shakers <family>\` (no subcommand) to list its subcommands.\n\n`
272
- + ` Run \`shakers <command> --help\` for a command's options.\n\n`;
273
- }
274
-
275
- function gateText(command, lang) {
276
- if (lang === 'es') {
277
- return `\n "${command}" requiere una sesión activa.\n`
278
- + ` Inicia sesión: shakers login\n`
279
- + ` ¿Sin cuenta?: shakers register\n\n`;
280
- }
281
- return `\n "${command}" requires an active session.\n`
282
- + ` Sign in: shakers login\n`
283
- + ` No account?: shakers register\n\n`;
284
- }
285
-
286
- function unknownText(command, lang) {
287
- if (lang === 'es') {
288
- return `\n Comando desconocido: "${command}".\n`
289
- + ` Ejecuta shakers para ver los comandos disponibles.\n\n`;
290
- }
291
- return `\n Unknown command: "${command}".\n`
292
- + ` Run \`shakers\` to see the available commands.\n\n`;
20
+ const problem = config.envProfileProblem(process.env);
21
+ if (problem) {
22
+ process.stderr.write(`\n shakers: ${problem}\n\n`);
23
+ process.exit(1);
293
24
  }
294
25
 
295
- async function main() {
296
- const argv = process.argv.slice(2);
297
- const lang = parseLang(argv) || detectReportLang();
298
-
299
- migrateLegacyConfigDir(process.env);
300
- sanitizeStaleBakedEndpoints(process.env);
301
- // Mint a fresh hub JWT from the stored cookie once per command (email sessions only; a no-op for Google/legacy or when no session exists).
302
- try { await ensureFreshSession(process.env); } catch { /* never blocks a command */ }
303
-
304
- const token = argv.find((a) => !a.startsWith('-'));
305
-
306
- // Single dispatch choke-point (ADR-060): audit EXACTLY once before branching, fire-and-forget (never awaited). MCP tool calls run in-process and never reach here.
307
- const auditCommand =
308
- !token || token.toLowerCase() === 'help' ? 'help' : token.toLowerCase();
309
- emitCommandAudit({ command: auditCommand, argv, env: process.env });
310
-
311
- // Non-blocking update notice: reads a cached version synchronously, registry check runs in a detached worker; stderr-only, suppressed for --json/non-TTY/CI/update/opt-out.
312
- if (!isSuppressed({ argv, command: auditCommand, env: process.env })) {
313
- maybeNotify({ currentVersion: VERSION, lang, env: process.env });
314
- triggerBackgroundCheck({ name: readPackageInfo().name, env: process.env });
315
- }
316
-
317
- if (!token || token.toLowerCase() === 'help') {
318
- let hideOnboarding = false;
319
- try {
320
- hideOnboarding = await checkOnboardingCompleted({ timeoutMs: 3000 });
321
- } catch {
322
- hideOnboarding = false;
323
- }
324
- process.stdout.write(usageText(lang, { hideOnboarding }));
325
- return;
326
- }
26
+ const argv = process.argv.slice(2);
27
+ const token = argv.find((a) => !a.startsWith('-'));
28
+ const rest = argv.filter((_, i) => i !== argv.indexOf(token));
327
29
 
328
- const rawCommand = token.toLowerCase();
329
- const rawArgs = argv.filter((_, i) => i !== argv.indexOf(token));
330
-
331
- // Family navigation layer (flat commands still work as aliases).
332
- const resolved = resolveFamily(rawCommand, rawArgs);
333
- if (resolved.familyHelp) {
334
- process.stdout.write(familyHelpText(rawCommand, lang));
335
- return;
336
- }
337
- const command = resolved.command;
338
- const args = resolved.args;
339
-
340
- const handler = HANDLERS[command];
341
- if (!handler) {
342
- process.stderr.write(unknownText(command, lang));
343
- process.exitCode = 1;
344
- return;
345
- }
346
-
347
- const wantsHelp = args.includes('--help') || args.includes('-h');
348
- const loggedIn = await hasUsableSession(process.env);
349
-
350
- if (!wantsHelp && !SESSION_EXEMPT.has(command) && !loggedIn) {
351
- process.stderr.write(gateText(command, lang));
352
- process.exitCode = 1;
353
- return;
354
- }
355
-
356
- if (command === 'ai-usage' && !wantsHelp && !loggedIn) {
357
- await remindAccountForAiUsage(lang, args);
358
- }
359
-
360
- await handler(args);
30
+ if (token && token.toLowerCase() === 'mcp' && rest[0] !== 'install') {
31
+ require('../src/config-dir').migrateLegacyConfigDir(process.env);
32
+ config.sanitizeStaleBakedEndpoints(process.env);
33
+ require('../src/session-refresh').ensureFreshSession(process.env)
34
+ .catch(() => null)
35
+ .then(() => require('../src/command-audit').emitCommandAudit({ command: 'mcp', argv, env: process.env }));
36
+ require('./mcp').run(rest, { makeInterviewClient: require('../src/interview-client').createInterviewClient });
37
+ } else {
38
+ require('./shakers-cli').main();
361
39
  }
362
-
363
- main();
package/bin/share.js CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  'use strict';
3
3
 
4
- // `share` — turns the project's AI-usage FOOTPRINT result into a branded card to post on LinkedIn (skill-code-certification).
4
+ // `share` — turns the project's AI-usage FOOTPRINT result into a branded card to post on LinkedIn.
5
5
 
6
6
  const { detectReportLang, getCatalog } = require('./../src/i18n');
7
7
  const { generateShareCard } = require('./../src/share-card');
package/bin/superadmin.js CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  'use strict';
3
3
 
4
- // `superadmin` — NON-PROD, password-authenticated superadmin SESSION (ADR-027, supersedes the ADR-021/022/023 provision/teardown/authorized-authoring flow).
4
+ // `superadmin` — NON-PROD, password-authenticated superadmin SESSION.
5
5
 
6
6
  const http = require('http');
7
7
  const https = require('https');
@@ -94,7 +94,7 @@ async function promptLine(ask, canPrompt, label) {
94
94
  return String(await ask(label)).trim();
95
95
  }
96
96
 
97
- // The SECRET prompt (issue 102).
97
+ // The SECRET prompt.
98
98
  async function promptSecret(ask, canPrompt, label) {
99
99
  if (!canPrompt) return '';
100
100
  const read = typeof ask.secret === 'function' ? ask.secret.bind(ask) : ask;
@@ -214,7 +214,7 @@ async function run(argv = process.argv.slice(2), { ask: injectedAsk = null, inpu
214
214
  return;
215
215
  }
216
216
  process.stdout.write(`\n ${c.sessionIntro}\n`);
217
- // talents-ai-score, ADR-020/021 (coordinator ruling): superadmin is DELIBERATELY single-hop, PRIMARY (certifications service) ONLY — never hub.
217
+ // Superadmin is DELIBERATELY single-hop, PRIMARY (certifications service) ONLY — never hub.
218
218
  process.stdout.write(` ${c.queryingPrimary(endpoint)}\n`);
219
219
  // Always true (three-way review): the password has NO argv flag anymore, so it must always be collectible — either from a real TTY, the REPL's injected reader, or a piped stdin.
220
220
  const password = await promptSecret(ask, true, c.passwordPrompt);
@@ -230,7 +230,7 @@ async function run(argv = process.argv.slice(2), { ask: injectedAsk = null, inpu
230
230
  }
231
231
  }
232
232
 
233
- // ADR-025 read-only attribution receipt for stored certifications.
233
+ // Read-only attribution receipt for stored certifications.
234
234
  async function runInspect({ opts, c, endpoint, password, ask, canPrompt }) {
235
235
  const email = await resolveEmailArg(opts, c, ask, canPrompt, c.inspectEmailPrompt);
236
236
  if (!password || !email || !isValidEmail(email)) {
@@ -308,7 +308,7 @@ async function resolveEmailArg(opts, c, ask, canPrompt, promptLabel) {
308
308
  return null;
309
309
  }
310
310
 
311
- // ADR-027: open a superadmin SESSION — validate the password server-side, get a token, persist it locally.
311
+ // Open a superadmin SESSION — validate the password server-side, get a token, persist it locally.
312
312
  async function runOpenSession({ opts, c, endpoint, password, ask, canPrompt }) {
313
313
  const email = await resolveEmailArg(opts, c, ask, canPrompt, c.emailPrompt);
314
314
  if (!password || !email || !isValidEmail(email)) {
package/bin/version.js ADDED
@@ -0,0 +1,3 @@
1
+ 'use strict';
2
+
3
+ process.stdout.write(`${require('../package.json').version}\n`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "shakers-cli",
3
- "version": "0.15.0",
3
+ "version": "0.17.0",
4
4
  "description": "Shakers AI usage & skills evaluator: a local-first CLI (`shakers` binary) plus a local MCP server. Commands: ai-usage (scan + classify your AI-tool setup, T0-T7), certify (certify Skills from your code), share (branded LinkedIn card).",
5
5
  "type": "commonjs",
6
6
  "bin": {
@@ -15,9 +15,11 @@
15
15
  "scripts": {
16
16
  "start": "node bin/shakers.js",
17
17
  "shakers": "node bin/shakers.js",
18
- "test": "node --test test/*.test.js",
18
+ "test": "node --require ./test-fixtures/test-env.js --test test/*.test.js",
19
19
  "build:mcpb": "sh scripts/build-mcpb.sh",
20
- "publish:beta": "node scripts/prepare-beta.js && npm publish --tag latest && git checkout -- package.json"
20
+ "publish:beta": "node scripts/prepare-beta.js && npm publish --tag latest && git checkout -- package.json README.md",
21
+ "lint": "oxlint --deny-warnings bin src scripts test test-fixtures",
22
+ "check:boundaries": "node scripts/check-boundaries.js"
21
23
  },
22
24
  "engines": {
23
25
  "node": ">=18"
@@ -35,8 +37,15 @@
35
37
  "certification"
36
38
  ],
37
39
  "license": "SEE LICENSE IN LICENSE",
40
+ "devDependencies": {
41
+ "oxlint": "1.86.0"
42
+ },
38
43
  "publishConfig": {
39
44
  "access": "public",
40
45
  "registry": "https://registry.npmjs.org/"
46
+ },
47
+ "amplitude": {
48
+ "env": "prod",
49
+ "apiKey": "03bbeda1f0c96d51774faee68ce12a3"
41
50
  }
42
51
  }
@@ -0,0 +1,40 @@
1
+ 'use strict';
2
+
3
+ const crypto = require('crypto');
4
+ const fs = require('fs');
5
+ const path = require('path');
6
+ const { getConfigDir } = require('./config-dir');
7
+
8
+ const MARKER_FILE_NAME = 'account-marker.json';
9
+ const MARKER_FILE_VERSION = 1;
10
+
11
+ function accountMarkerPath(env = process.env) {
12
+ return path.join(getConfigDir(env), MARKER_FILE_NAME);
13
+ }
14
+
15
+ function hasAccountMarker(env = process.env, { fs: fsImpl = fs } = {}) {
16
+ return fsImpl.existsSync(accountMarkerPath(env));
17
+ }
18
+
19
+ function recordAccountMarker(kind, env = process.env, { fs: fsImpl = fs } = {}) {
20
+ if (hasAccountMarker(env, { fs: fsImpl })) return;
21
+ const target = accountMarkerPath(env);
22
+ const dir = path.dirname(target);
23
+ const tmp = path.join(dir, `.${MARKER_FILE_NAME}.${process.pid}.${crypto.randomBytes(6).toString('hex')}.tmp`);
24
+ const payload = `${JSON.stringify({ version: MARKER_FILE_VERSION, kind, at: new Date().toISOString() }, null, 2)}\n`;
25
+ try {
26
+ fsImpl.mkdirSync(dir, { recursive: true, mode: 0o700 });
27
+ const fd = fsImpl.openSync(tmp, 'wx', 0o600);
28
+ try {
29
+ fsImpl.writeFileSync(fd, payload);
30
+ try { fsImpl.fsyncSync(fd); } catch {}
31
+ } finally {
32
+ fsImpl.closeSync(fd);
33
+ }
34
+ fsImpl.renameSync(tmp, target);
35
+ } catch {
36
+ try { fsImpl.unlinkSync(tmp); } catch {}
37
+ }
38
+ }
39
+
40
+ module.exports = { hasAccountMarker, recordAccountMarker };
@@ -0,0 +1,23 @@
1
+ 'use strict';
2
+
3
+ const { hasAuthSessionFile } = require('./auth-session-store');
4
+ const { loadConsentState } = require('./share-consent-state');
5
+ const { loadConfigFile } = require('./config');
6
+ const { hasAccountMarker, recordAccountMarker } = require('./account-marker');
7
+
8
+ function hasCurrentTrace(env) {
9
+ const consent = loadConsentState(env);
10
+ return hasAuthSessionFile(env)
11
+ || !!(consent && consent.lastSentAt)
12
+ || require('./report-store').hasRecordedUpload(env)
13
+ || !!loadConfigFile(env).superadminSession;
14
+ }
15
+
16
+ function hasAccountTrace(env = process.env) {
17
+ if (hasAccountMarker(env)) return true;
18
+ if (!hasCurrentTrace(env)) return false;
19
+ recordAccountMarker('existing-trace', env);
20
+ return true;
21
+ }
22
+
23
+ module.exports = { hasAccountTrace };
@@ -1,6 +1,6 @@
1
1
  'use strict';
2
2
 
3
- // THE AGENT-CATEGORY VOCABULARY, AND WHICH OF IT IS A SPECIALITY (issue 120).
3
+ // THE AGENT-CATEGORY VOCABULARY, AND WHICH OF IT IS A SPECIALITY.
4
4
 
5
5
  /** The floor category key, as the service's catalog defines it (`other-1`'s category). */
6
6
  const FLOOR_CATEGORY = 'other';
@@ -4,7 +4,7 @@ const { scrubSecrets } = require('./agent-synthesis');
4
4
  const { requestBackend } = require('./backend-request');
5
5
  const { reporterFor, REASON } = require('./model-call-record');
6
6
 
7
- // Agent definition-quality evaluation client (ADR-016, agent evaluation).
7
+ // Agent definition-quality evaluation client.
8
8
 
9
9
  // HOW LONG TO WAIT FOR THE AGENT EVALUATION, and why it keeps growing.
10
10
  const DEFAULT_TIMEOUT_MS = 150000;
@@ -29,7 +29,7 @@ function buildAgentEvaluationRequest(structuralAgents, definitionsByName, locale
29
29
  parent: a.parent || null,
30
30
  })),
31
31
  promptVersion: PROMPT_VERSION,
32
- // ADR-026: detected report language for the rationale + description prose.
32
+ // Detected report language for the rationale + description prose.
33
33
  ...(locale === 'es' || locale === 'en' ? { locale } : {}),
34
34
  ...(consentGranted === true ? { traceContentConsent: true } : {}),
35
35
  };
@@ -103,7 +103,7 @@ function normalizeEvaluations(list) {
103
103
  .map((e) => ({
104
104
  name: e.name,
105
105
  rationale: typeof e.rationale === 'string' ? e.rationale : '',
106
- // ADR-026: target-language one-line description; null when the server
106
+ // Target-language one-line description; null when the server
107
107
  // omitted it — the caller falls back to the verbatim phrase.
108
108
  description: typeof e.description === 'string' && e.description ? e.description : null,
109
109
  // v4 (agent classification): closest catalog agent + how it was matched.
@@ -4,7 +4,7 @@ const fs = require('fs');
4
4
  const path = require('path');
5
5
  const { getHomeDir } = require('./env-paths');
6
6
 
7
- // Deterministic (no-LLM) parser of the talent's AI agent org chart (talents-ai-score, ADR-009).
7
+ // Deterministic (no-LLM) parser of the talent's AI agent org chart.
8
8
 
9
9
  // Safety caps mirror scanner.js's FOOTPRINT_MAX_* (avoid pathological scans,
10
10
  // never a product requirement).
@@ -145,12 +145,12 @@ function buildAgentFromFrontmatter(fm, filePath) {
145
145
  // the card in place of the LLM `model`.
146
146
  aiProduct: deriveAiProduct(filePath),
147
147
  model: typeof fm.model === 'string' && fm.model ? fm.model : null,
148
- // Hierarchy (ADR-009): first honour an explicit `parent` frontmatter key when a project declares one.
148
+ // Hierarchy: first honour an explicit `parent` frontmatter key when a project declares one.
149
149
  parent: typeof fm.parent === 'string' && fm.parent ? fm.parent : null,
150
150
  };
151
151
  }
152
152
 
153
- // `parseAgentFile` USED TO BE HERE (removed by issue 093's sweep): read one `.md` and build an agent from its frontmatter.
153
+ // `parseAgentFile` USED TO BE HERE (removed): read one `.md` and build an agent from its frontmatter.
154
154
 
155
155
  // Markdown body (everything after the closing ` ` of the frontmatter).
156
156
  function bodyAfterFrontmatter(content) {
@@ -239,7 +239,7 @@ function parseAgentOrgChartFromDirs(dirs) {
239
239
  return agents;
240
240
  }
241
241
 
242
- // Deterministic (no-LLM) agent org chart, scoped to `.claude/agents/` in BOTH the project root and the home directory (ADR-014, project ∪ home).
242
+ // Deterministic (no-LLM) agent org chart, scoped to `.claude/agents/` in BOTH the project root and the home directory (project ∪ home).
243
243
  function parseAgentOrgChart(root) {
244
244
  return parseAgentOrgChartFromDirs(agentDirs(root));
245
245
  }
@@ -249,7 +249,7 @@ function parseAgentOrgChartForRoots(roots) {
249
249
  return parseAgentOrgChartFromDirs(agentDirsForRoots(Array.isArray(roots) ? roots : [roots]));
250
250
  }
251
251
 
252
- // talents-ai-score, ADR-010: returns `[{ name, description }]` — the ONLY function in this module that ever returns description/prompt content.
252
+ // Returns `[{ name, description }]` — the ONLY function in this module that ever returns description/prompt content.
253
253
  function parseAgentDescriptionsFromDirs(dirs) {
254
254
  const seen = new Set();
255
255
  const result = [];
@@ -3,7 +3,7 @@
3
3
  const { requestBackend } = require('./backend-request');
4
4
  const { reporterFor, REASON } = require('./model-call-record');
5
5
 
6
- // Agent synthesis client (talents-ai-score, ADR-010 / ADR-011).
6
+ // Agent synthesis client.
7
7
 
8
8
  const DEFAULT_TIMEOUT_MS = 8000;
9
9
 
@@ -28,7 +28,7 @@ const PEM_BLOCK_RE = /-----BEGIN[ A-Z]*PRIVATE KEY-----[\s\S]*?-----END[ A-Z]*PR
28
28
  const CONN_STRING_RE = /\b([a-zA-Z][a-zA-Z0-9+.-]{1,15}:\/\/)[^\s'"/:@]+:[^\s'"@]+@/g; // scheme://user:pass@ -> scheme://[REDACTED]@
29
29
  const GENERIC_KV_RE = /\b(?:password|secret|token|api[_-]?key|bearer)\s*[:=]\s*\S+/gi;
30
30
 
31
- // Heuristic, best-effort redaction — an "invisible safety net" (ADR-011), not a substitute for the talent's own judgment about what they write in agent descriptions.
31
+ // Heuristic, best-effort redaction — an "invisible safety net", not a substitute for the talent's own judgment about what they write in agent descriptions.
32
32
  function scrubSecrets(text) {
33
33
  if (typeof text !== 'string' || !text) return '';
34
34
  let out = text;
@@ -77,7 +77,7 @@ function isValidSynthesisResponse(parsed) {
77
77
 
78
78
  // Requests the agent-synthesis endpoint.
79
79
  async function requestAgentSynthesis(requestBody, { endpoint, timeoutMs = DEFAULT_TIMEOUT_MS, onOutcome = null } = {}) {
80
- // Issue 109: the four `return null`s below are four DIFFERENT situations that used to be one.
80
+ // The four `return null`s below are four DIFFERENT situations that used to be one.
81
81
  const outcome = reporterFor(onOutcome, { timeoutMs });
82
82
  if (!endpoint) {
83
83
  outcome.skip(REASON.NO_ENDPOINT);
@@ -4,7 +4,7 @@ const fs = require('fs');
4
4
  const path = require('path');
5
5
  const { getHomeDir } = require('./env-paths');
6
6
 
7
- // Local Claude Code agent-usage signal (ADR-016, agent evaluation feature).
7
+ // Local Claude Code agent-usage signal.
8
8
 
9
9
  const MAX_FILES = 500; // cap total session files scanned (bounded work)
10
10
  const MAX_BYTES_PER_FILE = 8 * 1024 * 1024; // read at most an 8MB head per file