shakers-cli-beta 0.10.3 → 0.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/i18n.js CHANGED
@@ -203,26 +203,26 @@ const catalogs = {
203
203
  blockingLabel: 'Criterio exacto que te impide subir de tier:',
204
204
  maxTierNote: 'Cumples todos los criterios de la escalera T0-T7: no hay un criterio adicional bloqueando tu progreso.',
205
205
  criterion: {
206
- t1Met: (n) => `Tienes al menos una herramienta de IA detectada y configurada en tu entorno (\`totalDetected = ${n}\`).`,
207
- t2Met: (n) => `Dispones de al menos un fichero de contexto persistente — instrucciones, configuración o reglas — para alguna herramienta (\`context = ${n}\`).`,
208
- t3Met: (n) => `Tienes al menos un servidor MCP conectado, dando a la IA acceso a datos o herramientas externas (\`mcpServers = ${n}\`).`,
209
- t4Met: (n) => `Has creado activos propios — skills, comandos o reglas personalizadas — más allá de la configuración por defecto (\`custom = ${n}\`).`,
210
- t5Met: (hasAgentic, mcp, custom) => `Operas con una CLI agéntica (Claude Code, Aider, Gemini CLI, Codex CLI o Amazon Q Developer) combinada con MCP y activos propios (\`hasAgentic = ${hasAgentic}\`, \`mcpServers = ${mcp}\`, \`custom = ${custom}\`).`,
211
- t6Met: (n) => `Tienes un equipo de al menos 2 agentes especializados definidos (\`agentCounts.agents = ${n}\`).`,
212
- t7Met: (n) => `Tienes automatización basada en hooks configurada (\`hooks = ${n}\`).`,
213
- t1Blocking: (n) => `Para subir a T1 (Primera herramienta) necesitas al menos una herramienta de IA detectada — actualmente \`totalDetected = ${n}\`.`,
214
- t2Blocking: (n) => `Para subir a T2 (Banco con notas) necesitas al menos un fichero de contexto persistente (instrucciones, configuración o reglas) — actualmente \`context = ${n}\`.`,
215
- t3Blocking: (n) => `Para subir a T3 (Banco conectado) necesitas conectar al menos un servidor MCP — actualmente \`mcpServers = ${n}\`.`,
216
- t4Blocking: (n) => `Para subir a T4 (Herramienta propia) necesitas crear al menos un activo propio — skill, comando o regla — más allá de la configuración por defecto — actualmente \`custom = ${n}\`.`,
206
+ t1Met: (n) => `Tienes al menos una herramienta de IA detectada y configurada en tu entorno (detectadas: ${n}).`,
207
+ t2Met: (n) => `Dispones de al menos un fichero de contexto persistente — instrucciones, configuración o reglas — para alguna herramienta (tienes ${n}).`,
208
+ t3Met: (n) => `Tienes al menos un servidor MCP conectado, dando a la IA acceso a datos o herramientas externas (tienes ${n}).`,
209
+ t4Met: (n) => `Has creado activos propios — skills, comandos o reglas personalizadas — más allá de la configuración por defecto (tienes ${n}).`,
210
+ t5Met: (hasAgentic, mcp, custom) => `Operas con una CLI agéntica (Claude Code, Aider, Gemini CLI, Codex CLI o Amazon Q Developer) combinada con MCP y activos propios (${mcp} servidores MCP y ${custom} activos propios).`,
211
+ t6Met: (n) => `Tienes un equipo de al menos 2 agentes especializados definidos (tienes ${n}).`,
212
+ t7Met: (n) => `Tienes automatización basada en hooks configurada (tienes ${n}).`,
213
+ t1Blocking: (n) => `Para subir a T1 (Primera herramienta) necesitas al menos una herramienta de IA detectada — ahora tienes ${n}.`,
214
+ t2Blocking: (n) => `Para subir a T2 (Banco con notas) necesitas al menos un fichero de contexto persistente (instrucciones, configuración o reglas) — ahora tienes ${n}.`,
215
+ t3Blocking: (n) => `Para subir a T3 (Banco conectado) necesitas conectar al menos un servidor MCP — ahora tienes ${n}.`,
216
+ t4Blocking: (n) => `Para subir a T4 (Herramienta propia) necesitas crear al menos un activo propio — skill, comando o regla — más allá de la configuración por defecto — ahora tienes ${n}.`,
217
217
  t5Blocking: (hasAgentic, mcp, custom) => {
218
218
  const missing = [];
219
219
  if (!hasAgentic) missing.push('una CLI agéntica (Claude Code, Aider, Gemini CLI, Codex CLI o Amazon Q Developer)');
220
220
  if (mcp < 1) missing.push('al menos 1 servidor MCP');
221
221
  if (custom < 1) missing.push('al menos 1 activo propio (skill, comando o regla)');
222
- return `Para subir a T5 (Operador agéntico) te falta: ${missing.join('; ')} (\`hasAgentic = ${hasAgentic}\`, \`mcpServers = ${mcp}\`, \`custom = ${custom}\`).`;
222
+ return `Para subir a T5 (Operador agéntico) te falta: ${missing.join('; ')}.`;
223
223
  },
224
224
  t6Blocking: (n) => `Para subir a T6 (Multi-agente) necesitas al menos 2 agentes especializados definidos en \`.claude/agents/\` — actualmente tienes ${n}.`,
225
- t7Blocking: (n) => `Para subir a T7 (Taller orquestado) necesitas al menos un hook de automatización configurado — actualmente \`hooks = ${n}\`.`,
225
+ t7Blocking: (n) => `Para subir a T7 (Taller orquestado) necesitas al menos un hook de automatización configurado — ahora tienes ${n}.`,
226
226
  },
227
227
  },
