shakers-cli-beta 0.11.0 → 0.13.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/config.js CHANGED
@@ -264,7 +264,7 @@ function getAuthTokenEndpoint(env = process.env) {
264
264
  function getCompleteRegistrationEndpoint(env = process.env) {
265
265
  const explicit = readEnv(env, 'COMPLETE_REGISTRATION_ENDPOINT');
266
266
  if (explicit && explicit.trim()) return explicit.trim();
267
- return deriveFromHub(env, 'works/auth/complete-registration');
267
+ return deriveFromHub(env, 'works/talent/auth/complete-registration');
268
268
  }
269
269
 
270
270
  // Google via device flow (RFC 8628): the Hub broker owns all Google config; the CLI holds no client_id.
@@ -287,7 +287,7 @@ function getDeviceTokenEndpoint(env = process.env) {
287
287
  function getMyCertificationsEndpoint(env = process.env) {
288
288
  const explicit = readEnv(env, 'MY_CERTIFICATIONS_ENDPOINT');
289
289
  if (explicit && explicit.trim()) return explicit.trim();
290
- return deriveFromHub(env, 'works/certifications/me');
290
+ return deriveFromHub(env, 'works/talent/certifications/me');
291
291
  }
292
292
 
293
293
  // Role/cluster endpoints (hub, talent Bearer). available-roles = the catalog the
@@ -296,19 +296,19 @@ function getMyCertificationsEndpoint(env = process.env) {
296
296
  function getAvailableRolesEndpoint(env = process.env) {
297
297
  const explicit = readEnv(env, 'AVAILABLE_ROLES_ENDPOINT');
298
298
  if (explicit && explicit.trim()) return explicit.trim();
299
- return deriveFromHub(env, 'works/certifications/me/available-roles');
299
+ return deriveFromHub(env, 'works/talent/certifications/me/available-roles');
300
300
  }
301
301
 
302
302
  function getSetMainRoleEndpoint(env = process.env) {
303
303
  const explicit = readEnv(env, 'SET_MAIN_ROLE_ENDPOINT');
304
304
  if (explicit && explicit.trim()) return explicit.trim();
305
- return deriveFromHub(env, 'works/certifications/me/main-role');
305
+ return deriveFromHub(env, 'works/talent/certifications/me/main-role');
306
306
  }
307
307
 
308
308
  function getAssignedClustersEndpoint(env = process.env) {
309
309
  const explicit = readEnv(env, 'ASSIGNED_CLUSTERS_ENDPOINT');
310
310
  if (explicit && explicit.trim()) return explicit.trim();
311
- return deriveFromHub(env, 'works/talents/me/work-details/assigned-clusters');
311
+ return deriveFromHub(env, 'works/talent/me/work-details/assigned-clusters');
312
312
  }
313
313
 
314
314
  function getTemplatesByDimensionEndpoint(env = process.env) {
@@ -326,12 +326,20 @@ function getMyAssessmentsEndpoint(env = process.env) {
326
326
  return base ? `${base}/users/me/assessments` : null;
327
327
  }
328
328
 
329
+ // `certify` creates its dimension interview here (certs, talent Bearer).
330
+ function getCertificationInterviewsEndpoint(env = process.env) {
331
+ const explicit = readEnv(env, 'CERTIFICATION_INTERVIEWS_ENDPOINT');
332
+ if (explicit && explicit.trim()) return explicit.trim();
333
+ const base = getCertsBase(env);
334
+ return base ? `${base}/users/me/certification-interviews` : null;
335
+ }
336
+
329
337
  // Find Positions list (hub `works/positions/find`, talent Bearer). Serves the
330
338
  // All/Recommended/Saved tabs via `criteria.savedByMe`; ordered by match desc.
331
339
  function getFindPositionsEndpoint(env = process.env) {
332
340
  const explicit = readEnv(env, 'FIND_POSITIONS_ENDPOINT');
333
341
  if (explicit && explicit.trim()) return explicit.trim();
334
- return deriveFromHub(env, 'works/positions/find');
342
+ return deriveFromHub(env, 'works/talent/positions/find');
335
343
  }
336
344
 
337
345
  // Marketplace read/write (hub, talent Bearer). The id-taking getters return null
@@ -340,20 +348,20 @@ function getPositionDetailEndpoint(positionId, env = process.env) {
340
348
  const explicit = readEnv(env, 'POSITION_DETAIL_ENDPOINT');
341
349
  if (explicit && explicit.trim()) return explicit.trim();
342
350
  if (positionId == null || typeof positionId === 'object') return null;
343
- return deriveFromHub(env, `works/positions/${encodeURIComponent(positionId)}/detail`);
351
+ return deriveFromHub(env, `works/talent/positions/${encodeURIComponent(positionId)}/detail`);
344
352
  }
345
353
 
346
354
  function getSavePositionEndpoint(env = process.env) {
347
355
  const explicit = readEnv(env, 'SAVE_POSITION_ENDPOINT');
348
356
  if (explicit && explicit.trim()) return explicit.trim();
349
- return deriveFromHub(env, 'works/saved-positions');
357
+ return deriveFromHub(env, 'works/talent/saved-positions');
350
358
  }
351
359
 
352
360
  function getUnsavePositionEndpoint(positionId, env = process.env) {
353
361
  const explicit = readEnv(env, 'UNSAVE_POSITION_ENDPOINT');
354
362
  if (explicit && explicit.trim()) return explicit.trim();
355
363
  if (positionId == null || typeof positionId === 'object') return null;
356
- return deriveFromHub(env, `works/saved-positions/${encodeURIComponent(positionId)}`);
364
+ return deriveFromHub(env, `works/talent/saved-positions/${encodeURIComponent(positionId)}`);
357
365
  }
358
366
 
359
367
  function getReceivedInvitationsEndpoint(env = process.env) {
@@ -365,7 +373,7 @@ function getReceivedInvitationsEndpoint(env = process.env) {
365
373
  function getMyCandidaturesEndpoint(env = process.env) {
366
374
  const explicit = readEnv(env, 'MY_CANDIDATURES_ENDPOINT');
367
375
  if (explicit && explicit.trim()) return explicit.trim();
368
- return deriveFromHub(env, 'works/candidatures/me');
376
+ return deriveFromHub(env, 'works/talent/candidatures/me');
369
377
  }
370
378
 
371
379
  function getMeAvailabilityEndpoint(env = process.env) {
@@ -377,13 +385,13 @@ function getMeAvailabilityEndpoint(env = process.env) {
377
385
  function getMeProfileEndpoint(env = process.env) {
378
386
  const explicit = readEnv(env, 'ME_PROFILE_ENDPOINT');
379
387
  if (explicit && explicit.trim()) return explicit.trim();
380
- return deriveFromHub(env, 'works/me/profile');
388
+ return deriveFromHub(env, 'works/talent/me/profile-summary');
381
389
  }
382
390
 
383
391
  function getTalentMeProfileEndpoint(env = process.env) {
384
392
  const explicit = readEnv(env, 'TALENT_ME_PROFILE_ENDPOINT');
385
393
  if (explicit && explicit.trim()) return explicit.trim();
386
- return deriveFromHub(env, 'works/talents/me/profile');
394
+ return deriveFromHub(env, 'works/talent/me/profile');
387
395
  }
388
396
 
389
397
  // Alma (the AI assistant) is a SEPARATE service — NOT derived from hubBase.
@@ -444,7 +452,7 @@ function getUsageReportEndpoint(env = process.env) {
444
452
  function getAiProfileEndpoint(env = process.env) {
445
453
  const explicit = readEnv(env, 'AI_PROFILE_ENDPOINT');
446
454
  if (explicit && explicit.trim()) return explicit.trim();
447
- return deriveFromHub(env, 'works/me/ai-profile');
455
+ return deriveFromHub(env, 'works/talent/me/ai-profile');
448
456
  }
449
457
 
450
458
  // Evidence emission endpoint for the `cli_scan` instrument (talents-ai-score Phase 2).
@@ -476,7 +484,7 @@ function getPortfolioDraftDescriptionEndpoint(env = process.env) {
476
484
  function getPortfolioUpdateEndpoint(env = process.env) {
477
485
  const explicit = readEnv(env, 'PORTFOLIO_UPDATE_ENDPOINT');
478
486
  if (explicit && explicit.trim()) return explicit.trim();
479
- return deriveFromHub(env, 'works/portfolios');
487
+ return deriveFromHub(env, 'works/talent/portfolios');
480
488
  }
481
489
 
482
490
  function getAgentsDeclareEndpoint(env = process.env) {
@@ -502,7 +510,7 @@ function getAgentPortfoliosEndpoint(agentId, env = process.env) {
502
510
  // Null on missing/non-scalar id: no bogus URL, and keeps config.test.js's `fn(env)`
503
511
  // getter sweep honest when the env object lands in `agentId`.
504
512
  if (agentId == null || typeof agentId === 'object') return null;
505
- return deriveFromHub(env, `works/me/agents/${encodeURIComponent(agentId)}/portfolios`);
513
+ return deriveFromHub(env, `works/talent/me/agents/${encodeURIComponent(agentId)}/portfolios`);
506
514
  }
507
515
 
508
516
  // Onboarding endpoints (evaluation-mcp, ADR-011..015/024 — the register flow).
@@ -533,7 +541,7 @@ function getSignUpEndpoint(env = process.env) {
533
541
  function getImportProfileEndpoint(env = process.env) {
534
542
  const explicit = readEnv(env, 'IMPORT_PROFILE_ENDPOINT');
535
543
  if (explicit && explicit.trim()) return explicit.trim();
536
- return deriveFromHub(env, 'works/me/import-profile');
544
+ return deriveFromHub(env, 'works/talent/me/import-profile');
537
545
  }
538
546
 
539
547
  // MCP sign-up (ticket 39540): public UNREGISTERED + claim code, its import status, and the one-time web login.
@@ -542,7 +550,7 @@ function getMcpSignupEndpoint(env = process.env) {
542
550
  }
543
551
 
544
552
  function getMyImportStatusEndpoint(env = process.env) {
545
- return deriveFromHub(env, 'works/me/import-profile/status');
553
+ return deriveFromHub(env, 'works/talent/me/import-profile/status');
546
554
  }
547
555
 
548
556
  function getOneTimeTokenEndpoint(env = process.env) {
@@ -564,25 +572,25 @@ function getOneTimeLoginUrl(token, env = process.env) {
564
572
  function getProfessionalDetailsEndpoint(env = process.env) {
565
573
  const explicit = readEnv(env, 'PROFESSIONAL_DETAILS_ENDPOINT');
566
574
  if (explicit && explicit.trim()) return explicit.trim();
567
- return deriveFromHub(env, 'works/me/professional-details');
575
+ return deriveFromHub(env, 'works/talent/me/professional-details');
568
576
  }
569
577
 
570
578
  function getPricingRateEndpoint(env = process.env) {
571
579
  const explicit = readEnv(env, 'PRICING_RATE_ENDPOINT');
572
580
  if (explicit && explicit.trim()) return explicit.trim();
573
- return deriveFromHub(env, 'works/talents/me/work-details/pricing-rate');
581
+ return deriveFromHub(env, 'works/talent/me/work-details/pricing-rate');
574
582
  }
575
583
 
576
584
  function getSetAvailabilityEndpoint(env = process.env) {
577
585
  const explicit = readEnv(env, 'AVAILABILITY_ENDPOINT');
578
586
  if (explicit && explicit.trim()) return explicit.trim();
579
- return deriveFromHub(env, 'works/talents/me/work-details/availability');
587
+ return deriveFromHub(env, 'works/talent/me/work-details/availability');
580
588
  }
581
589
 
582
590
  function getLanguagesEndpoint(env = process.env) {
583
591
  const explicit = readEnv(env, 'LANGUAGES_ENDPOINT');
584
592
  if (explicit && explicit.trim()) return explicit.trim();
585
- return deriveFromHub(env, 'works/talents/me/work-details/languages');
593
+ return deriveFromHub(env, 'works/talent/me/work-details/languages');
586
594
  }
587
595
 
588
596
  function getLanguagesCatalogEndpoint(env = process.env) {
@@ -613,10 +621,18 @@ function getTranslationsEndpoint(env = process.env) {
613
621
  return deriveFromHub(env, 'translations');
614
622
  }
615
623
 
624
+ // The signed-in user as hub sees it (GET only): read back the phone after saving it.
625
+ function getUserMeEndpoint(env = process.env) {
626
+ const explicit = readEnv(env, 'USER_ME_ENDPOINT');
627
+ if (explicit && explicit.trim()) return explicit.trim();
628
+ return deriveFromHub(env, 'works/users/me');
629
+ }
630
+
631
+ // Talent base fields (PATCH name, phone...). works/users/me is GET only.
616
632
  function getTalentMeEndpoint(env = process.env) {
617
633
  const explicit = readEnv(env, 'TALENT_ME_ENDPOINT');
618
634
  if (explicit && explicit.trim()) return explicit.trim();
619
- return deriveFromHub(env, 'works/me');
635
+ return deriveFromHub(env, 'works/talent/me');
620
636
  }
621
637
 
622
638
  function getOnboardingInterviewsEndpoint(env = process.env) {
@@ -758,6 +774,7 @@ module.exports = {
758
774
  getAssignedClustersEndpoint,
759
775
  getTemplatesByDimensionEndpoint,
760
776
  getMyAssessmentsEndpoint,
777
+ getCertificationInterviewsEndpoint,
761
778
  // Find Positions list (`find-projects` command / `find_projects` MCP tool).
762
779
  getFindPositionsEndpoint,
763
780
  // Marketplace (show-project / save-project / unsave-project / invitations).
@@ -810,6 +827,7 @@ module.exports = {
810
827
  getMePortfoliosEndpoint,
811
828
  getTranslationsEndpoint,
812
829
  getTalentMeEndpoint,
830
+ getUserMeEndpoint,
813
831
  getOnboardingInterviewsEndpoint,
814
832
  getCompleteOnboardingEndpoint,
815
833
  getOnboardingRestartEndpoint,
@@ -1,6 +1,6 @@
1
1
  'use strict';
2
2
 
3
- // Find Positions list client (hub `works/positions/find`, talent Bearer). Each
3
+ // Find Positions list client. Each
4
4
  // item carries the parent Project's id/name — a "project" here is the Position's
5
5
  // parent, not a separate list. Pagination (limit/offset) and filters map to the
6
6
  // endpoint's `criteria`; the server orders by match desc and returns the total.
package/src/i18n.js CHANGED
@@ -355,8 +355,10 @@ const catalogs = {
355
355
  subagents: (n) => `${n} ${n === 1 ? 'run' : 'runs'} de subagente`,
356
356
  hours: (h) => `${h} h activas (gap idle > 25 min = pausa)`,
357
357
  commits: (authored, total) => `${authored}/${total} commits tuyos`,
358
- lines: (added, deleted) => `+${added}/-${deleted} líneas`,
359
- velocity: (v) => `${v} commits/día`,
358
+ lines: (added, deleted) => `+${added}/-${deleted} líneas (del repo)`,
359
+ velocity: (v) => `${v} commits/día (del repo)`,
360
+ authorshipMismatch: (emails, account) =>
361
+ `Tus commits en ${emails.length === 1 ? 'este repo' : 'estos repos'} están a nombre de ${emails.join(', ')}, que no es tu email de Shakers (${account}). Puede que tu trabajo no se te atribuya: configura git con tu email de Shakers (git config user.email "${account}") para que cuente.`,
360
362
  workStreams: (streams, multiDay, maxSpan) =>
361
363
  `${streams} ${streams === 1 ? 'workstream' : 'workstreams'} (${multiDay} multi-día, máx ${maxSpan}d)`,
362
364
  tractionLabel: 'Actividad reciente',
@@ -531,7 +533,7 @@ const catalogs = {
531
533
  error: 'No se pudo generar el informe.',
532
534
  },
533
535
  frameworkIntroUsage:
534
- 'Esto mide tu fluidez con IA en dos ejes: Setup (madurez de tu tooling, T0–T7, determinista) y Usage (cómo lo usas, evaluado en entrevista + código). Te sitúa en el mapa 3×3 (Explorer → AI Native). Completarlo posiciona tu perfil agéntico para los proyectos que valoran cómo trabajas con IA.',
536
+ 'Esto mide tu fluidez con IA en dos ejes: Setup (madurez de tu tooling, T0–T7, determinista) y Usage (cómo lo usas, evaluado en entrevista + código). Te sitúa en el mapa 3×3 (Explorer → AI Native). Completarlo posiciona tu perfil agéntico para los proyectos que valoran cómo trabajas con IA. Por defecto leo el uso de IA de todo este equipo y asumo que es tuyo (un equipo = tú).',
535
537
  // Terminal progress feedback (talents-ai-score): stderr-only status
536
538
  // during the two slow phases (see src/terminal-progress.js).
537
539
  scanningLabel: 'Escaneando entorno y detectores…',
@@ -759,39 +761,9 @@ const catalogs = {
759
761
  dimensionInvalid: 'Esa dimensión no está entre las certificables ahora mismo.',
760
762
  dimensionOnCooldown: (slug, day) => `Aún no puedes repetir ${slug}: estará disponible el ${day}.`,
761
763
  dimensionUsing: (slug) => `Dimensión elegida: ${slug}`,
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.',
764
+ webHandoff: 'Esta certificación se hace en la web. Abre este enlace para empezar la entrevista:',
765
+ webHandoffLink: (url) => ` ${url}`,
766
+ webHandoffNoLink: 'No se pudo generar el enlace. Entra en tu perfil de Shakers y abre la sección de certificaciones.',
795
767
  },
796
768
  findProjects: {
797
769
  help:
@@ -1244,7 +1216,7 @@ const catalogs = {
1244
1216
  // Branded mini-shell chrome (skill-code-certification / ADR-014).
1245
1217
  login: {
1246
1218
  intro: 'Inicia sesión con tu email y contraseña de Shakers.',
1247
- sessionExpiredIntro: 'Tu sesión anterior caducó. Vuelve a iniciar sesión con tu email y contraseña.',
1219
+ sessionExpiredIntro: 'Tu sesión anterior caducó. Vuelve a iniciar sesión.',
1248
1220
  alreadyLoggedIn: 'Ya has iniciado sesión. Usa `logout` si quieres cambiar de cuenta.',
1249
1221
  emailPrompt: 'Email:',
1250
1222
  passwordPrompt: 'Contraseña:',
@@ -1274,16 +1246,19 @@ const catalogs = {
1274
1246
  googleErrorState: 'La respuesta del navegador no coincidió con lo esperado (posible interferencia). Por seguridad no se inició sesión; inténtalo de nuevo.',
1275
1247
  googleErrorDenied: 'Cancelaste el acceso en Google (o no se concedió). No se inició sesión.',
1276
1248
  googleErrorGeneric: 'No se pudo iniciar sesión con Google. Inténtalo de nuevo.',
1277
- // Google por device flow (RFC 8628): la config de Google la tiene el Hub; abres una URL y escribes un código.
1249
+ // 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.
1278
1250
  deviceIntro: (name) => `Inicia sesión con ${name}. Abre el enlace de abajo e introduce el código para autorizar; luego vuelve aquí.`,
1251
+ // 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.
1252
+ 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í.',
1279
1253
  deviceVisit: 'Abre esta página en tu navegador:',
1280
1254
  deviceCodeLabel: (code) => `e introduce este código: ${code}`,
1281
1255
  deviceWaiting: 'Esperando a que autorices en el navegador…',
1282
- deviceErrorExpired: 'La solicitud de acceso con Google caducó antes de autorizarse. Vuelve a ejecutar `login --google` cuando estés listo.',
1283
- deviceErrorDenied: 'Se canceló el acceso con Google (o no se concedió). No se inició sesión.',
1284
- deviceErrorGeneric: 'No se pudo iniciar sesión con Google. Inténtalo de nuevo.',
1256
+ 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.',
1257
+ deviceErrorExpired: 'La solicitud de acceso caducó antes de autorizarse. Vuelve a ejecutar `login` cuando estés listo.',
1258
+ deviceErrorDenied: 'Se canceló el acceso (o no se concedió). No se inició sesión.',
1259
+ deviceErrorGeneric: 'No se pudo iniciar sesión. Inténtalo de nuevo.',
1285
1260
  chooseMethodHeading: '¿Cómo quieres iniciar sesión?',
1286
- methodEmail: 'Email y contraseña',
1261
+ methodEmail: 'Email',
1287
1262
  methodGoogle: 'Google',
1288
1263
  methodLinkedin: 'LinkedIn',
1289
1264
  selectHint: 'Flechas ↑/↓ para moverte · enter para elegir · esc para cancelar',
@@ -2028,8 +2003,10 @@ const catalogs = {
2028
2003
  subagents: (n) => `${n} subagent ${n === 1 ? 'run' : 'runs'}`,
2029
2004
  hours: (h) => `${h} active h (idle gap > 25 min = pause)`,
2030
2005
  commits: (authored, total) => `${authored}/${total} commits by you`,
2031
- lines: (added, deleted) => `+${added}/-${deleted} lines`,
2032
- velocity: (v) => `${v} commits/day`,
2006
+ lines: (added, deleted) => `+${added}/-${deleted} lines (repo-wide)`,
2007
+ velocity: (v) => `${v} commits/day (repo-wide)`,
2008
+ authorshipMismatch: (emails, account) =>
2009
+ `Your commits in ${emails.length === 1 ? 'this repo' : 'these repos'} are under ${emails.join(', ')}, which is not your Shakers email (${account}). Your work may not be attributed to you: set git to your Shakers email (git config user.email "${account}") so it counts.`,
2033
2010
  workStreams: (streams, multiDay, maxSpan) =>
2034
2011
  `${streams} ${streams === 1 ? 'workstream' : 'workstreams'} (${multiDay} multi-day, max ${maxSpan}d)`,
2035
2012
  tractionLabel: 'Traction',
@@ -2200,7 +2177,7 @@ const catalogs = {
2200
2177
  error: 'Could not generate the report.',
2201
2178
  },
2202
2179
  frameworkIntroUsage:
2203
- 'This measures your AI fluency across two axes: Setup (your tooling\'s maturity, T0–T7, deterministic) and Usage (how you use it, assessed via interview + code). It places you on the 3×3 map (Explorer → AI Native). Completing it positions your agentic profile for projects that value how you work with AI.',
2180
+ 'This measures your AI fluency across two axes: Setup (your tooling\'s maturity, T0–T7, deterministic) and Usage (how you use it, assessed via interview + code). It places you on the 3×3 map (Explorer → AI Native). Completing it positions your agentic profile for projects that value how you work with AI. By default it reads the AI usage across this whole machine and assumes it is yours (one machine = you).',
2204
2181
  scanningLabel: 'Scanning environment and detectors…',
2205
2182
  // Loader for the per-selected-repo aggregation phase (git + sessions + steering/decisions + evidence + agent org chart).
2206
2183
  aggregatingLabel: 'Aggregating signals across the selected repos…',
@@ -2421,39 +2398,9 @@ const catalogs = {
2421
2398
  dimensionInvalid: 'That dimension is not among the certifiable ones right now.',
2422
2399
  dimensionOnCooldown: (slug, day) => `You can't retake ${slug} yet: it will be available on ${day}.`,
2423
2400
  dimensionUsing: (slug) => `Chosen dimension: ${slug}`,
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.',
2401
+ webHandoff: 'This certification is done on the web. Open this link to start the interview:',
2402
+ webHandoffLink: (url) => ` ${url}`,
2403
+ webHandoffNoLink: 'Could not build the link. Go to your Shakers profile and open the certifications section.',
2457
2404
  },
2458
2405
  findProjects: {
2459
2406
  help:
@@ -2902,7 +2849,7 @@ const catalogs = {
2902
2849
  // Branded mini-shell chrome (skill-code-certification / ADR-014).
2903
2850
  login: {
2904
2851
  intro: 'Log in with your Shakers email and password.',
2905
- sessionExpiredIntro: 'Your previous session expired. Log in again with your email and password.',
2852
+ sessionExpiredIntro: 'Your previous session expired. Log in again.',
2906
2853
  alreadyLoggedIn: 'You are already logged in. Use `logout` if you want to switch accounts.',
2907
2854
  emailPrompt: 'Email:',
2908
2855
  passwordPrompt: 'Password:',
@@ -2932,17 +2879,20 @@ const catalogs = {
2932
2879
  googleErrorState: 'The browser response did not match what was expected (possible interference). For safety no session was started; try again.',
2933
2880
  googleErrorDenied: 'You cancelled access in Google (or it was not granted). No session was started.',
2934
2881
  googleErrorGeneric: 'Could not log in with Google. Try again.',
2935
- // Google via device flow (RFC 8628): the Hub owns Google config; you open a URL and type a code.
2882
+ // 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.
2936
2883
  deviceIntro: (name) => `Sign in with ${name}. Open the link below and enter the code to authorize; then come back here.`,
2884
+ // 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.
2885
+ 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.',
2937
2886
  deviceVisit: 'Open this page in your browser:',
2938
2887
  deviceCodeLabel: (code) => `and enter this code: ${code}`,
2939
2888
  deviceWaiting: 'Waiting for you to authorize in the browser…',
2940
- deviceErrorExpired: 'The Google sign-in request expired before it was authorized. Run `login --google` again when you are ready.',
2941
- deviceErrorDenied: 'The Google sign-in was cancelled or not granted. No session was started.',
2942
- deviceErrorGeneric: 'Could not sign in with Google. Try again.',
2889
+ 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.',
2890
+ deviceErrorExpired: 'The sign-in request expired before it was authorized. Run `login` again when you are ready.',
2891
+ deviceErrorDenied: 'The sign-in was cancelled or not granted. No session was started.',
2892
+ deviceErrorGeneric: 'Could not sign in. Try again.',
2943
2893
  // 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).
2944
2894
  chooseMethodHeading: 'How do you want to log in?',
2945
- methodEmail: 'Email and password',
2895
+ methodEmail: 'Email',
2946
2896
  methodGoogle: 'Google',
2947
2897
  methodLinkedin: 'LinkedIn',
2948
2898
  selectHint: 'Arrows ↑/↓ to move · enter to pick · esc to cancel',
@@ -1,6 +1,6 @@
1
1
  'use strict';
2
2
 
3
- // Languages client. VIEW = GET `works/talents/me/work-details/languages`; the
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
 
@@ -1,6 +1,7 @@
1
1
  'use strict';
2
2
 
3
3
  const http = require('http');
4
+ const crypto = require('crypto');
4
5
  const { openPath } = require('./open-file');
5
6
  const flow = require('./onboarding-flow');
6
7
  const { methodIdsForFlow } = require('./auth-methods');
@@ -184,7 +185,7 @@ function loginFormHtml(lang, { error } = {}) {
184
185
  const c = copy(lang);
185
186
  return page(`Shakers · ${c.signin}`, {
186
187
  inner: `<h1>${c.signin}</h1>${errorBanner(errorText(c, error))}`
187
- + `<form method="post" action="/submit">`
188
+ + `<form method="post" action="submit">`
188
189
  + `<label>${c.email}</label><input name="email" type="email" required autofocus>`
189
190
  + `<label>${c.password}</label><input name="password" type="password" required>`
190
191
  + `<button type="submit">${c.signin}</button>`
@@ -205,7 +206,7 @@ function registerFormHtml(lang, { error, missing } = {}) {
205
206
  return page(`Shakers · ${c.create}`, {
206
207
  banner: panel(c.welcome),
207
208
  inner: `<h1>${c.create}</h1>${errorBanner(errorText(c, error))}`
208
- + `<form method="post" action="/submit">`
209
+ + `<form method="post" action="submit">`
209
210
  + missingIdentityInputs(c, missing).replace('required', 'required autofocus')
210
211
  + `<label>${c.password}</label><input name="password" type="password" required minlength="8"${missing && (missing.email || missing.name || missing.lastName) ? '' : ' autofocus'}>`
211
212
  + `<label>${c.repeat}</label><input name="passwordConfirm" type="password" required minlength="8">`
@@ -250,9 +251,9 @@ function methodChoiceHtml(mode, lang, methods) {
250
251
  const emailLabel = mode === 'register' ? c.continueEmailRegister : c.continueEmail;
251
252
  const register = mode === 'register';
252
253
  const buttons = WINDOW_METHODS[mode].filter((id) => methods.includes(id)).map((id) => {
253
- if (id === 'google') return `<a class="mbtn primary" href="/google${register ? '?legal=1' : ''}">${GOOGLE_G}${c.continueGoogle}</a>`;
254
- if (id === 'linkedin') return `<a class="mbtn secondary" href="/linkedin">${LINKEDIN_IN}${c.continueLinkedin}</a>`;
255
- return `<a class="mbtn secondary" href="/email">${emailLabel}</a>`;
254
+ if (id === 'google') return `<a class="mbtn primary" href="google${register ? '?legal=1' : ''}">${GOOGLE_G}${c.continueGoogle}</a>`;
255
+ if (id === 'linkedin') return `<a class="mbtn secondary" href="linkedin">${LINKEDIN_IN}${c.continueLinkedin}</a>`;
256
+ return `<a class="mbtn secondary" href="email">${emailLabel}</a>`;
256
257
  }).join('');
257
258
  return page(`Shakers · ${title}`, {
258
259
  inner: `<h1>${title}</h1><div class="actions">${buttons}</div><p class="hint">${c.hint}</p>${register ? legalHtml(lang) : ''}`,
@@ -264,12 +265,12 @@ function existingAccountHtml(lang, { email, error } = {}) {
264
265
  const c = copy(lang);
265
266
  return page(`Shakers · ${c.signin}`, {
266
267
  inner: `<h1>${c.signin}</h1>${errorBanner(error ? errorText(c, error) : c.existing)}`
267
- + `<form method="post" action="/submit"><input type="hidden" name="intent" value="login">`
268
+ + `<form method="post" action="submit"><input type="hidden" name="intent" value="login">`
268
269
  + `<label>${c.email}</label><input name="email" type="email" required value="${esc(email || '')}">`
269
270
  + `<label>${c.password}</label><input name="password" type="password" required autofocus>`
270
271
  + `<button type="submit">${c.signin}</button>`
271
272
  + `</form>`
272
- + `<div class="actions"><a class="mbtn secondary" href="/google?legal=1">${GOOGLE_G}${c.continueGoogle}</a></div>`
273
+ + `<div class="actions"><a class="mbtn secondary" href="google?legal=1">${GOOGLE_G}${c.continueGoogle}</a></div>`
273
274
  + `<p class="hint">${c.hint}</p>`,
274
275
  });
275
276
  }
@@ -371,7 +372,25 @@ function runLoopbackAuth(
371
372
  resolve(result);
372
373
  };
373
374
 
375
+ // Any page in the talent's browser can reach a loopback port, and a rebound DNS name can read it: only this
376
+ // window's own host, its own origin and its unguessable path get an answer. Every link in the pages is relative.
377
+ const secretPath = `/${crypto.randomBytes(32).toString('base64url')}`;
378
+ let port = null;
379
+ const ownHosts = () => [`127.0.0.1:${port}`, `localhost:${port}`];
380
+ const forbidden = (req) => {
381
+ if (!ownHosts().includes(String(req.headers.host || '').toLowerCase())) return true;
382
+ if (req.method === 'GET') return false;
383
+ const origin = req.headers.origin;
384
+ if (origin !== undefined && !ownHosts().map((h) => `http://${h}`).includes(origin)) return true;
385
+ const site = req.headers['sec-fetch-site'];
386
+ return site !== undefined && site !== 'same-origin';
387
+ };
388
+
374
389
  const server = http.createServer(async (req, res) => {
390
+ if (forbidden(req)) {
391
+ res.writeHead(403).end();
392
+ return;
393
+ }
375
394
  let url;
376
395
  try {
377
396
  url = new URL(req.url, 'http://127.0.0.1');
@@ -379,7 +398,17 @@ function runLoopbackAuth(
379
398
  res.writeHead(400).end();
380
399
  return;
381
400
  }
382
- if (req.method === 'GET' && (url.pathname === '/' || url.pathname === '')) {
401
+ if (url.pathname === secretPath) {
402
+ res.writeHead(302, { Location: `${secretPath}/${url.search}` });
403
+ res.end();
404
+ return;
405
+ }
406
+ if (!url.pathname.startsWith(`${secretPath}/`)) {
407
+ res.writeHead(404).end();
408
+ return;
409
+ }
410
+ url.pathname = url.pathname.slice(secretPath.length);
411
+ if (req.method === 'GET' && url.pathname === '/') {
383
412
  res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
384
413
  res.end(showMethodScreen ? methodChoiceHtml(mode, lang, methods) : formHtml(mode, lang, formOpts));
385
414
  return;
@@ -440,8 +469,8 @@ function runLoopbackAuth(
440
469
  if (timer.unref) timer.unref();
441
470
  server.on('error', () => finish({ ok: false, reason: 'local-error' }));
442
471
  server.listen(0, '127.0.0.1', () => {
443
- const { port } = server.address();
444
- const formUrl = `http://127.0.0.1:${port}/`;
472
+ ({ port } = server.address());
473
+ const formUrl = `http://127.0.0.1:${port}${secretPath}/`;
445
474
  if (typeof onUrl === 'function') onUrl(formUrl);
446
475
  try { openBrowser(formUrl); } catch { /* headless: the onUrl fallback covers it */ }
447
476
  });
package/src/mcp-choice.js CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
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 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.';
5
+ const SAY_MESSAGE = 'Print `say` word for word as your whole reply: it already holds the texts, the question and its options as a list. Present them as a choice and pass back the exact label the talent picks, never a number; add nothing of your own and call no tool until the talent answers.';
6
6
 
7
7
  // Fixed texts that are not a question ride on the next question block: a model skips a standalone relay.
8
8
  const notices = [];
@@ -17,14 +17,14 @@ function takeNotices() {
17
17
 
18
18
  // Fixed texts travel with their question, so a model that skips "show this first" still shows them.
19
19
  function sayBlock(texts, question, options) {
20
- return [...texts, question, options.map((o, i) => `${i + 1}. ${o}`).join('\n')].join('\n\n');
20
+ return [...texts, question, options.map((o) => `- ${o}`).join('\n')].join('\n\n');
21
21
  }
22
22
 
23
23
  function chatChoice(question, options, texts = []) {
24
24
  return { say: sayBlock([...takeNotices(), ...texts], question, options), options, choiceMessage: SAY_MESSAGE };
25
25
  }
26
26
 
27
- // The option an answer names, by its text or by its number in the printed list.
27
+ // The option an answer names: by its label, or by a position number if the talent still types one.
28
28
  function optionFor(answer, options) {
29
29
  if (typeof answer !== 'string') return null;
30
30
  const text = answer.trim();
@@ -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 social provider. Defaults to 'google'. Same device flow either way; it selects which social button the sign-in page opens." },
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 social sign-in (or registration) via the device flow — provider 'google' (default) or 'linkedin'. 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, ... }.",
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
  },