shakers-cli-beta 0.11.0 → 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/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/config.js +31 -22
- package/src/find-projects-client.js +1 -1
- package/src/i18n.js +24 -78
- package/src/lang-client.js +1 -1
- package/src/mcp-device-login-tools.js +3 -3
- 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/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/
|
|
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/
|
|
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/
|
|
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/
|
|
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/
|
|
593
|
+
return deriveFromHub(env, 'works/talent/me/work-details/languages');
|
|
586
594
|
}
|
|
587
595
|
|
|
588
596
|
function getLanguagesCatalogEndpoint(env = process.env) {
|
|
@@ -616,7 +624,7 @@ function getTranslationsEndpoint(env = process.env) {
|
|
|
616
624
|
function getTalentMeEndpoint(env = process.env) {
|
|
617
625
|
const explicit = readEnv(env, 'TALENT_ME_ENDPOINT');
|
|
618
626
|
if (explicit && explicit.trim()) return explicit.trim();
|
|
619
|
-
return deriveFromHub(env, 'works/me');
|
|
627
|
+
return deriveFromHub(env, 'works/users/me');
|
|
620
628
|
}
|
|
621
629
|
|
|
622
630
|
function getOnboardingInterviewsEndpoint(env = process.env) {
|
|
@@ -758,6 +766,7 @@ module.exports = {
|
|
|
758
766
|
getAssignedClustersEndpoint,
|
|
759
767
|
getTemplatesByDimensionEndpoint,
|
|
760
768
|
getMyAssessmentsEndpoint,
|
|
769
|
+
getCertificationInterviewsEndpoint,
|
|
761
770
|
// Find Positions list (`find-projects` command / `find_projects` MCP tool).
|
|
762
771
|
getFindPositionsEndpoint,
|
|
763
772
|
// Marketplace (show-project / save-project / unsave-project / invitations).
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
// Find Positions list client
|
|
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
|
@@ -759,39 +759,9 @@ const catalogs = {
|
|
|
759
759
|
dimensionInvalid: 'Esa dimensión no está entre las certificables ahora mismo.',
|
|
760
760
|
dimensionOnCooldown: (slug, day) => `Aún no puedes repetir ${slug}: estará disponible el ${day}.`,
|
|
761
761
|
dimensionUsing: (slug) => `Dimensión elegida: ${slug}`,
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
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.',
|
|
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.',
|
|
795
765
|
},
|
|
796
766
|
findProjects: {
|
|
797
767
|
help:
|
|
@@ -1244,7 +1214,7 @@ const catalogs = {
|
|
|
1244
1214
|
// Branded mini-shell chrome (skill-code-certification / ADR-014).
|
|
1245
1215
|
login: {
|
|
1246
1216
|
intro: 'Inicia sesión con tu email y contraseña de Shakers.',
|
|
1247
|
-
sessionExpiredIntro: 'Tu sesión anterior caducó. Vuelve a iniciar sesión
|
|
1217
|
+
sessionExpiredIntro: 'Tu sesión anterior caducó. Vuelve a iniciar sesión.',
|
|
1248
1218
|
alreadyLoggedIn: 'Ya has iniciado sesión. Usa `logout` si quieres cambiar de cuenta.',
|
|
1249
1219
|
emailPrompt: 'Email:',
|
|
1250
1220
|
passwordPrompt: 'Contraseña:',
|
|
@@ -1274,16 +1244,19 @@ const catalogs = {
|
|
|
1274
1244
|
googleErrorState: 'La respuesta del navegador no coincidió con lo esperado (posible interferencia). Por seguridad no se inició sesión; inténtalo de nuevo.',
|
|
1275
1245
|
googleErrorDenied: 'Cancelaste el acceso en Google (o no se concedió). No se inició sesión.',
|
|
1276
1246
|
googleErrorGeneric: 'No se pudo iniciar sesión con Google. Inténtalo de nuevo.',
|
|
1277
|
-
//
|
|
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.
|
|
1278
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í.',
|
|
1279
1251
|
deviceVisit: 'Abre esta página en tu navegador:',
|
|
1280
1252
|
deviceCodeLabel: (code) => `e introduce este código: ${code}`,
|
|
1281
1253
|
deviceWaiting: 'Esperando a que autorices en el navegador…',
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
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.',
|
|
1285
1258
|
chooseMethodHeading: '¿Cómo quieres iniciar sesión?',
|
|
1286
|
-
methodEmail: 'Email
|
|
1259
|
+
methodEmail: 'Email',
|
|
1287
1260
|
methodGoogle: 'Google',
|
|
1288
1261
|
methodLinkedin: 'LinkedIn',
|
|
1289
1262
|
selectHint: 'Flechas ↑/↓ para moverte · enter para elegir · esc para cancelar',
|
|
@@ -2421,39 +2394,9 @@ const catalogs = {
|
|
|
2421
2394
|
dimensionInvalid: 'That dimension is not among the certifiable ones right now.',
|
|
2422
2395
|
dimensionOnCooldown: (slug, day) => `You can't retake ${slug} yet: it will be available on ${day}.`,
|
|
2423
2396
|
dimensionUsing: (slug) => `Chosen dimension: ${slug}`,
|
|
2424
|
-
|
|
2425
|
-
|
|
2426
|
-
|
|
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.',
|
|
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.',
|
|
2457
2400
|
},
|
|
2458
2401
|
findProjects: {
|
|
2459
2402
|
help:
|
|
@@ -2902,7 +2845,7 @@ const catalogs = {
|
|
|
2902
2845
|
// Branded mini-shell chrome (skill-code-certification / ADR-014).
|
|
2903
2846
|
login: {
|
|
2904
2847
|
intro: 'Log in with your Shakers email and password.',
|
|
2905
|
-
sessionExpiredIntro: 'Your previous session expired. Log in again
|
|
2848
|
+
sessionExpiredIntro: 'Your previous session expired. Log in again.',
|
|
2906
2849
|
alreadyLoggedIn: 'You are already logged in. Use `logout` if you want to switch accounts.',
|
|
2907
2850
|
emailPrompt: 'Email:',
|
|
2908
2851
|
passwordPrompt: 'Password:',
|
|
@@ -2932,17 +2875,20 @@ const catalogs = {
|
|
|
2932
2875
|
googleErrorState: 'The browser response did not match what was expected (possible interference). For safety no session was started; try again.',
|
|
2933
2876
|
googleErrorDenied: 'You cancelled access in Google (or it was not granted). No session was started.',
|
|
2934
2877
|
googleErrorGeneric: 'Could not log in with Google. Try again.',
|
|
2935
|
-
//
|
|
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.
|
|
2936
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.',
|
|
2937
2882
|
deviceVisit: 'Open this page in your browser:',
|
|
2938
2883
|
deviceCodeLabel: (code) => `and enter this code: ${code}`,
|
|
2939
2884
|
deviceWaiting: 'Waiting for you to authorize in the browser…',
|
|
2940
|
-
|
|
2941
|
-
|
|
2942
|
-
|
|
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.',
|
|
2943
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).
|
|
2944
2890
|
chooseMethodHeading: 'How do you want to log in?',
|
|
2945
|
-
methodEmail: 'Email
|
|
2891
|
+
methodEmail: 'Email',
|
|
2946
2892
|
methodGoogle: 'Google',
|
|
2947
2893
|
methodLinkedin: 'LinkedIn',
|
|
2948
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-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 : [];
|