228
228
  ladder: {
@@ -487,6 +487,39 @@ const catalogs = {
487
487
  yes: 'Sí, iniciar sesión',
488
488
  no: 'No, continuar sin cuenta',
489
489
  },
490
+ // `update` command: actualiza el CLI a la última versión publicada (solo CLI, no MCP).
491
+ update: {
492
+ help:
493
+ 'update — actualiza el CLI de Shakers a la última versión publicada.\n\n'
494
+ + ' Uso: shakers update [--check]\n\n'
495
+ + ' (sin flag) Comprueba la última versión y, si hay una más nueva, la instala\n'
496
+ + ' por el mismo método con el que se instaló este CLI.\n'
497
+ + ' --check Solo informa de tu versión frente a la última publicada; no instala.\n'
498
+ + ' --json Salida JSON (paquete, versión actual/última, si hay actualización).\n',
499
+ checking: 'Comprobando actualizaciones…',
500
+ current: (v) => `Versión instalada: ${v}`,
501
+ upToDate: (v) => `Ya tienes la última versión (${v}).`,
502
+ available: (from, to) => `Hay una actualización disponible: ${from} → ${to}`,
503
+ checkHint: 'Ejecuta `shakers update` (sin --check) para instalarla.',
504
+ detected: (label) => `Instalación detectada: ${label}`,
505
+ updating: (name, from, to) => `Actualizando ${name}: ${from} → ${to}…`,
506
+ success: (v) => `Actualizado a la versión ${v}.`,
507
+ reRun: 'Vuelve a ejecutar tu comando para usar la nueva versión.',
508
+ latestUnknown: (reason) =>
509
+ `No se pudo determinar la última versión (${reason}). Revisa tu conexión y el acceso al registro.`,
510
+ failInstall: (reason) => `La actualización falló (${reason}).`,
511
+ failHint: (cmd) =>
512
+ `Revisa tu conexión y permisos y vuelve a intentarlo, o actualiza a mano:\n ${cmd}`,
513
+ sourceCheckout:
514
+ 'Este CLI se ejecuta desde un checkout de código — actualízalo con `git pull` en ese repositorio.',
515
+ unknownInstall: (cmd) =>
516
+ `No se pudo detectar cómo se instaló este CLI. Actualiza a mano:\n ${cmd}`,
517
+ },
518
+ // Aviso pasivo "hay una versión nueva" al arrancar cualquier comando (lectura de caché, sin red en el camino caliente).
519
+ updateNotifier: {
520
+ available: (from, to) => `Hay una versión nueva del CLI de Shakers: ${from} → ${to}`,
521
+ howTo: 'Ejecuta `shakers update` para actualizar.',
522
+ },
490
523
  // `report` command (ADR-016): genera y ABRE el informe HTML completo y compartible de este proyecto (uso de IA + Skills certificadas).
491
524
  report: {
492
525
  help: 'report — genera y abre el informe HTML completo de este proyecto (uso de IA + Skills certificadas) para compartir con tu equipo.\n'
@@ -714,6 +747,8 @@ const catalogs = {
714
747
  discoverFailed: (reason) => `No se pudieron obtener tus dimensiones (${reason}). Inténtalo de nuevo en un momento.`,
715
748
  unmatchedNote: (keys) => `Aviso: algunas dimensiones no tienen plantilla asociada y no se pueden ofrecer aquí (${keys}).`,
716
749
  noneOfferable: 'Ahora mismo no hay dimensiones certificables por aquí. Vuelve a intentarlo cuando tengas alguna disponible.',
750
+ onCooldownNote: (items) => `Aún no puedes repetir: ${items}.`,
751
+ onCooldownItem: (slug, day) => `${slug} (disponible el ${day})`,
717
752
  mainRole: (role) => `Tu rol principal: ${role}`,
718
753
  selectHeading: 'Elige la dimensión que quieres certificar:',
719
754
  selectHint: '↑/↓ para moverte, Enter para elegir, Esc para cancelar.',
@@ -722,10 +757,41 @@ const catalogs = {
722
757
  selectNoneChosen: 'No se eligió ninguna dimensión.',
723
758
  stateExpired: 'caducada',
724
759
  dimensionInvalid: 'Esa dimensión no está entre las certificables ahora mismo.',
760
+ dimensionOnCooldown: (slug, day) => `Aún no puedes repetir ${slug}: estará disponible el ${day}.`,
725
761
  dimensionUsing: (slug) => `Dimensión elegida: ${slug}`,
726
- webHandoff: 'La entrevista de certificación se hace en la web.',
727
- webHandoffLink: (url) => `Ábrela aquí para certificar esta dimensión:\n ${url}`,
728
- webHandoffNoLink: 'Entra en tu perfil de Shakers en la web y ve a Certificaciones para hacerla.',
762
+ creating: 'Creando la entrevista…',
763
+ createFailed: (reason) => `No se pudo crear la entrevista (${reason}).`,
764
+ onCooldown: (day) => (day
765
+ ? `Ya hiciste esta dimensión hace poco. Podrás repetirla a partir del ${day}.`
766
+ : 'Ya hiciste esta dimensión hace poco y todavía no se puede repetir.'),
767
+ needsWeb: 'Esta dimensión lleva un ejercicio que por ahora solo se hace en la web, no por terminal.',
768
+ interviewUnavailable: (reason) => `La entrevista no pudo completarse (${reason}).`,
769
+ reportPolling: 'Calculando tu veredicto…',
770
+ reportTimeout: 'El veredicto todavía se está calculando. Consúltalo en tu perfil en unos minutos.',
771
+ reportFailed: (reason) => `No se pudo obtener el veredicto (${reason}). Consúltalo en tu perfil.`,
772
+ verdictTitle: 'Veredicto',
773
+ verdictDimension: (dim) => `Dimensión: ${dim}`,
774
+ verdictBand: (band) => `Nivel: ${band}`,
775
+ verdictCertified: 'Resultado: certificada',
776
+ verdictNotCertified: 'Resultado: no certificada esta vez',
777
+ done: 'Certificación completada.',
778
+ // The same case copy the web preview shows (works `certifications.preview`).
779
+ caseTitle: 'El caso',
780
+ caseExpectationsTitle: 'Qué se espera',
781
+ caseExpectations: {
782
+ CASE_STUDY: {
783
+ reasoning: 'Se evalúa tu razonamiento y los supuestos que haces, no una respuesta perfecta.',
784
+ askForData: 'Puedes pedir datos que no están en el caso: di para qué los quieres.',
785
+ takeTime: 'Puedes pararte a pensar antes de responder.',
786
+ },
787
+ ROLE_PLAY: {
788
+ reasoning: 'Se evalúa cómo razonas en la conversación y los supuestos que haces, no una respuesta perfecta.',
789
+ askForData: 'Puedes pedir a la otra parte datos que no están en el caso.',
790
+ takeTime: 'Puedes pararte a pensar antes de responder.',
791
+ },
792
+ },
793
+ caseBeforeStart: 'Alma te va a hacer preguntas sobre este caso. Si te falta algún dato, pídeselo durante la entrevista.',
794
+ caseReadyPrompt: 'Pulsa Enter cuando lo hayas leído para empezar la entrevista.',
729
795
  },
730
796
  findProjects: {
731
797
  help:
@@ -949,6 +1015,7 @@ const catalogs = {
949
1015
  noTerminal: 'No hay una terminal donde preguntarte, así que no he tocado nada. Vuelve a ejecutar el instalador en tu terminal para conectarlo.',
950
1016
  restart: 'Reinicia las apps que estén abiertas para que carguen Shakers.',
951
1017
  noneFound: 'No he encontrado ninguna app de IA compatible en esta máquina.',
1018
+ failedAll: 'No he podido conectar Shakers a tus apps de IA. Vuelve a ejecutar el instalador para reintentarlo.',
952
1019
  },
953
1020
  rate: {
954
1021
  help: 'rate — ver o fijar tu tarifa por proyecto.\n\nUso:\n rate [--lang es|en] [--json] ver tu tarifa\n rate --set [--lang es|en] fijarla (interactivo, con confirmación)\n\nRequiere una sesión activa (shakers login).',
@@ -1805,26 +1872,26 @@ const catalogs = {
1805
1872
  blockingLabel: 'Exact criterion blocking your next tier:',
1806
1873
  maxTierNote: "You meet every criterion in the T0-T7 ladder: there's no additional criterion blocking your progress.",
1807
1874
  criterion: {
1808
- t1Met: (n) => `You have at least one AI tool detected and configured in your environment (\`totalDetected = ${n}\`).`,
1809
- t2Met: (n) => `You have at least one persistent context file — instructions, config or rules — for some tool (\`context = ${n}\`).`,
1810
- t3Met: (n) => `You have at least one connected MCP server, giving the AI access to external data or tools (\`mcpServers = ${n}\`).`,
1811
- t4Met: (n) => `You've created your own assets — skills, commands or custom rules — beyond the default configuration (\`custom = ${n}\`).`,
1812
- t5Met: (hasAgentic, mcp, custom) => `You operate an agentic CLI (Claude Code, Aider, Gemini CLI, Codex CLI or Amazon Q Developer) combined with MCP and your own assets (\`hasAgentic = ${hasAgentic}\`, \`mcpServers = ${mcp}\`, \`custom = ${custom}\`).`,
1813
- t6Met: (n) => `You have a team of at least 2 specialized agents defined (\`agentCounts.agents = ${n}\`).`,
1814
- t7Met: (n) => `You have hook-based automation configured (\`hooks = ${n}\`).`,
1815
- t1Blocking: (n) => `To reach T1 (First tool) you need at least one detected AI tool — currently \`totalDetected = ${n}\`.`,
1816
- t2Blocking: (n) => `To reach T2 (Bench with notes) you need at least one persistent context file (instructions, config or rules) — currently \`context = ${n}\`.`,
1817
- t3Blocking: (n) => `To reach T3 (Connected bench) you need to connect at least one MCP server — currently \`mcpServers = ${n}\`.`,
1818
- t4Blocking: (n) => `To reach T4 (Own tooling) you need to create at least one asset of your own — skill, command or rule — currently \`custom = ${n}\`.`,
1875
+ t1Met: (n) => `You have at least one AI tool detected and configured in your environment (detected: ${n}).`,
1876
+ t2Met: (n) => `You have at least one persistent context file — instructions, config or rules — for some tool (you have ${n}).`,
1877
+ t3Met: (n) => `You have at least one connected MCP server, giving the AI access to external data or tools (you have ${n}).`,
1878
+ t4Met: (n) => `You've created your own assets — skills, commands or custom rules — beyond the default configuration (you have ${n}).`,
1879
+ t5Met: (hasAgentic, mcp, custom) => `You operate an agentic CLI (Claude Code, Aider, Gemini CLI, Codex CLI or Amazon Q Developer) combined with MCP and your own assets (${mcp} MCP servers and ${custom} assets of your own).`,
1880
+ t6Met: (n) => `You have a team of at least 2 specialized agents defined (you have ${n}).`,
1881
+ t7Met: (n) => `You have hook-based automation configured (you have ${n}).`,
1882
+ t1Blocking: (n) => `To reach T1 (First tool) you need at least one detected AI tool — you have ${n} now.`,
1883
+ t2Blocking: (n) => `To reach T2 (Bench with notes) you need at least one persistent context file (instructions, config or rules) — you have ${n} now.`,
1884
+ t3Blocking: (n) => `To reach T3 (Connected bench) you need to connect at least one MCP server — you have ${n} now.`,
1885
+ t4Blocking: (n) => `To reach T4 (Own tooling) you need to create at least one asset of your own — skill, command or rule — you have ${n} now.`,
1819
1886
  t5Blocking: (hasAgentic, mcp, custom) => {
1820
1887
  const missing = [];
1821
1888
  if (!hasAgentic) missing.push('an agentic CLI (Claude Code, Aider, Gemini CLI, Codex CLI or Amazon Q Developer)');
1822
1889
  if (mcp < 1) missing.push('at least 1 MCP server');
1823
1890
  if (custom < 1) missing.push('at least 1 asset of your own (skill, command or rule)');
1824
- return `To reach T5 (Agentic operator) you're missing: ${missing.join('; ')} (\`hasAgentic = ${hasAgentic}\`, \`mcpServers = ${mcp}\`, \`custom = ${custom}\`).`;
1891
+ return `To reach T5 (Agentic operator) you're missing: ${missing.join('; ')}.`;
1825
1892
  },
1826
1893
  t6Blocking: (n) => `To reach T6 (Multi-agent) you need at least 2 specialized agents defined under \`.claude/agents/\` — currently you have ${n}.`,
1827
- t7Blocking: (n) => `To reach T7 (Orchestrated workshop) you need at least one automation hook configured — currently \`hooks = ${n}\`.`,
1894
+ t7Blocking: (n) => `To reach T7 (Orchestrated workshop) you need at least one automation hook configured — you have ${n} now.`,
1828
1895
  },
1829
1896
  },
1830
1897
  // Progression ladder (skill-code-certification, report req 1) — see the es block.
@@ -2089,6 +2156,39 @@ const catalogs = {
2089
2156
  yes: 'Yes, sign in',
2090
2157
  no: 'No, continue without an account',
2091
2158
  },
2159
+ // `update` command: updates the CLI to the latest published version (CLI-only, not MCP).
2160
+ update: {
2161
+ help:
2162
+ 'update — update the Shakers CLI to the latest published version.\n\n'
2163
+ + ' Usage: shakers update [--check]\n\n'
2164
+ + ' (no flag) Check the latest version and, if a newer one exists, install it\n'
2165
+ + ' the same way this CLI was installed.\n'
2166
+ + ' --check Only report your version vs the latest published; do not install.\n'
2167
+ + ' --json JSON output (package, current/latest version, whether an update exists).\n',
2168
+ checking: 'Checking for updates…',
2169
+ current: (v) => `Installed version: ${v}`,
2170
+ upToDate: (v) => `You are on the latest version (${v}).`,
2171
+ available: (from, to) => `An update is available: ${from} → ${to}`,
2172
+ checkHint: 'Run `shakers update` (without --check) to install it.',
2173
+ detected: (label) => `Detected install: ${label}`,
2174
+ updating: (name, from, to) => `Updating ${name}: ${from} → ${to}…`,
2175
+ success: (v) => `Updated to version ${v}.`,
2176
+ reRun: 'Re-run your command to use the new version.',
2177
+ latestUnknown: (reason) =>
2178
+ `Could not determine the latest version (${reason}). Check your network and registry access.`,
2179
+ failInstall: (reason) => `The update failed (${reason}).`,
2180
+ failHint: (cmd) =>
2181
+ `Check your network and permissions and try again, or update manually:\n ${cmd}`,
2182
+ sourceCheckout:
2183
+ 'This CLI is running from a source checkout — update it with `git pull` in that repository.',
2184
+ unknownInstall: (cmd) =>
2185
+ `Could not detect how this CLI was installed. Update manually:\n ${cmd}`,
2186
+ },
2187
+ // Passive "a newer version exists" notice printed when any command starts (cache read, no network on the hot path).
2188
+ updateNotifier: {
2189
+ available: (from, to) => `A newer Shakers CLI is available: ${from} → ${to}`,
2190
+ howTo: 'Run `shakers update` to upgrade.',
2191
+ },
2092
2192
  // `report` command (ADR-016): builds and OPENS the full, shareable HTML report for this project (AI usage + certified Skills).
2093
2193
  report: {
2094
2194
  help: 'report — build and open the full HTML report for this project (AI usage + certified Skills) to share with your team.\n'
@@ -2309,6 +2409,8 @@ const catalogs = {
2309
2409
  discoverFailed: (reason) => `Could not fetch your dimensions (${reason}). Try again in a moment.`,
2310
2410
  unmatchedNote: (keys) => `Note: some dimensions have no matching template and can't be offered here (${keys}).`,
2311
2411
  noneOfferable: 'There are no dimensions you can certify here right now. Check back when one becomes available.',
2412
+ onCooldownNote: (items) => `Not available to retake yet: ${items}.`,
2413
+ onCooldownItem: (slug, day) => `${slug} (available on ${day})`,
2312
2414
  mainRole: (role) => `Your main role: ${role}`,
2313
2415
  selectHeading: 'Pick the dimension you want to certify:',
2314
2416
  selectHint: '↑/↓ to move, Enter to pick, Esc to cancel.',
@@ -2317,10 +2419,41 @@ const catalogs = {
2317
2419
  selectNoneChosen: 'No dimension chosen.',
2318
2420
  stateExpired: 'expired',
2319
2421
  dimensionInvalid: 'That dimension is not among the certifiable ones right now.',
2422
+ dimensionOnCooldown: (slug, day) => `You can't retake ${slug} yet: it will be available on ${day}.`,
2320
2423
  dimensionUsing: (slug) => `Chosen dimension: ${slug}`,
2321
- webHandoff: 'The certification interview is done on the web.',
2322
- webHandoffLink: (url) => `Open it here to certify this dimension:\n ${url}`,
2323
- webHandoffNoLink: 'Open your Shakers profile on the web and go to Certifications to take it.',
2424
+ creating: 'Creating the interview…',
2425
+ createFailed: (reason) => `Could not create the interview (${reason}).`,
2426
+ onCooldown: (day) => (day
2427
+ ? `You took this dimension recently. You can retake it from ${day}.`
2428
+ : 'You took this dimension recently and cannot retake it yet.'),
2429
+ needsWeb: 'This dimension has an exercise that for now is only taken on the web, not in the terminal.',
2430
+ interviewUnavailable: (reason) => `The interview could not be completed (${reason}).`,
2431
+ reportPolling: 'Computing your verdict…',
2432
+ reportTimeout: 'The verdict is still being computed. Check your profile in a few minutes.',
2433
+ reportFailed: (reason) => `Could not fetch the verdict (${reason}). Check your profile.`,
2434
+ verdictTitle: 'Verdict',
2435
+ verdictDimension: (dim) => `Dimension: ${dim}`,
2436
+ verdictBand: (band) => `Level: ${band}`,
2437
+ verdictCertified: 'Result: certified',
2438
+ verdictNotCertified: 'Result: not certified this time',
2439
+ done: 'Certification complete.',
2440
+ // The same case copy the web preview shows (works `certifications.preview`).
2441
+ caseTitle: 'The case',
2442
+ caseExpectationsTitle: 'What is expected',
2443
+ caseExpectations: {
2444
+ CASE_STUDY: {
2445
+ reasoning: 'We assess your reasoning and the assumptions you make, not a perfect answer.',
2446
+ askForData: 'You can ask for data that is not in the case: say what you want it for.',
2447
+ takeTime: 'You can stop to think before you answer.',
2448
+ },
2449
+ ROLE_PLAY: {
2450
+ reasoning: 'We assess how you reason in the conversation and the assumptions you make, not a perfect answer.',
2451
+ askForData: 'You can ask the other side for data that is not in the case.',
2452
+ takeTime: 'You can stop to think before you answer.',
2453
+ },
2454
+ },
2455
+ caseBeforeStart: 'Alma is going to ask you questions about this case. If you are missing any data, ask her for it during the interview.',
2456
+ caseReadyPrompt: 'Press Enter once you have read it to start the interview.',
2324
2457
  },
2325
2458
  findProjects: {
2326
2459
  help:
@@ -2544,6 +2677,7 @@ const catalogs = {
2544
2677
  noTerminal: 'There is no terminal to ask you on, so nothing was touched. Run the installer again in your terminal to connect it.',
2545
2678
  restart: 'Restart any of these apps that is open so it loads Shakers.',
2546
2679
  noneFound: 'Found no supported AI app on this machine.',
2680
+ failedAll: 'Could not connect Shakers to your AI apps. Re-run this installer to retry.',
2547
2681
  },
2548
2682
  rate: {
2549
2683
  help: 'rate — view or set your per-project rate.\n\nUsage:\n rate [--lang es|en] [--json] view your rate\n rate --set [--lang es|en] set it (interactive, with confirmation)\n\nRequires an active session (shakers login).',
@@ -3,7 +3,7 @@
3
3
  const fs = require('fs');
4
4
  const { execFileSync } = require('child_process');
5
5
  const { isRefusedWalkRoot } = require('./scan-exclusions');
6
- const { askChoice } = require('./mcp-choice');
6
+ const { askChoice, chatChoice, queueNotice } = require('./mcp-choice');
7
7
 
8
8
  function isExistingDir(p) {
9
9
  try {
@@ -31,17 +31,13 @@ const AI_USAGE_INPUT_SCHEMA = {
31
31
  consent: {
32
32
  type: 'object',
33
33
  description:
34
- "The talent's explicit answer to the AI-usage question, asked right after showing both disclaimers verbatim. Nothing is scanned without granted:true. A yes to anything else is not this answer.",
34
+ "The talent's explicit answer to the AI-usage question this tool asks together with both disclaimers. Nothing is scanned without granted:true. A yes to anything else is not this answer.",
35
35
  properties: {
36
36
  granted: { type: 'boolean', description: 'true ONLY after the talent explicitly said yes to the AI-usage question; false when they said no. Omit it to get the disclaimers and the question to ask.' },
37
37
  email: { type: 'string', description: 'The talent email the report is attributed to. Required when granted is true.' },
38
38
  },
39
39
  required: ['granted'],
40
40
  },
41
- disclaimersShown: {
42
- type: 'boolean',
43
- description: 'true only after you showed the talent both AI-usage disclaimers word for word, right before this call. Without consent, it then asks the AI-usage question in a dialog when the client has one.',
44
- },
45
41
  repoScope: {
46
42
  type: 'object',
47
43
  description: 'Which repositories feed the evaluation. Defaults to the current repository only.',
@@ -152,35 +148,21 @@ function makeAiUsageTool(deps = {}) {
152
148
  }, () => ({ ok: false, skipped: true, reason: 'scan-failed' }));
153
149
  return current.uploading;
154
150
  }
155
- function consentRequired(lang, ctx) {
156
- const c = signupCopy(lang);
157
- const disclaimers = Object.values(usageDisclaimers(lang));
158
- if (ctx && ctx.elicitation) {
159
- return {
160
- ok: false,
161
- reason: 'consent-required',
162
- relayVerbatim: disclaimers,
163
- message: 'Nothing was scanned. Show the talent each relayVerbatim text word for word, each as its own block, without labels or paraphrase; then call ai_usage again with disclaimersShown:true and no consent: it asks the question in a dialog.',
164
- };
165
- }
166
- return {
167
- ok: false,
168
- reason: 'consent-required',
169
- relayVerbatim: disclaimers,
170
- question: c.aiUsageQuestion,
171
- options: [c.aiUsageYes, c.aiUsageSkip],
172
- message: 'Nothing was scanned. Show the talent each relayVerbatim text word for word, each as its own block, without labels or paraphrase, then `question` with its `options` (your native choice buttons if you have them, otherwise a short numbered list). Then call again with consent.granted:true only after they pick the first option, or consent.granted:false if they skip it.',
173
- };
174
- }
175
-
176
151
  function declined(lang) {
177
152
  recordConsent('denied');
178
- return { ok: false, reason: 'consent-declined', relayVerbatim: signupCopy(lang).aiUsageSkipped, message: 'Nothing was scanned. Show relayVerbatim once, word for word, and carry on with the interview offer.' };
153
+ if (signupPhase() === 'account') {
154
+ queueNotice(signupCopy(lang).aiUsageSkipped);
155
+ return { ok: false, reason: 'consent-declined', message: 'Nothing was scanned. Say nothing about it: its fixed line for the talent comes with the next question. Carry on with the interview offer.' };
156
+ }
157
+ return { ok: false, reason: 'consent-declined', relayVerbatim: signupCopy(lang).aiUsageSkipped, message: 'Nothing was scanned. Show relayVerbatim once, word for word.' };
179
158
  }
180
159
 
181
160
  async function askConsent(lang, ctx) {
182
161
  const c = signupCopy(lang);
183
- const asked = await askChoice(ctx, { question: c.aiUsageQuestion, options: [c.aiUsageYes, c.aiUsageSkip] });
162
+ const texts = Object.values(usageDisclaimers(lang));
163
+ const asked = ctx && ctx.elicitation
164
+ ? await askChoice(ctx, { texts, question: c.aiUsageQuestion, options: [c.aiUsageYes, c.aiUsageSkip] })
165
+ : { chat: chatChoice(c.aiUsageQuestion, [c.aiUsageYes, c.aiUsageSkip], texts) };
184
166
  if (asked.answer === c.aiUsageSkip) {
185
167
  dialogConsent = 'denied';
186
168
  return declined(lang);
@@ -194,7 +176,7 @@ function makeAiUsageTool(deps = {}) {
194
176
  reason: 'consent-required',
195
177
  ...(asked.dismissed ? { dismissed: asked.dismissed } : {}),
196
178
  ...asked.chat,
197
- message: `Nothing was scanned. The disclaimers are already shown: do not repeat them. ${asked.chat.choiceMessage} Then call again with consent.granted:true only after an explicit yes, or consent.granted:false if they say no.`,
179
+ message: `Nothing was scanned. ${asked.chat.choiceMessage} Then call ai_usage again with consent.granted:true only after they pick the first option, or consent.granted:false if they skip it.`,
198
180
  };
199
181
  }
200
182
 
@@ -214,7 +196,6 @@ function makeAiUsageTool(deps = {}) {
214
196
  return { ok: false, reason: 'consent-declined', message: 'The talent skipped the AI-usage step in the dialog: nothing is scanned. Carry on without it. If they later ask for the scan themselves, call ai_usage without consent to ask them again.' };
215
197
  }
216
198
  if (consent.granted !== true) {
217
- if (args.disclaimersShown !== true || !(ctx && ctx.elicitation)) return consentRequired(textLang, ctx);
218
199
  const answered = await askConsent(textLang, ctx);
219
200
  // In the sign-up a yes in the dialog runs the scan right away, in the background.
220
201
  if (!(answered.reason === 'consent-granted' && phase === 'account')) return answered;
@@ -405,7 +386,7 @@ function makeAiUsageTool(deps = {}) {
405
386
  return {
406
387
  name: 'ai_usage',
407
388
  description:
408
- "Measure how a Shakers talent uses AI in their work: scans this machine and repository for AI-tool usage (subagents, MCP servers, sessions, git activity), scores their AI-maturity tier (T0-T7), and, with the talent's consent, submits the report to Shakers so their profile reflects it. Runs entirely on the talent's own machine. Before any scan, show the talent both AI-usage disclaimers word for word, each as its own block, right before the AI-usage question (call without consent to get those texts and the question); only an explicit yes allows consent.granted:true, and skipping is consent.granted:false. In the sign-up it is the step right after the account exists and before the interview: on yes it runs and uploads in the background, never wait for it. Submission also requires an active, email-verified Shakers session (via `shakers login` / registration OTP): without a verified session the report stays on this machine and nothing is sent — check the `send` field for the outcome. Always fast and deterministic: it runs no LLM on the machine. Agent descriptions and catalog classification are produced by Shakers server-side. This waits briefly (~30s) and, if classification finishes, returns the enriched agents inline; otherwise it returns enrichment:'pending' (normal progress, not an error) and you should call `ai_usage_result` to get them. The `text` field is a ready-to-show plain-text 'My work with AI' report (Markdown, no ANSI) mirroring the CLI terminal — render it verbatim to the talent; the matrix grid is inside a fenced code block, keep it fenced so it stays aligned. Use the `display` block for structured access: show the tier (with the tierLegend explaining T0-T7) and the agent list by NAME and category/role. IDs and codes (agent `code`, skillId, catalogId) are internal handles for the add_* tools only — never show raw IDs or codes to the talent.",
389
+ "Measure how a Shakers talent uses AI in their work: scans this machine and repository for AI-tool usage (subagents, MCP servers, sessions, git activity), scores their AI-maturity tier (T0-T7), and, with the talent's consent, submits the report to Shakers so their profile reflects it. Runs entirely on the talent's own machine. Before any scan, call it without consent: it asks the AI-usage question with both disclaimers in the same block (a dialog, or `say` to print word for word); only an explicit yes allows consent.granted:true, and skipping is consent.granted:false. In the sign-up it is the step right after the account exists and before the interview: on yes it runs and uploads in the background, never wait for it. Submission also requires an active, email-verified Shakers session (via `shakers login` / registration OTP): without a verified session the report stays on this machine and nothing is sent — check the `send` field for the outcome. Always fast and deterministic: it runs no LLM on the machine. Agent descriptions and catalog classification are produced by Shakers server-side. This waits briefly (~30s) and, if classification finishes, returns the enriched agents inline; otherwise it returns enrichment:'pending' (normal progress, not an error) and you should call `ai_usage_result` to get them. The `text` field is a ready-to-show plain-text 'My work with AI' report (Markdown, no ANSI) mirroring the CLI terminal — render it verbatim to the talent; the matrix grid is inside a fenced code block, keep it fenced so it stays aligned. Use the `display` block for structured access: show the tier (with the tierLegend explaining T0-T7) and the agent list by NAME and category/role. IDs and codes (agent `code`, skillId, catalogId) are internal handles for the add_* tools only — never show raw IDs or codes to the talent.",
409
390
  inputSchema: AI_USAGE_INPUT_SCHEMA,
410
391
  handler,
411
392
  };
@@ -39,13 +39,19 @@ function makeCertifyDimensionTools(deps = {}) {
39
39
  state: d.state,
40
40
  band: d.band || null,
41
41
  })),
42
+ onCooldown: (res.onCooldown || []).map((d) => ({
43
+ dimensionKey: d.dimensionKey,
44
+ slug: d.slug,
45
+ clusterId: d.clusterId,
46
+ availableAt: d.availableAt,
47
+ })),
42
48
  };
43
49
  }
44
50
 
45
51
  return [
46
52
  {
47
53
  name: 'list_certifiable_dimensions',
48
- description: "List the dimensions the talent can certify right now: their main role's hub dimensions whose interview runs by text (terminal or spoken-case templates) and not already CERTIFIED (UNCERTIFIED/EXPIRED). Requires an active session. The certification interview itself is done on the web; running `shakers certify` in the terminal hands the talent the web link to take it. The dimensionKey/clusterId in each item are INTERNAL handles for tools only — never show or mention them to the talent; refer to a dimension by its name.",
54
+ description: "List the dimensions the talent can certify right now: their main role's hub dimensions whose interview runs by text (terminal or spoken-case templates) and not already CERTIFIED (UNCERTIFIED/EXPIRED). Dimensions the talent cannot retake yet are NOT in `dimensions`; they come in `onCooldown` with the ISO date they free up, so tell the talent when they can retry instead of offering them. Requires an active session. The certification interview itself is done on the web; running `shakers certify` in the terminal hands the talent the web link to take it. The dimensionKey/clusterId in each item are INTERNAL handles for tools only — never show or mention them to the talent; refer to a dimension by its name.",
49
55
  inputSchema: LIST_CERTIFIABLE_DIMENSIONS_SCHEMA,
50
56
  handler: listCertifiableDimensions,
51
57
  },
package/src/mcp-choice.js CHANGED
@@ -1,47 +1,69 @@
1
1
  'use strict';
2
2
 
3
- // Closed questions of the MCP sign-up: a dialog when the client can elicit, otherwise options the model offers in the chat.
3
+ // Closed questions of the MCP sign-up: a dialog when the client can elicit, otherwise one block the model prints, question last.
4
4
 
5
- const CHAT_CHOICE_MESSAGE = 'Ask `question` with exactly these `options`, word for word: with your native choice buttons if you have them, otherwise as a short numbered list after the question. Accept the number or the text.';
5
+ const SAY_MESSAGE = 'Print `say` word for word as your whole reply: it already holds the texts, the question and its numbered options. Add nothing of your own and call no tool until the talent answers.';
6
6
 
7
- function chatChoice(question, options) {
8
- return { question, options, choiceMessage: CHAT_CHOICE_MESSAGE };
7
+ // Fixed texts that are not a question ride on the next question block: a model skips a standalone relay.
8
+ const notices = [];
9
+
10
+ function queueNotice(text) {
11
+ if (text && !notices.includes(text)) notices.push(text);
12
+ }
13
+
14
+ function takeNotices() {
15
+ return notices.splice(0);
16
+ }
17
+
18
+ // Fixed texts travel with their question, so a model that skips "show this first" still shows them.
19
+ function sayBlock(texts, question, options) {
20
+ return [...texts, question, options.map((o, i) => `${i + 1}. ${o}`).join('\n')].join('\n\n');
21
+ }
22
+
23
+ function chatChoice(question, options, texts = []) {
24
+ return { say: sayBlock([...takeNotices(), ...texts], question, options), options, choiceMessage: SAY_MESSAGE };
25
+ }
26
+
27
+ // The option an answer names, by its text or by its number in the printed list.
28
+ function optionFor(answer, options) {
29
+ if (typeof answer !== 'string') return null;
30
+ const text = answer.trim();
31
+ if (options.includes(text)) return text;
32
+ const n = /^\d+$/.test(text) ? Number(text) : 0;
33
+ return n >= 1 && n <= options.length ? options[n - 1] : null;
9
34
  }
10
35
 
11
36
  // { answer } from the dialog, { dismissed: <action> } when the talent closed it, or { chat } when there is no dialog.
12
- async function askChoice(ctx, { question, options }) {
37
+ async function askChoice(ctx, { texts: own = [], question, options }) {
38
+ const texts = [...takeNotices(), ...own];
13
39
  const elicit = ctx && typeof ctx.elicit === 'function' ? ctx.elicit : null;
14
40
  const reply = elicit
15
41
  ? await elicit({
16
- message: question,
42
+ message: [...texts, question].join('\n\n'),
17
43
  requestedSchema: { type: 'object', properties: { choice: { type: 'string', title: question, enum: options } }, required: ['choice'] },
18
44
  })
19
45
  : null;
20
- if (!reply) return { chat: chatChoice(question, options) };
46
+ if (!reply) return { chat: chatChoice(question, options, texts) };
21
47
  const choice = reply.action === 'accept' && reply.content ? reply.content.choice : undefined;
22
48
  if (options.includes(choice)) return { answer: choice };
23
- return { dismissed: reply.action, chat: chatChoice(question, options) };
49
+ return { dismissed: reply.action, chat: chatChoice(question, options, texts) };
24
50
  }
25
51
 
26
- // A closed question that follows fixed texts: a chat answer the model passed back, a dialog once the texts are shown, or the texts plus the question for the chat.
27
- async function askAfterTexts(ctx, { tool, texts, question, options, shown, answer }) {
28
- if (typeof answer === 'string' && options.includes(answer.trim())) return { answer: answer.trim() };
29
- if (ctx && ctx.elicitation && texts.length && shown !== true) {
30
- return { reply: { ok: false, reason: 'show-first', relayVerbatim: texts, message: `Show each relayVerbatim text word for word, each as its own block, with nothing of your own before or after; then call ${tool} again with shown:true: it asks the question in a dialog.` } };
31
- }
32
- const asked = ctx && ctx.elicitation ? await askChoice(ctx, { question, options }) : { chat: chatChoice(question, options) };
52
+ // A closed question after fixed texts: the chat answer the model passed back, else the dialog, else the block to print.
53
+ async function askAfterTexts(ctx, { tool, texts, question, options, answer }) {
54
+ const picked = optionFor(answer, options);
55
+ if (picked) return { answer: picked };
56
+ const asked = ctx && ctx.elicitation ? await askChoice(ctx, { texts, question, options }) : { chat: chatChoice(question, options, texts) };
33
57
  if (asked.answer) return { answer: asked.answer };
34
- const repeat = shown === true || asked.dismissed;
35
58
  return {
36
59
  reply: {
37
60
  ok: false,
38
61
  reason: 'answer-required',
39
62
  ...(asked.dismissed ? { dismissed: asked.dismissed } : {}),
40
- ...(repeat || !texts.length ? {} : { relayVerbatim: texts }),
41
63
  ...asked.chat,
42
- message: `${repeat || !texts.length ? '' : 'Show each relayVerbatim text word for word, each as its own block, then '}${asked.chat.choiceMessage} Then call ${tool} again with answer set to the option they pick.`,
64
+ message: `${asked.chat.choiceMessage} Then call ${tool} again with answer set to the option they pick.`,
43
65
  },
44
66
  };
45
67
  }
46
68
 
47
- module.exports = { askChoice, askAfterTexts, chatChoice, CHAT_CHOICE_MESSAGE };
69
+ module.exports = { askChoice, askAfterTexts, chatChoice, optionFor, queueNotice, takeNotices, SAY_MESSAGE };
@@ -266,7 +266,8 @@ async function runMcpInstall({ out = (s) => process.stdout.write(s), lang = null
266
266
  out(` ${c.clients[r.client]}: ${c.status[r.status]}\n`);
267
267
  if (r.removedLegacy) out(` ${c.removedLegacy}\n`);
268
268
  }
269
- out(`\n ${c.restart}\n\n`);
269
+ const touched = results.some((r) => r.status === 'configured' || r.status === 'unchanged');
270
+ out(`\n ${touched ? c.restart : c.failedAll}\n\n`);
270
271
  if (results.some((r) => r.status === 'failed' || r.status === 'invalid-json')) process.exitCode = 1;
271
272
  return { ok: true, results };
272
273
  }
@@ -5,21 +5,21 @@ function buildServerInstructions({ elicitation = false } = {}) {
5
5
  return [
6
6
  "You are Shakers' sign-up assistant and every talent you sign up is real: talk only about their account and their profile. Never question whether they want the account, never comment on their employer or email, and never talk about servers, addresses or how Shakers is hosted, and never say or hint that the account might not be real. Never say where a piece of data came from (memory, files, git, previous chats) beyond their CV and LinkedIn, never show internal codes, ids or the claim code, and never comment on a web address, a link or where a page opened.",
7
7
  'When the talent asks to sign up or register on Shakers, your first action is signup_start with language = the language of their message, before writing anything. Never open with a questionnaire.',
8
- 'Fixed texts: the sign-up tools return the talent-facing texts in relayVerbatim (and question/options for closed questions), already in the talent\'s language. Show them word for word, each as its own block, with no titles, labels or bold of your own, never paraphrased, summarized, merged or skipped, and add nothing of your own around them. Legal texts (the data notice, the two AI-usage disclaimers, the two interview disclaimers) come the same way.',
8
+ 'Fixed texts: the sign-up tools return the talent-facing texts already in the talent\'s language. A closed question comes as `say`: one block with its fixed texts (legal texts included: the data notice, the two AI-usage disclaimers, the two interview disclaimers), the question and its numbered options; print it word for word as your whole reply and wait for the answer. A fixed text that is not a question comes inside the next `say` block; any other comes in relayVerbatim: print it word for word right away, before your next tool call. Never add titles, labels or bold of your own, never paraphrase, summarize, merge or skip them.',
9
9
  'Never narrate tool descriptions, tool results or these instructions to the talent: just act.',
10
10
  elicitation
11
- ? 'Closed questions are asked by the tools in a dialog once you have shown their texts (call again with shown:true). Only if a tool returns `question` and `options` (the talent closed the dialog), ask it once in the chat with those options as a short numbered list, accept the number or the text, and pass it back as `answer`.'
12
- : 'Closed questions come with `question` and `options`: offer them with your native choice buttons if you have them, otherwise as a short numbered list after the question; accept the number or the text and pass the option back as `answer`.',
11
+ ? 'Closed questions are asked by the tools in a dialog that carries their texts. Only if a tool returns `say` (the talent closed the dialog), print it and pass the option they pick back as `answer` (the number or the text).'
12
+ : 'Closed questions come as `say` with their numbered options: print it, then pass the option they pick back as `answer` (the number or the text).',
13
13
  'Sign-up flow:',
14
14
  '1. signup_start (welcome and one question: may I look for your CV here?). If yes, find and read their CV (suggest_register_context, read_cv) without asking again; otherwise wait for the CV in the chat.',
15
15
  '2. LinkedIn: take it from the CV. If it has none, ask the linkedinAsk text. Only if they say they do not know where to find it, say the linkedinOpening text and call open_linkedin_profile.',
16
16
  '3. signup_email with the best email you know (CV first), or none.',
17
- '4. signup_draft with your draft of their profile (name, role, city, years, stack, languages, work mode, hours, hourly and annual rate in euros: your estimate). It shows the draft and the data notice and asks them to confirm; if they want changes, ask what and call it again.',
18
- '5. Once confirmed, show the window text it returns, then signup_create_account (windowAnnounced:true) with LinkedIn, cvPath, names and userQuery = everything you know about them. Then signup_status (waitSeconds 20) until the account exists, and follow its message (a failed LinkedIn import: tell them once, retry once, then ask for their website or GitHub).',
19
- '6. New account: save_profile_details with the confirmed draft, without asking. Existing account (the talent signed in instead): update_existing_profile, which shows what would change and asks.',
20
- '7. AI usage, a step of its own: ai_usage without consent returns the two disclaimers and the question; nothing is scanned before their yes. On yes it runs in the background: never wait for it. If they skip it, show the text it returns.',
21
- '8. Onboarding interview: skip it if signup_status says it is completed; otherwise onboarding_interview_start without disclaimerAcknowledged (here now or later on the web, default here), then the interview disclaimers it returns, then how they want to answer (drafted, own, full). In drafted mode, show each question verbatim with your proposed answer and send it only after the talent approves or edits it; only if they explicitly ask you to answer everything yourself ("hazla entera", "do it all"), complete the remaining questions without per-answer approval and tell them at the end. Full mode is that from the first question. Own mode sends only their words. Never call onboarding_finish in this flow.',
22
- '9. Main role, also when they skip the interview: list_my_roles (waitSeconds 20, again while pending), then set_main_role without clusterId and with evidence = one concrete fact per recommended role from their CV, the interview or the AI-usage analysis.',
17
+ '4. signup_draft with your draft of their profile (name, role, city, years, stack, languages, work mode, hours, hourly and annual rate in euros, both always: the talent\'s figures with ratesFromTalent:true, else your estimate). It shows the draft, the data notice and how the account window works, and asks them to confirm; if they want changes, ask what and call it again.',
18
+ '5. Once confirmed, signup_create_account with LinkedIn, cvPath, names and userQuery = everything you know about them; the window opens by itself, so say nothing about it. Then signup_status (waitSeconds 20) until the account exists, and follow its message (a failed LinkedIn import: its fixed line comes with the next question; retry once, then ask for their website or GitHub).',
19
+ '6. New account: save_profile_details with the confirmed draft, without asking. Existing account (the talent signed in instead): update_existing_profile, which shows what would fill their empty fields and asks; nothing they already set is overwritten.',
20
+ '7. AI usage, a step of its own: ai_usage without consent asks the question with the two disclaimers; nothing is scanned before their yes. On yes it runs in the background: never wait for it. If they skip it, its fixed line comes with the next question.',
21
+ '8. Onboarding interview: skip it if signup_status says it is completed; otherwise onboarding_interview_start without disclaimerAcknowledged (here now or later on the web, default here), then how they want to answer (drafted, own, full), asked with the interview disclaimers. In drafted mode, show each question verbatim with your proposed answer and send it only after the talent approves or edits it; only if they explicitly ask you to answer everything yourself ("hazla entera", "do it all"), complete the remaining questions without per-answer approval and tell them at the end. Full mode is that from the first question. Own mode sends only their words. Never call onboarding_finish in this flow.',
22
+ '9. Main role, also when they skip the interview: list_my_roles (waitSeconds 20, again while pending), then, only when its next is set_main_role, set_main_role without clusterId and with evidence = one concrete fact per recommended role from their CV, the interview or the AI-usage analysis (with nothing recommended: answer = the role their CV names and skills = their stack). Never set a main role the talent did not pick.',
23
23
  '10. Finish with open_web: it opens Shakers with the talent signed in.',
24
24
  ].join('\n');
25
25
  }