shakers-cli-beta 0.10.4 → 0.12.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/bin/certify.js +1 -1
- package/bin/login.js +44 -111
- package/bin/shakers.js +25 -6
- package/bin/update-check-worker.js +70 -0
- package/bin/update.js +111 -0
- package/package.json +1 -1
- package/src/agents-client.js +1 -1
- package/src/ai-profile-client.js +1 -1
- package/src/applications-client.js +1 -1
- package/src/availability-client.js +1 -1
- package/src/certifications-client.js +1 -1
- package/src/certify-dimension-client.js +0 -79
- package/src/certify-dimension-flow.js +24 -105
- package/src/command-audit.js +105 -0
- package/src/config.js +31 -22
- package/src/find-projects-client.js +1 -1
- package/src/i18n.js +92 -78
- package/src/lang-client.js +1 -1
- package/src/mcp-device-login-tools.js +3 -3
- package/src/mcp-install.js +2 -1
- package/src/mcp-instructions.js +2 -2
- package/src/mcp-onboarding-tools.js +10 -16
- package/src/mcp-role-tools.js +13 -5
- package/src/mcp-signup-client.js +1 -1
- package/src/mcp-signup-tools.js +76 -18
- package/src/profile-client.js +1 -4
- package/src/rate-client.js +1 -2
- package/src/roles-client.js +0 -4
- package/src/saved-positions-client.js +1 -1
- package/src/show-project-client.js +1 -1
- package/src/signup-copy.js +34 -12
- package/src/update-flow.js +223 -0
- package/src/update-notifier.js +113 -0
package/src/i18n.js
CHANGED
|
@@ -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'
|
|
@@ -726,39 +759,9 @@ const catalogs = {
|
|
|
726
759
|
dimensionInvalid: 'Esa dimensión no está entre las certificables ahora mismo.',
|
|
727
760
|
dimensionOnCooldown: (slug, day) => `Aún no puedes repetir ${slug}: estará disponible el ${day}.`,
|
|
728
761
|
dimensionUsing: (slug) => `Dimensión elegida: ${slug}`,
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
? `Ya hiciste esta dimensión hace poco. Podrás repetirla a partir del ${day}.`
|
|
733
|
-
: 'Ya hiciste esta dimensión hace poco y todavía no se puede repetir.'),
|
|
734
|
-
needsWeb: 'Esta dimensión lleva un ejercicio que por ahora solo se hace en la web, no por terminal.',
|
|
735
|
-
interviewUnavailable: (reason) => `La entrevista no pudo completarse (${reason}).`,
|
|
736
|
-
reportPolling: 'Calculando tu veredicto…',
|
|
737
|
-
reportTimeout: 'El veredicto todavía se está calculando. Consúltalo en tu perfil en unos minutos.',
|
|
738
|
-
reportFailed: (reason) => `No se pudo obtener el veredicto (${reason}). Consúltalo en tu perfil.`,
|
|
739
|
-
verdictTitle: 'Veredicto',
|
|
740
|
-
verdictDimension: (dim) => `Dimensión: ${dim}`,
|
|
741
|
-
verdictBand: (band) => `Nivel: ${band}`,
|
|
742
|
-
verdictCertified: 'Resultado: certificada',
|
|
743
|
-
verdictNotCertified: 'Resultado: no certificada esta vez',
|
|
744
|
-
done: 'Certificación completada.',
|
|
745
|
-
// The same case copy the web preview shows (works `certifications.preview`).
|
|
746
|
-
caseTitle: 'El caso',
|
|
747
|
-
caseExpectationsTitle: 'Qué se espera',
|
|
748
|
-
caseExpectations: {
|
|
749
|
-
CASE_STUDY: {
|
|
750
|
-
reasoning: 'Se evalúa tu razonamiento y los supuestos que haces, no una respuesta perfecta.',
|
|
751
|
-
askForData: 'Puedes pedir datos que no están en el caso: di para qué los quieres.',
|
|
752
|
-
takeTime: 'Puedes pararte a pensar antes de responder.',
|
|
753
|
-
},
|
|
754
|
-
ROLE_PLAY: {
|
|
755
|
-
reasoning: 'Se evalúa cómo razonas en la conversación y los supuestos que haces, no una respuesta perfecta.',
|
|
756
|
-
askForData: 'Puedes pedir a la otra parte datos que no están en el caso.',
|
|
757
|
-
takeTime: 'Puedes pararte a pensar antes de responder.',
|
|
758
|
-
},
|
|
759
|
-
},
|
|
760
|
-
caseBeforeStart: 'Alma te va a hacer preguntas sobre este caso. Si te falta algún dato, pídeselo durante la entrevista.',
|
|
761
|
-
caseReadyPrompt: 'Pulsa Enter cuando lo hayas leído para empezar la entrevista.',
|
|
762
|
+
webHandoff: 'Esta certificación se hace en la web. Abre este enlace para empezar la entrevista:',
|
|
763
|
+
webHandoffLink: (url) => ` ${url}`,
|
|
764
|
+
webHandoffNoLink: 'No se pudo generar el enlace. Entra en tu perfil de Shakers y abre la sección de certificaciones.',
|
|
762
765
|
},
|
|
763
766
|
findProjects: {
|
|
764
767
|
help:
|
|
@@ -982,6 +985,7 @@ const catalogs = {
|
|
|
982
985
|
noTerminal: 'No hay una terminal donde preguntarte, así que no he tocado nada. Vuelve a ejecutar el instalador en tu terminal para conectarlo.',
|
|
983
986
|
restart: 'Reinicia las apps que estén abiertas para que carguen Shakers.',
|
|
984
987
|
noneFound: 'No he encontrado ninguna app de IA compatible en esta máquina.',
|
|
988
|
+
failedAll: 'No he podido conectar Shakers a tus apps de IA. Vuelve a ejecutar el instalador para reintentarlo.',
|
|
985
989
|
},
|
|
986
990
|
rate: {
|
|
987
991
|
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).',
|
|
@@ -1210,7 +1214,7 @@ const catalogs = {
|
|
|
1210
1214
|
// Branded mini-shell chrome (skill-code-certification / ADR-014).
|
|
1211
1215
|
login: {
|
|
1212
1216
|
intro: 'Inicia sesión con tu email y contraseña de Shakers.',
|
|
1213
|
-
sessionExpiredIntro: 'Tu sesión anterior caducó. Vuelve a iniciar sesión
|
|
1217
|
+
sessionExpiredIntro: 'Tu sesión anterior caducó. Vuelve a iniciar sesión.',
|
|
1214
1218
|
alreadyLoggedIn: 'Ya has iniciado sesión. Usa `logout` si quieres cambiar de cuenta.',
|
|
1215
1219
|
emailPrompt: 'Email:',
|
|
1216
1220
|
passwordPrompt: 'Contraseña:',
|
|
@@ -1240,16 +1244,19 @@ const catalogs = {
|
|
|
1240
1244
|
googleErrorState: 'La respuesta del navegador no coincidió con lo esperado (posible interferencia). Por seguridad no se inició sesión; inténtalo de nuevo.',
|
|
1241
1245
|
googleErrorDenied: 'Cancelaste el acceso en Google (o no se concedió). No se inició sesión.',
|
|
1242
1246
|
googleErrorGeneric: 'No se pudo iniciar sesión con Google. Inténtalo de nuevo.',
|
|
1243
|
-
//
|
|
1247
|
+
// Device flow (RFC 8628) para TODO método (Google, LinkedIn, email): el Hub tiene la config; abres una URL y, si hace falta, escribes un código.
|
|
1244
1248
|
deviceIntro: (name) => `Inicia sesión con ${name}. Abre el enlace de abajo e introduce el código para autorizar; luego vuelve aquí.`,
|
|
1249
|
+
// Email por device flow: se abre la web de Shakers; reutiliza tu sesión abierta o entra con tu email ahí. Nunca pide contraseña en la terminal.
|
|
1250
|
+
emailDeviceIntro: 'Inicia sesión con tu email. Abrimos la web de Shakers: entra con tu email o aprueba si ya tienes la sesión abierta. No hace falta contraseña aquí.',
|
|
1245
1251
|
deviceVisit: 'Abre esta página en tu navegador:',
|
|
1246
1252
|
deviceCodeLabel: (code) => `e introduce este código: ${code}`,
|
|
1247
1253
|
deviceWaiting: 'Esperando a que autorices en el navegador…',
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1254
|
+
deviceApproveHint: 'Abrimos tu navegador: apruébalo ahí para terminar (o entra con tu email si aún no has iniciado sesión). No hace falta contraseña.',
|
|
1255
|
+
deviceErrorExpired: 'La solicitud de acceso caducó antes de autorizarse. Vuelve a ejecutar `login` cuando estés listo.',
|
|
1256
|
+
deviceErrorDenied: 'Se canceló el acceso (o no se concedió). No se inició sesión.',
|
|
1257
|
+
deviceErrorGeneric: 'No se pudo iniciar sesión. Inténtalo de nuevo.',
|
|
1251
1258
|
chooseMethodHeading: '¿Cómo quieres iniciar sesión?',
|
|
1252
|
-
methodEmail: 'Email
|
|
1259
|
+
methodEmail: 'Email',
|
|
1253
1260
|
methodGoogle: 'Google',
|
|
1254
1261
|
methodLinkedin: 'LinkedIn',
|
|
1255
1262
|
selectHint: 'Flechas ↑/↓ para moverte · enter para elegir · esc para cancelar',
|
|
@@ -2122,6 +2129,39 @@ const catalogs = {
|
|
|
2122
2129
|
yes: 'Yes, sign in',
|
|
2123
2130
|
no: 'No, continue without an account',
|
|
2124
2131
|
},
|
|
2132
|
+
// `update` command: updates the CLI to the latest published version (CLI-only, not MCP).
|
|
2133
|
+
update: {
|
|
2134
|
+
help:
|
|
2135
|
+
'update — update the Shakers CLI to the latest published version.\n\n'
|
|
2136
|
+
+ ' Usage: shakers update [--check]\n\n'
|
|
2137
|
+
+ ' (no flag) Check the latest version and, if a newer one exists, install it\n'
|
|
2138
|
+
+ ' the same way this CLI was installed.\n'
|
|
2139
|
+
+ ' --check Only report your version vs the latest published; do not install.\n'
|
|
2140
|
+
+ ' --json JSON output (package, current/latest version, whether an update exists).\n',
|
|
2141
|
+
checking: 'Checking for updates…',
|
|
2142
|
+
current: (v) => `Installed version: ${v}`,
|
|
2143
|
+
upToDate: (v) => `You are on the latest version (${v}).`,
|
|
2144
|
+
available: (from, to) => `An update is available: ${from} → ${to}`,
|
|
2145
|
+
checkHint: 'Run `shakers update` (without --check) to install it.',
|
|
2146
|
+
detected: (label) => `Detected install: ${label}`,
|
|
2147
|
+
updating: (name, from, to) => `Updating ${name}: ${from} → ${to}…`,
|
|
2148
|
+
success: (v) => `Updated to version ${v}.`,
|
|
2149
|
+
reRun: 'Re-run your command to use the new version.',
|
|
2150
|
+
latestUnknown: (reason) =>
|
|
2151
|
+
`Could not determine the latest version (${reason}). Check your network and registry access.`,
|
|
2152
|
+
failInstall: (reason) => `The update failed (${reason}).`,
|
|
2153
|
+
failHint: (cmd) =>
|
|
2154
|
+
`Check your network and permissions and try again, or update manually:\n ${cmd}`,
|
|
2155
|
+
sourceCheckout:
|
|
2156
|
+
'This CLI is running from a source checkout — update it with `git pull` in that repository.',
|
|
2157
|
+
unknownInstall: (cmd) =>
|
|
2158
|
+
`Could not detect how this CLI was installed. Update manually:\n ${cmd}`,
|
|
2159
|
+
},
|
|
2160
|
+
// Passive "a newer version exists" notice printed when any command starts (cache read, no network on the hot path).
|
|
2161
|
+
updateNotifier: {
|
|
2162
|
+
available: (from, to) => `A newer Shakers CLI is available: ${from} → ${to}`,
|
|
2163
|
+
howTo: 'Run `shakers update` to upgrade.',
|
|
2164
|
+
},
|
|
2125
2165
|
// `report` command (ADR-016): builds and OPENS the full, shareable HTML report for this project (AI usage + certified Skills).
|
|
2126
2166
|
report: {
|
|
2127
2167
|
help: 'report — build and open the full HTML report for this project (AI usage + certified Skills) to share with your team.\n'
|
|
@@ -2354,39 +2394,9 @@ const catalogs = {
|
|
|
2354
2394
|
dimensionInvalid: 'That dimension is not among the certifiable ones right now.',
|
|
2355
2395
|
dimensionOnCooldown: (slug, day) => `You can't retake ${slug} yet: it will be available on ${day}.`,
|
|
2356
2396
|
dimensionUsing: (slug) => `Chosen dimension: ${slug}`,
|
|
2357
|
-
|
|
2358
|
-
|
|
2359
|
-
|
|
2360
|
-
? `You took this dimension recently. You can retake it from ${day}.`
|
|
2361
|
-
: 'You took this dimension recently and cannot retake it yet.'),
|
|
2362
|
-
needsWeb: 'This dimension has an exercise that for now is only taken on the web, not in the terminal.',
|
|
2363
|
-
interviewUnavailable: (reason) => `The interview could not be completed (${reason}).`,
|
|
2364
|
-
reportPolling: 'Computing your verdict…',
|
|
2365
|
-
reportTimeout: 'The verdict is still being computed. Check your profile in a few minutes.',
|
|
2366
|
-
reportFailed: (reason) => `Could not fetch the verdict (${reason}). Check your profile.`,
|
|
2367
|
-
verdictTitle: 'Verdict',
|
|
2368
|
-
verdictDimension: (dim) => `Dimension: ${dim}`,
|
|
2369
|
-
verdictBand: (band) => `Level: ${band}`,
|
|
2370
|
-
verdictCertified: 'Result: certified',
|
|
2371
|
-
verdictNotCertified: 'Result: not certified this time',
|
|
2372
|
-
done: 'Certification complete.',
|
|
2373
|
-
// The same case copy the web preview shows (works `certifications.preview`).
|
|
2374
|
-
caseTitle: 'The case',
|
|
2375
|
-
caseExpectationsTitle: 'What is expected',
|
|
2376
|
-
caseExpectations: {
|
|
2377
|
-
CASE_STUDY: {
|
|
2378
|
-
reasoning: 'We assess your reasoning and the assumptions you make, not a perfect answer.',
|
|
2379
|
-
askForData: 'You can ask for data that is not in the case: say what you want it for.',
|
|
2380
|
-
takeTime: 'You can stop to think before you answer.',
|
|
2381
|
-
},
|
|
2382
|
-
ROLE_PLAY: {
|
|
2383
|
-
reasoning: 'We assess how you reason in the conversation and the assumptions you make, not a perfect answer.',
|
|
2384
|
-
askForData: 'You can ask the other side for data that is not in the case.',
|
|
2385
|
-
takeTime: 'You can stop to think before you answer.',
|
|
2386
|
-
},
|
|
2387
|
-
},
|
|
2388
|
-
caseBeforeStart: 'Alma is going to ask you questions about this case. If you are missing any data, ask her for it during the interview.',
|
|
2389
|
-
caseReadyPrompt: 'Press Enter once you have read it to start the interview.',
|
|
2397
|
+
webHandoff: 'This certification is done on the web. Open this link to start the interview:',
|
|
2398
|
+
webHandoffLink: (url) => ` ${url}`,
|
|
2399
|
+
webHandoffNoLink: 'Could not build the link. Go to your Shakers profile and open the certifications section.',
|
|
2390
2400
|
},
|
|
2391
2401
|
findProjects: {
|
|
2392
2402
|
help:
|
|
@@ -2610,6 +2620,7 @@ const catalogs = {
|
|
|
2610
2620
|
noTerminal: 'There is no terminal to ask you on, so nothing was touched. Run the installer again in your terminal to connect it.',
|
|
2611
2621
|
restart: 'Restart any of these apps that is open so it loads Shakers.',
|
|
2612
2622
|
noneFound: 'Found no supported AI app on this machine.',
|
|
2623
|
+
failedAll: 'Could not connect Shakers to your AI apps. Re-run this installer to retry.',
|
|
2613
2624
|
},
|
|
2614
2625
|
rate: {
|
|
2615
2626
|
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).',
|
|
@@ -2834,7 +2845,7 @@ const catalogs = {
|
|
|
2834
2845
|
// Branded mini-shell chrome (skill-code-certification / ADR-014).
|
|
2835
2846
|
login: {
|
|
2836
2847
|
intro: 'Log in with your Shakers email and password.',
|
|
2837
|
-
sessionExpiredIntro: 'Your previous session expired. Log in again
|
|
2848
|
+
sessionExpiredIntro: 'Your previous session expired. Log in again.',
|
|
2838
2849
|
alreadyLoggedIn: 'You are already logged in. Use `logout` if you want to switch accounts.',
|
|
2839
2850
|
emailPrompt: 'Email:',
|
|
2840
2851
|
passwordPrompt: 'Password:',
|
|
@@ -2864,17 +2875,20 @@ const catalogs = {
|
|
|
2864
2875
|
googleErrorState: 'The browser response did not match what was expected (possible interference). For safety no session was started; try again.',
|
|
2865
2876
|
googleErrorDenied: 'You cancelled access in Google (or it was not granted). No session was started.',
|
|
2866
2877
|
googleErrorGeneric: 'Could not log in with Google. Try again.',
|
|
2867
|
-
//
|
|
2878
|
+
// Device flow (RFC 8628) for EVERY method (Google, LinkedIn, email): the Hub owns the config; you open a URL and, if needed, type a code.
|
|
2868
2879
|
deviceIntro: (name) => `Sign in with ${name}. Open the link below and enter the code to authorize; then come back here.`,
|
|
2880
|
+
// Email via device flow: the Shakers web opens; reuse your open session or sign in with your email there. It never asks for a password in the terminal.
|
|
2881
|
+
emailDeviceIntro: 'Sign in with your email. We open the Shakers web: sign in with your email, or approve if you are already signed in. No password is needed here.',
|
|
2869
2882
|
deviceVisit: 'Open this page in your browser:',
|
|
2870
2883
|
deviceCodeLabel: (code) => `and enter this code: ${code}`,
|
|
2871
2884
|
deviceWaiting: 'Waiting for you to authorize in the browser…',
|
|
2872
|
-
|
|
2873
|
-
|
|
2874
|
-
|
|
2885
|
+
deviceApproveHint: 'We opened your browser: approve there to finish (or sign in with your email if you are not signed in yet). No password needed.',
|
|
2886
|
+
deviceErrorExpired: 'The sign-in request expired before it was authorized. Run `login` again when you are ready.',
|
|
2887
|
+
deviceErrorDenied: 'The sign-in was cancelled or not granted. No session was started.',
|
|
2888
|
+
deviceErrorGeneric: 'Could not sign in. Try again.',
|
|
2875
2889
|
// Interactive method picker (`login` with no --email/--google/--provider): reuses the SAME single-select picker `certify agents` uses to choose an agent (src/interactive-select.js).
|
|
2876
2890
|
chooseMethodHeading: 'How do you want to log in?',
|
|
2877
|
-
methodEmail: 'Email
|
|
2891
|
+
methodEmail: 'Email',
|
|
2878
2892
|
methodGoogle: 'Google',
|
|
2879
2893
|
methodLinkedin: 'LinkedIn',
|
|
2880
2894
|
selectHint: 'Arrows ↑/↓ to move · enter to pick · esc to cancel',
|
package/src/lang-client.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
// Languages client.
|
|
3
|
+
// Languages client. The
|
|
4
4
|
// catalog (id<->name) = GET `static-data/languages` (@Public); SET = PATCH the
|
|
5
5
|
// same work-details endpoint (REPLACE-ALL: send the full list of {id, level}).
|
|
6
6
|
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
const START_SCHEMA = {
|
|
6
6
|
type: 'object',
|
|
7
7
|
properties: {
|
|
8
|
-
provider: { type: 'string', enum: ['google', 'linkedin'], description: "Optional
|
|
8
|
+
provider: { type: 'string', enum: ['google', 'linkedin', 'email'], description: "Optional provider. Defaults to 'google'. Same device flow either way; it selects which option the Shakers sign-in page opens — 'email' opens the email sign-in (the talent reuses their open web session or signs in there, never a password in the chat)." },
|
|
9
9
|
mode: { type: 'string', enum: ['login', 'register'], description: "Optional. 'login' signs an existing talent in; 'register' creates the account if new (the profile steps come from the register/onboarding tools afterwards). Defaults to 'login'." },
|
|
10
10
|
freelanceType: { type: 'string', description: 'Optional, register only: FREELANCE | EMPLOYEE | AGENCY | POTENTIAL_FREELANCE. Used to complete a new account.' },
|
|
11
11
|
preferredLanguage: { type: 'string', description: 'Optional, register only: es | en | it | pt | fr.' },
|
|
@@ -33,7 +33,7 @@ function makeDeviceLoginTools(deps = {}) {
|
|
|
33
33
|
|
|
34
34
|
async function start(args = {}) {
|
|
35
35
|
const mode = args.mode === 'register' ? 'register' : 'login';
|
|
36
|
-
const provider = args.provider === 'linkedin' ? 'linkedin' : 'google';
|
|
36
|
+
const provider = args.provider === 'linkedin' ? 'linkedin' : args.provider === 'email' ? 'email' : 'google';
|
|
37
37
|
const authorizeEndpoint = getDeviceAuthorizeEndpoint();
|
|
38
38
|
const tokenEndpoint = getDeviceTokenEndpoint();
|
|
39
39
|
const authTokenEndpoint = getAuthTokenEndpoint();
|
|
@@ -157,7 +157,7 @@ function makeDeviceLoginTools(deps = {}) {
|
|
|
157
157
|
return [
|
|
158
158
|
{
|
|
159
159
|
name: 'social_signin_start',
|
|
160
|
-
description: "Begin
|
|
160
|
+
description: "Begin sign-in (or registration) via the device flow — provider 'google' (default), 'linkedin' or 'email'. Email goes through the SAME web flow: the talent reuses their open Shakers session or signs in with their email on the page, never a password in the chat. Returns a verificationUrl and a userCode: show BOTH to the talent verbatim — they open the URL and enter the code in their own browser (they never type anything in the chat, and you never invent or fill the code). Pass mode 'register' to create the account if it is new. After showing them, call social_signin_poll. Returns { ok, status, provider, verificationUrl, userCode, pollIntervalSeconds, ... }.",
|
|
161
161
|
inputSchema: START_SCHEMA,
|
|
162
162
|
handler: start,
|
|
163
163
|
},
|
package/src/mcp-install.js
CHANGED
|
@@ -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
|
-
|
|
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
|
}
|
package/src/mcp-instructions.js
CHANGED
|
@@ -11,10 +11,10 @@ function buildServerInstructions({ elicitation = false } = {}) {
|
|
|
11
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
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
|
-
'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.',
|
|
14
|
+
'1. signup_start (welcome and one question: may I look for your CV here?); a CV path in their first message goes as cvPath and skips the question. 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, 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.',
|
|
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; sources: cv and/or linkedin, only those you have). 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
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
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
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.',
|
|
@@ -5,7 +5,7 @@ const path = require('path');
|
|
|
5
5
|
const LOGIN_SCHEMA = {
|
|
6
6
|
type: 'object',
|
|
7
7
|
properties: {
|
|
8
|
-
method: { type: 'string', enum: ['email', 'google', 'linkedin'], description: "Optional auth method.
|
|
8
|
+
method: { type: 'string', enum: ['email', 'google', 'linkedin'], description: "Optional auth method. None of them sign in here: this tool points you to social_signin_start/social_signin_poll (the device flow) with that provider. 'email' reuses the talent's open Shakers web session or signs them in on the page (never a password in the chat); 'google'/'linkedin' open that social sign-in. Omit it to default to email (the page still offers Google and LinkedIn)." },
|
|
9
9
|
},
|
|
10
10
|
};
|
|
11
11
|
|
|
@@ -92,7 +92,6 @@ function makeOnboardingTools(deps = {}) {
|
|
|
92
92
|
normalizeDomain = require('./start-add-portfolio').normalizeDomain,
|
|
93
93
|
isValidEmail = require('./share').isValidEmail,
|
|
94
94
|
normalizeEmail = require('./share').normalizeEmail,
|
|
95
|
-
runLoopbackAuth = require('./mcp-auth-form').runLoopbackAuth,
|
|
96
95
|
} = deps;
|
|
97
96
|
|
|
98
97
|
const requireSession = () => {
|
|
@@ -118,8 +117,10 @@ function makeOnboardingTools(deps = {}) {
|
|
|
118
117
|
return endpoint;
|
|
119
118
|
};
|
|
120
119
|
|
|
121
|
-
//
|
|
122
|
-
|
|
120
|
+
// Every method runs through the device flow (start/poll tools); a blocking form
|
|
121
|
+
// can't surface the code, and email now reuses the web session there instead of
|
|
122
|
+
// prompting for a password. The start/poll pair persists the session.
|
|
123
|
+
const socialSigninPointer = (mode, provider = 'email') => ({
|
|
123
124
|
ok: false,
|
|
124
125
|
reason: 'use-device-flow',
|
|
125
126
|
next: 'social_signin_start',
|
|
@@ -128,18 +129,11 @@ function makeOnboardingTools(deps = {}) {
|
|
|
128
129
|
message: `For ${provider}, call social_signin_start (provider "${provider}", mode "${mode}") to get a URL and code to show the talent, then social_signin_poll.`,
|
|
129
130
|
});
|
|
130
131
|
|
|
131
|
-
//
|
|
132
|
-
|
|
133
|
-
const result = await runLoopbackAuth(method ? { mode: 'login', method } : { mode: 'login' });
|
|
134
|
-
if (result.viaSocial) return socialSigninPointer('login', result.provider || 'google');
|
|
135
|
-
if (!result.ok) return { ok: false, reason: result.reason || 'login-failed' };
|
|
136
|
-
return { ok: true, method: 'email', email: result.email || null };
|
|
137
|
-
}
|
|
138
|
-
|
|
132
|
+
// Email, Google and LinkedIn all hand off to social_signin_start/poll. Email
|
|
133
|
+
// defaults when no method is given; the /cli-login page still offers the others.
|
|
139
134
|
async function login(args = {}) {
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
return loginViaForm();
|
|
135
|
+
const provider = args.method === 'google' || args.method === 'linkedin' ? args.method : 'email';
|
|
136
|
+
return socialSigninPointer('login', provider);
|
|
143
137
|
}
|
|
144
138
|
|
|
145
139
|
async function logout() {
|
|
@@ -340,7 +334,7 @@ function makeOnboardingTools(deps = {}) {
|
|
|
340
334
|
return [
|
|
341
335
|
{
|
|
342
336
|
name: 'login',
|
|
343
|
-
description: "
|
|
337
|
+
description: "Start signing the talent in to Shakers so the add_* and certify tools can run. This tool does not sign in itself: it points you to social_signin_start + social_signin_poll (the device flow), which open the Shakers web page and persist the session. Email (the default) reuses the talent's open web session or signs them in on the page — never a password in the chat; 'google'/'linkedin' open that social sign-in. Returns a { ok: false, reason: 'use-device-flow', next, provider } pointer; call social_signin_start next.",
|
|
344
338
|
inputSchema: LOGIN_SCHEMA,
|
|
345
339
|
handler: login,
|
|
346
340
|
},
|
package/src/mcp-role-tools.js
CHANGED
|
@@ -22,7 +22,12 @@ const fold = (text) => String(text || '').normalize('NFD').replace(/[\u0300-\u03
|
|
|
22
22
|
|
|
23
23
|
const wordsOf = (text) => fold(text).split(/[^a-z0-9+#.]+/).filter((w) => w.length >= 2);
|
|
24
24
|
|
|
25
|
-
//
|
|
25
|
+
// Words that name no specialty: a role sharing only these with the talent does not match.
|
|
26
|
+
const GENERIC_ROLE_WORDS = new Set(['developer', 'developers', 'dev', 'engineer', 'engineering', 'software', 'programmer', 'specialist', 'expert', 'consultant', 'senior', 'junior', 'lead', 'freelance',
|
|
27
|
+
'desarrollador', 'desarrolladora', 'programador', 'programadora', 'ingeniero', 'ingeniera', 'especialista', 'experto', 'experta', 'consultor', 'consultora',
|
|
28
|
+
'sviluppatore', 'sviluppatrice', 'programmatore', 'ingegnere', 'esperto', 'consulente', 'desenvolvedor', 'desenvolvedora', 'engenheiro', 'engenheira']);
|
|
29
|
+
|
|
30
|
+
// Catalogue roles by relevance: the exact name first, then shared role words, then stack words; a word many roles share (engineer) counts less, and only a specific word qualifies a role.
|
|
26
31
|
function rankRoles(roles, said, skills) {
|
|
27
32
|
const names = roles.map((r) => wordsOf(r.name));
|
|
28
33
|
const df = new Map();
|
|
@@ -31,13 +36,16 @@ function rankRoles(roles, said, skills) {
|
|
|
31
36
|
const asked = new Set(wordsOf(said));
|
|
32
37
|
const stack = new Set(wordsOf(skills));
|
|
33
38
|
const exact = fold(said).trim();
|
|
34
|
-
const
|
|
39
|
+
const specific = (words) => words.filter((w) => !GENERIC_ROLE_WORDS.has(w));
|
|
40
|
+
const shown = (role, words) => specific(words).map((w) => role.name.split(/[^A-Za-z0-9+#.]+/).find((t) => fold(t) === w) || w);
|
|
35
41
|
return roles
|
|
36
42
|
.map((role, i) => {
|
|
37
43
|
const byRole = names[i].filter((w) => asked.has(w));
|
|
38
44
|
const byStack = names[i].filter((w) => stack.has(w) && !asked.has(w));
|
|
39
|
-
const
|
|
40
|
-
|
|
45
|
+
const isExact = !!exact && fold(role.name) === exact;
|
|
46
|
+
const score = (isExact ? 100 : 0) + weight(byRole) + weight(byStack) / 2;
|
|
47
|
+
const matches = isExact || specific([...byRole, ...byStack]).length > 0;
|
|
48
|
+
return { role, score, matches, byRole: shown(role, byRole), byStack: shown(role, byStack) };
|
|
41
49
|
})
|
|
42
50
|
.sort((a, b) => b.score - a.score);
|
|
43
51
|
}
|
|
@@ -269,7 +277,7 @@ function makeRoleTools(deps = {}) {
|
|
|
269
277
|
const roles = byName(available.roles);
|
|
270
278
|
if (!roles.length) return { reply: { ok: false, reason: 'no-roles' } };
|
|
271
279
|
const ranked = rankRoles(roles, said, args.skills);
|
|
272
|
-
const shown = roles.length <= MAX_ROLE_OPTIONS ? ranked : ranked.filter((m) => m.
|
|
280
|
+
const shown = roles.length <= MAX_ROLE_OPTIONS ? ranked : ranked.filter((m) => m.matches).slice(0, MAX_ROLE_OPTIONS);
|
|
273
281
|
if (!shown.length) return askOwnRole();
|
|
274
282
|
// This call's own list: another call may replace `offered` while the dialog is open.
|
|
275
283
|
const options = shown.map((m) => m.role);
|
package/src/mcp-signup-client.js
CHANGED
|
@@ -65,7 +65,7 @@ async function requestMcpSignup(
|
|
|
65
65
|
return { ok: true, claimCode: data.claimCode, claimCodeExpiresAt: data.claimCodeExpiresAt || null };
|
|
66
66
|
}
|
|
67
67
|
|
|
68
|
-
//
|
|
68
|
+
// { state: running | done | failed, sources: { linkedin?, cv? } }, the shape signup_status reports.
|
|
69
69
|
function importStateOf(data) {
|
|
70
70
|
const status = data.profileImportStatus;
|
|
71
71
|
const reported = Array.isArray(data.sources) ? data.sources : [];
|