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/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/authorship-mismatch.js +62 -0
- 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 +40 -22
- package/src/find-projects-client.js +1 -1
- package/src/i18n.js +34 -84
- package/src/lang-client.js +1 -1
- package/src/mcp-auth-form.js +39 -10
- package/src/mcp-choice.js +3 -3
- package/src/mcp-device-login-tools.js +3 -3
- package/src/mcp-instructions.js +7 -7
- package/src/mcp-onboarding-tools.js +10 -16
- package/src/mcp-prompts.js +1 -1
- package/src/mcp-register-tools.js +1 -1
- package/src/mcp-role-tools.js +26 -10
- package/src/mcp-server.js +9 -2
- package/src/mcp-signup-client.js +1 -1
- package/src/mcp-signup-tools.js +142 -50
- package/src/profile-client.js +12 -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 +59 -33
- package/src/terminal-agents.js +8 -0
- package/src/usage-run-steps.js +3 -0
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) {
|
|
@@ -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
|
|
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
|
-
|
|
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.',
|
|
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
|
|
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
|
-
//
|
|
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
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
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
|
|
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
|
-
|
|
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.',
|
|
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
|
|
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
|
-
//
|
|
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
|
-
|
|
2941
|
-
|
|
2942
|
-
|
|
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
|
|
2895
|
+
methodEmail: 'Email',
|
|
2946
2896
|
methodGoogle: 'Google',
|
|
2947
2897
|
methodLinkedin: 'LinkedIn',
|
|
2948
2898
|
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
|
|
package/src/mcp-auth-form.js
CHANGED
|
@@ -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="
|
|
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="
|
|
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="
|
|
254
|
-
if (id === 'linkedin') return `<a class="mbtn secondary" href="
|
|
255
|
-
return `<a class="mbtn secondary" href="
|
|
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="
|
|
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="
|
|
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 (
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
},
|