shakers-cli 0.18.0 → 0.19.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/add-agent.js +12 -6
- package/bin/ai-usage.js +2 -1
- package/bin/apply.js +66 -0
- package/bin/mcp.js +5 -2
- package/bin/shakers-cli.js +7 -3
- package/package.json +1 -1
- package/src/apply-client.js +164 -0
- package/src/apply-flow.js +105 -0
- package/src/command-audit.js +13 -3
- package/src/config.js +15 -0
- package/src/detected-agents.js +40 -0
- package/src/i18n.js +98 -2
- package/src/mcp-ai-usage-tool.js +1 -1
- package/src/mcp-apply-tools.js +152 -0
- package/src/mcp-onboarding-tools.js +89 -0
- package/src/signup-copy.js +127 -1
- package/src/start-add-agents.js +106 -3
- package/src/usage-run-steps.js +4 -1
package/src/i18n.js
CHANGED
|
@@ -823,6 +823,42 @@ const catalogs = {
|
|
|
823
823
|
faqsHeading: 'Preguntas frecuentes',
|
|
824
824
|
done: 'Detalle completado.',
|
|
825
825
|
},
|
|
826
|
+
apply: {
|
|
827
|
+
help:
|
|
828
|
+
'apply — aplica a un proyecto/posición con tu mensaje y tus respuestas a sus preguntas.\n\n'
|
|
829
|
+
+ 'Uso:\n apply <número|id> [--lang es|en]\n\n'
|
|
830
|
+
+ 'Requiere una sesión activa (shakers login) y una terminal interactiva.\n',
|
|
831
|
+
loginRequired: '"apply" requiere una sesión activa.\n Falta: iniciar sesión. Ejecuta primero: shakers login',
|
|
832
|
+
needsInteractive: 'Aplicar necesita una terminal interactiva: te pido tu mensaje y tus respuestas, y que confirmes antes de enviar. Desde un asistente de IA, usa la herramienta MCP apply_to_project.',
|
|
833
|
+
idRequired: 'Falta el proyecto. Uso: apply <número|id>.',
|
|
834
|
+
refNoCache: 'No hay un listado reciente. Ejecuta primero shakers find-projects y usa el número que aparece (o pasa un id).',
|
|
835
|
+
refOutOfRange: (count) => `Ese número no está en el último listado (hay ${count}). Ejecuta shakers find-projects otra vez.`,
|
|
836
|
+
loading: 'Cargando las preguntas de la candidatura…',
|
|
837
|
+
fetchFailed: (reason) => `No se pudieron obtener las preguntas (${reason}). Inténtalo de nuevo en un momento.`,
|
|
838
|
+
notFound: 'No se encontró ese proyecto/posición.',
|
|
839
|
+
notOpen: 'Este proyecto no admite tu candidatura ahora mismo: ya aplicaste, está cerrado o no está disponible para ti.',
|
|
840
|
+
untitled: 'Sin título',
|
|
841
|
+
companyLine: (name) => `Empresa: ${name}`,
|
|
842
|
+
mainMotivationFallback: '¿Cuál es tu principal motivación para aplicar?',
|
|
843
|
+
askMessage: (question) => `${question} (tu mensaje para el equipo; enter para dejarlo en blanco):`,
|
|
844
|
+
askOpen: (question) => `${question} (enter para no responder):`,
|
|
845
|
+
choiceHint: 'Flechas ↑/↓ para moverte · enter para elegir',
|
|
846
|
+
skipChoice: 'No responder',
|
|
847
|
+
summaryTitle: 'Esto es lo que se enviará',
|
|
848
|
+
titleLine: (title) => `Proyecto: ${title}`,
|
|
849
|
+
messageLine: (message) => `Mensaje: ${message}`,
|
|
850
|
+
empty: '(vacío)',
|
|
851
|
+
answerLine: (question, answer) => `${question}: ${answer}`,
|
|
852
|
+
sendNotice: (title) => `Se envían tu mensaje y tus respuestas para «${title}»: el cliente verá tu perfil de Shakers y se abre un chat con el equipo. Tu perfil no cambia.`,
|
|
853
|
+
reviewNotice: 'En este proyecto Shakers revisa tu candidatura antes de que la vea el cliente.',
|
|
854
|
+
confirm: '¿Enviar la candidatura? (s/n):',
|
|
855
|
+
cancelled: 'Cancelado. No se ha enviado nada.',
|
|
856
|
+
sending: 'Enviando tu candidatura…',
|
|
857
|
+
sent: 'Candidatura enviada. Puedes seguirla con shakers applications.',
|
|
858
|
+
sentPending: 'Candidatura enviada. Shakers la revisa antes de que la vea el cliente; puedes seguirla con shakers applications.',
|
|
859
|
+
alreadyApplied: (status) => (status ? `Ya aplicaste a este proyecto (estado: ${status}).` : 'Ya aplicaste a este proyecto.'),
|
|
860
|
+
failed: (reason) => `No se pudo enviar la candidatura (${reason}). Inténtalo de nuevo en un momento.`,
|
|
861
|
+
},
|
|
826
862
|
savedPositions: {
|
|
827
863
|
helpSave: 'save-project — guarda un proyecto/posición en tus guardados.\n\nUso:\n save-project <número|id> [--json]\n\nRequiere una sesión activa (shakers login).',
|
|
828
864
|
helpUnsave: 'unsave-project — quita un proyecto/posición de tus guardados.\n\nUso:\n unsave-project <número|id> [--json]\n\nRequiere una sesión activa (shakers login).',
|
|
@@ -1228,6 +1264,7 @@ const catalogs = {
|
|
|
1228
1264
|
},
|
|
1229
1265
|
},
|
|
1230
1266
|
trust: {
|
|
1267
|
+
detectedAgents: 'Si confirmas, añado a tu perfil de Shakers los agentes marcados, solo su nombre y su descripción, y los clientes los verán en tu perfil. No leo ni subo nada más de este ordenador. Puedes quitarlos cuando quieras desde tu perfil en la web de Shakers.',
|
|
1231
1268
|
uploadWarning: 'Antes de aprobar: se enviarían hasta 40 extractos literales de tus mensajes (correcciones y decisiones) y 20 comandos, de 200 caracteres como máximo cada uno, la configuración y la definición de tus agentes y recuentos de actividad. Se quita lo que parece un secreto, un email o una ruta, pero el filtro es aproximado: no lo apruebes si hay datos que no puedes compartir (de clientes, de tu empresa o de terceros).',
|
|
1232
1269
|
cli: {
|
|
1233
1270
|
'ai-usage': { before: 'Voy a analizar en este ordenador tus herramientas de IA, el historial de tus proyectos de código y tus conversaciones con asistentes de programación con IA (Claude Code, Codex, Gemini, Cursor, Continue, Cline/Roo, Aider, Pi, OpenCode). Si analizas un repo, solo cubre las conversaciones de ese repo; si eliges toda la máquina o es tu alta, todas las de este equipo. Antes de enviar nada a Shakers te enseño qué se enviaría y te pregunto. No lo uses en un equipo ni con conversaciones que tengan datos que no puedes compartir (de clientes, de tu empresa o de terceros).', after: 'Hecho. Si aprobaste la subida, un modelo de IA de un proveedor externo (OpenAI, Google o Anthropic) analiza lo enviado por encargo de Shakers para calcular tu evaluación y después se descarta; solo se guarda el resultado (niveles, puntuaciones, recuentos y los nombres de tus herramientas y agentes). Si no la aprobaste, no se ha subido nada a tu perfil. Revisión humana: «Solicitar revisión humana» en la web o talent@shakersworks.com; para pedir que se borre lo enviado, el mismo correo. Para dejar de enviar desde este equipo: `shakers ai-usage --consent-revoke`.' },
|
|
@@ -1235,12 +1272,13 @@ const catalogs = {
|
|
|
1235
1272
|
share: { before: 'Voy a crear en este ordenador una tarjeta con tu informe de IA para compartir. No envío nada.', after: 'Hecho. La tarjeta está en este ordenador; no se ha enviado nada.' },
|
|
1236
1273
|
report: { before: 'Voy a leer tu informe de IA (de Shakers o de tu último análisis en este ordenador) y a guardarlo como archivo en este ordenador. No cambio tu perfil.' },
|
|
1237
1274
|
'add-skill': { before: 'Voy a leer las skills de tu último análisis en Shakers y a añadir a tu perfil las que elijas.', after: 'Hecho. Tu perfil de Shakers tiene las skills que elegiste.' },
|
|
1238
|
-
'add-agent': { before: 'Voy a leer los agentes de tu último análisis en Shakers y a añadir a tu perfil el que elijas, con la descripción que apruebes.', after: 'Hecho. Tu perfil de Shakers tiene
|
|
1275
|
+
'add-agent': { before: 'Voy a leer los agentes de tu último análisis en Shakers y a añadir a tu perfil el que elijas, con la descripción que apruebes, o todos los nuevos de una vez si lo eliges.', after: 'Hecho. Tu perfil de Shakers tiene los agentes que elegiste.' },
|
|
1239
1276
|
'add-project': { before: 'Voy a leer el nombre de esta carpeta, la dirección web de su código y las fechas de su primer y último cambio, y te pregunto antes de enviarlos a tu portfolio de Shakers. No envío tu código.', after: 'Hecho. El proyecto está en tu portfolio de Shakers.' },
|
|
1240
1277
|
'add-role': { before: 'Voy a leer los roles de Shakers y a añadir a tu perfil el que elijas.', after: 'Hecho. Tu perfil de Shakers tiene el rol que elegiste.' },
|
|
1241
1278
|
'change-role': { before: 'Voy a leer tus roles de Shakers y a guardar como principal el que elijas.', after: 'Hecho. Tu rol principal está guardado en Shakers.' },
|
|
1242
1279
|
'find-projects': { before: 'Solo leo de Shakers los proyectos que encajan contigo; no cambio nada.' },
|
|
1243
1280
|
'show-project': { before: 'Solo leo de Shakers el detalle del proyecto; no cambio nada.' },
|
|
1281
|
+
apply: { before: 'Voy a leer de Shakers las preguntas de este proyecto y, si confirmas, enviar tu mensaje y tus respuestas: el cliente verá tu perfil de Shakers y se abre un chat con el equipo. Si el proyecto lo pide, Shakers revisa tu candidatura antes de que la vea el cliente. No cambio tu perfil.', after: 'Hecho. Tu candidatura está en Shakers; puedes seguirla con shakers applications.' },
|
|
1244
1282
|
'save-project': { before: 'Voy a guardar este proyecto en tus proyectos guardados de Shakers.', after: 'Hecho. El proyecto está en tus guardados.' },
|
|
1245
1283
|
'unsave-project': { before: 'Voy a quitar este proyecto de tus proyectos guardados de Shakers.', after: 'Hecho. El proyecto ya no está en tus guardados.' },
|
|
1246
1284
|
invitations: { before: 'Solo leo tus invitaciones de Shakers; no cambio nada.' },
|
|
@@ -1474,6 +1512,16 @@ const catalogs = {
|
|
|
1474
1512
|
agentDeclaredSuccess: (name) => `✓ "${name}" añadido a tu perfil.`,
|
|
1475
1513
|
agentDeclareFailed: (reason) => `✗ No se pudo añadir el agente a tu perfil: ${reason}`,
|
|
1476
1514
|
agentHubSessionExpired: 'Tu sesión de Shakers ha caducado mientras añadíamos el agente a tu perfil.',
|
|
1515
|
+
addAllDetectedEntry: (n) => `Añadir todos los agentes detectados que aún no tienes (${n})`,
|
|
1516
|
+
detectedAgentsHeading: 'Agentes detectados que aún no están en tu perfil:',
|
|
1517
|
+
detectedAgentsHint: 'Flechas ↑/↓ para moverte · espacio para marcar o desmarcar · a para todos · enter para añadir los marcados · esc para no añadir ninguno',
|
|
1518
|
+
detectedAgentsNeedsYes: (n) => `Hay ${n} agente(s) detectado(s) que aún no están en tu perfil. Sin terminal no los añado; para añadirlos todos: shakers add-agent --all --yes`,
|
|
1519
|
+
detectedAgentsCheckFailed: (reason) => `No se pudo comprobar qué agentes tienes ya en tu perfil: ${reason} No añado ninguno para no duplicarlos; inténtalo más tarde con shakers add-agent --all.`,
|
|
1520
|
+
detectedAgentsAllOnProfile: 'Todos los agentes detectados ya están en tu perfil.',
|
|
1521
|
+
detectedAgentsDeclaring: 'Añadiendo los agentes a tu perfil…',
|
|
1522
|
+
detectedAgentsHubSessionExpired: 'Tu sesión de Shakers ha caducado mientras añadíamos los agentes a tu perfil.',
|
|
1523
|
+
detectedAgentFailed: (name, reason) => `✗ No se pudo añadir "${name}": ${reason}`,
|
|
1524
|
+
detectedAgentsSummary: (added, total) => `${added} de ${total} agente(s) añadido(s) a tu perfil.`,
|
|
1477
1525
|
agentRelateNoPortfolios: 'Todavía no tienes experiencias en tu perfil, así que el agente se ha añadido sin relacionar. Puedes añadir una con "Añadir proyecto al portfolio" y relacionarlo más tarde.',
|
|
1478
1526
|
agentRelateAsk: (agentName) => `¿Quieres relacionar "${agentName}" con alguna de tus experiencias? (s/n):`,
|
|
1479
1527
|
agentRelateSelectHeading: (agentName) => `Relaciona "${agentName}" con una o varias experiencias:`,
|
|
@@ -2502,6 +2550,42 @@ const catalogs = {
|
|
|
2502
2550
|
faqsHeading: 'FAQs',
|
|
2503
2551
|
done: 'Detail complete.',
|
|
2504
2552
|
},
|
|
2553
|
+
apply: {
|
|
2554
|
+
help:
|
|
2555
|
+
'apply — apply to a project/position with your message and your answers to its questions.\n\n'
|
|
2556
|
+
+ 'Usage:\n apply <number|id> [--lang es|en]\n\n'
|
|
2557
|
+
+ 'Requires an active session (shakers login) and an interactive terminal.\n',
|
|
2558
|
+
loginRequired: '"apply" requires an active session.\n Missing: sign-in. Run `shakers login` first.',
|
|
2559
|
+
needsInteractive: 'Applying needs an interactive terminal: I ask for your message and your answers, and for your confirmation before sending. From an AI assistant, use the apply_to_project MCP tool.',
|
|
2560
|
+
idRequired: 'The project is missing. Usage: apply <number|id>.',
|
|
2561
|
+
refNoCache: 'There is no recent listing. Run shakers find-projects first and use the number it shows (or pass an id).',
|
|
2562
|
+
refOutOfRange: (count) => `That number is not in the last listing (it has ${count}). Run shakers find-projects again.`,
|
|
2563
|
+
loading: 'Loading the application questions…',
|
|
2564
|
+
fetchFailed: (reason) => `Could not get the questions (${reason}). Try again in a moment.`,
|
|
2565
|
+
notFound: 'That project/position was not found.',
|
|
2566
|
+
notOpen: 'This project does not take your application right now: you already applied, it is closed or it is not available to you.',
|
|
2567
|
+
untitled: 'Untitled',
|
|
2568
|
+
companyLine: (name) => `Company: ${name}`,
|
|
2569
|
+
mainMotivationFallback: "What's your main motivation to apply?",
|
|
2570
|
+
askMessage: (question) => `${question} (your message for the team; enter to leave it blank):`,
|
|
2571
|
+
askOpen: (question) => `${question} (enter to skip):`,
|
|
2572
|
+
choiceHint: 'Arrows ↑/↓ to move · enter to pick',
|
|
2573
|
+
skipChoice: 'Skip',
|
|
2574
|
+
summaryTitle: 'This is what will be sent',
|
|
2575
|
+
titleLine: (title) => `Project: ${title}`,
|
|
2576
|
+
messageLine: (message) => `Message: ${message}`,
|
|
2577
|
+
empty: '(empty)',
|
|
2578
|
+
answerLine: (question, answer) => `${question}: ${answer}`,
|
|
2579
|
+
sendNotice: (title) => `Your message and your answers for "${title}" are sent: the client will see your Shakers profile and a chat with the team opens. Your profile does not change.`,
|
|
2580
|
+
reviewNotice: 'On this project Shakers reviews your application before the client sees it.',
|
|
2581
|
+
confirm: 'Send the application? (y/n):',
|
|
2582
|
+
cancelled: 'Cancelled. Nothing was sent.',
|
|
2583
|
+
sending: 'Sending your application…',
|
|
2584
|
+
sent: 'Application sent. You can follow it with shakers applications.',
|
|
2585
|
+
sentPending: 'Application sent. Shakers reviews it before the client sees it; you can follow it with shakers applications.',
|
|
2586
|
+
alreadyApplied: (status) => (status ? `You already applied to this project (status: ${status}).` : 'You already applied to this project.'),
|
|
2587
|
+
failed: (reason) => `Could not send the application (${reason}). Try again in a moment.`,
|
|
2588
|
+
},
|
|
2505
2589
|
savedPositions: {
|
|
2506
2590
|
helpSave: 'save-project — bookmark a project/position.\n\nUsage:\n save-project <number|id> [--json]\n\nRequires an active session (shakers login).',
|
|
2507
2591
|
helpUnsave: 'unsave-project — remove a project/position from your saved list.\n\nUsage:\n unsave-project <number|id> [--json]\n\nRequires an active session (shakers login).',
|
|
@@ -2903,6 +2987,7 @@ const catalogs = {
|
|
|
2903
2987
|
},
|
|
2904
2988
|
},
|
|
2905
2989
|
trust: {
|
|
2990
|
+
detectedAgents: 'If you confirm, I add the marked agents to your Shakers profile, only their name and description, and clients will see them on your profile. Nothing else is read or uploaded from this computer. You can remove them any time from your profile on the Shakers website.',
|
|
2906
2991
|
uploadWarning: "Before you approve: up to 40 literal excerpts of your messages (corrections and decisions) and 20 commands, 200 characters max each, your agents' configuration and definitions, and activity counts would be sent. Anything that looks like a secret, an email or a path is removed, but the filter is approximate: do not approve it if there is data you are not allowed to share (client, employer or third-party data).",
|
|
2907
2992
|
cli: {
|
|
2908
2993
|
'ai-usage': { before: "I am going to analyse on this computer your AI tools, the history of your code projects and your conversations with AI coding assistants (Claude Code, Codex, Gemini, Cursor, Continue, Cline/Roo, Aider, Pi, OpenCode). Analysing a repo covers only that repo's conversations; choosing the whole machine, or signing up, covers every conversation on this computer. Before sending anything to Shakers I show you what would be sent and ask you. Do not use it on a computer or with conversations holding data you are not allowed to share (client, employer or third-party data).", after: 'Done. If you approved the upload, an AI model from an external provider (OpenAI, Google or Anthropic) analyses what was sent on behalf of Shakers to compute your evaluation and then it is discarded; only the result is kept (levels, scores, counts and the names of your tools and agents). If you did not approve it, nothing went to your profile. Human review: "Request human review" on the web or talent@shakersworks.com; to ask for deletion of what was sent, the same email. To stop sending from this computer: `shakers ai-usage --consent-revoke`.' },
|
|
@@ -2910,12 +2995,13 @@ const catalogs = {
|
|
|
2910
2995
|
share: { before: 'I am going to create on this computer a card with your AI report to share. I send nothing.', after: 'Done. The card is on this computer; nothing was sent.' },
|
|
2911
2996
|
report: { before: 'I am going to read your AI report (from Shakers or from your last analysis on this computer) and save it as a file on this computer. I do not change your profile.' },
|
|
2912
2997
|
'add-skill': { before: 'I am going to read the skills from your last analysis on Shakers and add the ones you pick to your profile.', after: 'Done. Your Shakers profile has the skills you picked.' },
|
|
2913
|
-
'add-agent': { before: 'I am going to read the agents from your last analysis on Shakers and add the one you pick to your profile, with the description you approve.', after: 'Done. Your Shakers profile has the
|
|
2998
|
+
'add-agent': { before: 'I am going to read the agents from your last analysis on Shakers and add the one you pick to your profile, with the description you approve, or every new one at once if you choose so.', after: 'Done. Your Shakers profile has the agents you picked.' },
|
|
2914
2999
|
'add-project': { before: 'I am going to read this folder\'s name, the web address of its code and the dates of its first and last change, and ask you before sending them to your Shakers portfolio. I do not send your code.', after: 'Done. The project is in your Shakers portfolio.' },
|
|
2915
3000
|
'add-role': { before: 'I am going to read the Shakers roles and add the one you pick to your profile.', after: 'Done. Your Shakers profile has the role you picked.' },
|
|
2916
3001
|
'change-role': { before: 'I am going to read your Shakers roles and save the one you pick as your main role.', after: 'Done. Your main role is saved on Shakers.' },
|
|
2917
3002
|
'find-projects': { before: 'I only read from Shakers the projects that fit you; I change nothing.' },
|
|
2918
3003
|
'show-project': { before: 'I only read the project details from Shakers; I change nothing.' },
|
|
3004
|
+
apply: { before: 'I am going to read the questions of this project from Shakers and, if you confirm, send your message and your answers: the client will see your Shakers profile and a chat with the team opens. If the project asks for it, Shakers reviews your application before the client sees it. I do not change your profile.', after: 'Done. Your application is on Shakers; you can follow it with shakers applications.' },
|
|
2919
3005
|
'save-project': { before: 'I am going to save this project to your saved projects on Shakers.', after: 'Done. The project is in your saved projects.' },
|
|
2920
3006
|
'unsave-project': { before: 'I am going to remove this project from your saved projects on Shakers.', after: 'Done. The project is no longer in your saved projects.' },
|
|
2921
3007
|
invitations: { before: 'I only read your Shakers invitations; I change nothing.' },
|
|
@@ -3153,6 +3239,16 @@ const catalogs = {
|
|
|
3153
3239
|
agentDeclaredSuccess: (name) => `✓ "${name}" added to your profile.`,
|
|
3154
3240
|
agentDeclareFailed: (reason) => `✗ Could not add the agent to your profile: ${reason}`,
|
|
3155
3241
|
agentHubSessionExpired: 'Your Shakers session expired while we were adding the agent to your profile.',
|
|
3242
|
+
addAllDetectedEntry: (n) => `Add every detected agent you do not have yet (${n})`,
|
|
3243
|
+
detectedAgentsHeading: 'Detected agents that are not on your profile yet:',
|
|
3244
|
+
detectedAgentsHint: 'Arrows ↑/↓ to move · space to mark or unmark · a for all · enter to add the marked ones · esc to add none',
|
|
3245
|
+
detectedAgentsNeedsYes: (n) => `${n} detected agent(s) are not on your profile yet. Without a terminal I do not add them; to add them all: shakers add-agent --all --yes`,
|
|
3246
|
+
detectedAgentsCheckFailed: (reason) => `Could not check which agents are already on your profile: ${reason} I add none so they are not duplicated; try again later with shakers add-agent --all.`,
|
|
3247
|
+
detectedAgentsAllOnProfile: 'Every detected agent is already on your profile.',
|
|
3248
|
+
detectedAgentsDeclaring: 'Adding the agents to your profile…',
|
|
3249
|
+
detectedAgentsHubSessionExpired: 'Your Shakers session expired while we were adding the agents to your profile.',
|
|
3250
|
+
detectedAgentFailed: (name, reason) => `✗ Could not add "${name}": ${reason}`,
|
|
3251
|
+
detectedAgentsSummary: (added, total) => `${added} of ${total} agent(s) added to your profile.`,
|
|
3156
3252
|
// AFTER the agent is declared, offer to relate it to the experiences/projects the talent already declared (portfolioId only).
|
|
3157
3253
|
agentRelateNoPortfolios: "You don't have any experiences in your profile yet, so the agent was added without a relation. Add one with \"Add project to portfolio\" and relate it later.",
|
|
3158
3254
|
agentRelateAsk: (agentName) => `Do you want to relate "${agentName}" to any of your experiences? (y/n):`,
|
package/src/mcp-ai-usage-tool.js
CHANGED
|
@@ -313,7 +313,7 @@ function makeAiUsageTools(deps = {}) {
|
|
|
313
313
|
aiProfile,
|
|
314
314
|
relayVerbatim: [...confirmation, reviewTexts.resultText(review.lang, aiProfile.status === 'ready' ? aiProfile.preview : null, { explained })].join('\n\n'),
|
|
315
315
|
next: enrichment === 'pending' || aiProfile.status !== 'ready' ? 'ai_usage_result' : null,
|
|
316
|
-
message: `Uploaded. ${VERBATIM} Then show \`text\`, the full report, verbatim.${enrichment === 'pending' || aiProfile.status !== 'ready' ? ' Shakers is still evaluating (normal, not an error): call ai_usage_result in a few minutes for the result and its explanation.' : ''}`,
|
|
316
|
+
message: `Uploaded. ${VERBATIM} Then show \`text\`, the full report, verbatim.${enrichment === 'pending' || aiProfile.status !== 'ready' ? ' Shakers is still evaluating (normal, not an error): call ai_usage_result in a few minutes for the result and its explanation.' : ''}${hasAgents && !explained ? ' Then call add_detected_agents to offer adding the detected agents to the profile.' : ''}`,
|
|
317
317
|
display: {
|
|
318
318
|
tier: { key: maturity.tierKey, name: maturity.tierName, setupLevel: maturity.name, score: maturity.score },
|
|
319
319
|
aiProfile: {
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const ID_PROPERTY = { type: 'string', description: 'Position id (UUID or legacy Mongo id) from find_projects or show_project. An internal handle: never show it to the talent.' };
|
|
4
|
+
|
|
5
|
+
const PREPARE_APPLICATION_SCHEMA = {
|
|
6
|
+
type: 'object',
|
|
7
|
+
properties: { id: ID_PROPERTY },
|
|
8
|
+
required: ['id'],
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
const APPLY_TO_PROJECT_SCHEMA = {
|
|
12
|
+
type: 'object',
|
|
13
|
+
properties: {
|
|
14
|
+
id: ID_PROPERTY,
|
|
15
|
+
message: { type: 'string', description: "The talent's own message for the team, in their words, answering mainMotivationQuestion. Never write it for them." },
|
|
16
|
+
answers: {
|
|
17
|
+
type: 'array',
|
|
18
|
+
description: 'The answers the talent gave, one per question they answered; leave out the ones they skipped.',
|
|
19
|
+
items: {
|
|
20
|
+
type: 'object',
|
|
21
|
+
properties: {
|
|
22
|
+
question: { type: 'integer', description: 'The question number from prepare_application.' },
|
|
23
|
+
answer: { type: 'string', description: 'For OPEN, the talent\'s text. For MULTIPLE_CHOICE, exactly one of that question\'s options.' },
|
|
24
|
+
},
|
|
25
|
+
required: ['question', 'answer'],
|
|
26
|
+
},
|
|
27
|
+
},
|
|
28
|
+
confirm: { type: 'boolean', description: 'true ONLY after the talent explicitly said yes to the summary this tool returned for these exact arguments.' },
|
|
29
|
+
},
|
|
30
|
+
required: ['id'],
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
function parseAnswers(raw, questions) {
|
|
34
|
+
const answers = [];
|
|
35
|
+
const invalid = [];
|
|
36
|
+
for (const entry of Array.isArray(raw) ? raw : []) {
|
|
37
|
+
const q = entry && Number.isInteger(entry.question) ? questions[entry.question - 1] : null;
|
|
38
|
+
const said = entry && typeof entry.answer === 'string' ? entry.answer.trim() : '';
|
|
39
|
+
if (!q) { invalid.push({ question: entry && entry.question, reason: 'unknown-question' }); continue; }
|
|
40
|
+
if (!said) continue;
|
|
41
|
+
if (q.type === 'MULTIPLE_CHOICE') {
|
|
42
|
+
const option = q.options.find((o) => o.toLowerCase() === said.toLowerCase());
|
|
43
|
+
if (!option) { invalid.push({ question: entry.question, reason: 'not-an-option', options: q.options }); continue; }
|
|
44
|
+
answers.push({ question: q.text, answer: [option] });
|
|
45
|
+
} else {
|
|
46
|
+
answers.push({ question: q.text, answer: said });
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return { answers, invalid };
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function makeApplyTools(deps = {}) {
|
|
53
|
+
const {
|
|
54
|
+
loadApplication = (opts) => require('./apply-client').loadApplication({}, opts),
|
|
55
|
+
submitApplication = (opts) => require('./apply-client').submitApplication({}, opts),
|
|
56
|
+
applicationSummary = require('./apply-client').applicationSummary,
|
|
57
|
+
loadAuthSession = require('./auth-session-store').loadAuthSession,
|
|
58
|
+
sessionStatus = require('./auth-session-store').sessionStatus,
|
|
59
|
+
applyCopy = () => require('./signup-copy').applyCopy(require('./signup-language').signupLanguage(require('./i18n').detectFlowLang())),
|
|
60
|
+
} = deps;
|
|
61
|
+
|
|
62
|
+
const shownSummaries = new Set();
|
|
63
|
+
|
|
64
|
+
const requireLoginSession = () => {
|
|
65
|
+
const session = loadAuthSession();
|
|
66
|
+
if (sessionStatus(session) !== 'active') {
|
|
67
|
+
throw new Error('no active Shakers session — sign in with social_signin_start (or run `shakers login`) first (this tool is session-gated).');
|
|
68
|
+
}
|
|
69
|
+
return session;
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
async function readApplication(args) {
|
|
73
|
+
const session = requireLoginSession();
|
|
74
|
+
const positionId = typeof args.id === 'string' ? args.id.trim() : '';
|
|
75
|
+
if (!positionId) return { session, result: { ok: false, reason: 'no-id' } };
|
|
76
|
+
const app = await loadApplication({ hubAccessToken: session.hubAccessToken, positionId });
|
|
77
|
+
if (!app.ok) return { session, result: { ok: false, reason: app.reason } };
|
|
78
|
+
if (!app.open) return { session, result: { ok: false, reason: 'not-open', relayVerbatim: applyCopy().notOpen, message: 'Nothing was sent. Print relayVerbatim word for word.' } };
|
|
79
|
+
return { session, positionId, app };
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
async function prepareApplication(args = {}) {
|
|
83
|
+
const { result, app } = await readApplication(args);
|
|
84
|
+
if (result) return result;
|
|
85
|
+
const t = applyCopy();
|
|
86
|
+
return {
|
|
87
|
+
ok: true,
|
|
88
|
+
title: app.title,
|
|
89
|
+
company: app.company,
|
|
90
|
+
mainMotivationQuestion: app.mainMotivationQuestion || t.mainMotivationFallback,
|
|
91
|
+
questions: app.questions.map((q, i) => ({ number: i + 1, ...q })),
|
|
92
|
+
reviewedByShakers: !!app.doubleCheck && app.doubleCheck !== 'NO',
|
|
93
|
+
message: 'Nothing has been sent. Ask the talent the mainMotivationQuestion and then each question, one at a time, in their language; for MULTIPLE_CHOICE list its options numbered. Every answer is optional. Use only what the talent writes: never invent, complete or improve the message or the answers. Then call apply_to_project with them.',
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
async function applyToProject(args = {}) {
|
|
98
|
+
const { session, positionId, app, result } = await readApplication(args);
|
|
99
|
+
if (result) return result;
|
|
100
|
+
const t = applyCopy();
|
|
101
|
+
const { answers, invalid } = parseAnswers(args.answers, app.questions);
|
|
102
|
+
if (invalid.length) {
|
|
103
|
+
return { ok: false, reason: 'invalid-answers', invalid, message: 'Nothing was sent. Some answers do not match the questions from prepare_application: ask the talent again for those and call apply_to_project with the question number and, for MULTIPLE_CHOICE, one of its options exactly.' };
|
|
104
|
+
}
|
|
105
|
+
const message = typeof args.message === 'string' ? args.message.trim() : '';
|
|
106
|
+
const key = JSON.stringify([positionId, message, answers]);
|
|
107
|
+
if (args.confirm !== true || !shownSummaries.has(key)) {
|
|
108
|
+
shownSummaries.add(key);
|
|
109
|
+
return {
|
|
110
|
+
ok: false,
|
|
111
|
+
reason: 'confirmation-required',
|
|
112
|
+
say: [...applicationSummary(t, app, { message, answers }), t.confirmQuestion].join('\n\n'),
|
|
113
|
+
message: "Nothing has been sent. Print say word for word and wait for the talent. Call apply_to_project again with exactly the same arguments and confirm:true only if the talent explicitly says yes to sending it; if they want changes, collect them and call it without confirm. Only the talent decides: never confirm on their behalf, and never invent the message or the answers.",
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
shownSummaries.delete(key);
|
|
117
|
+
const mainMotivationQuestion = app.mainMotivationQuestion || t.mainMotivationFallback;
|
|
118
|
+
const res = await submitApplication({ hubAccessToken: session.hubAccessToken, positionId, message, answers, mainMotivationQuestion });
|
|
119
|
+
if (!res.ok) {
|
|
120
|
+
if (res.reason === 'already-applied') return { ok: false, reason: res.reason, applicationStatus: res.applicationStatus, relayVerbatim: t.alreadyApplied(res.applicationStatus), message: 'Print relayVerbatim word for word.' };
|
|
121
|
+
if (res.reason === 'position-unavailable') return { ok: false, reason: res.reason, relayVerbatim: t.notOpen, message: 'Print relayVerbatim word for word.' };
|
|
122
|
+
return { ok: false, reason: res.reason, message: 'The application was not sent. Tell the talent it could not be sent and that they can try again later.' };
|
|
123
|
+
}
|
|
124
|
+
return {
|
|
125
|
+
ok: true,
|
|
126
|
+
status: res.status,
|
|
127
|
+
relayVerbatim: res.status === 'PENDING' ? t.sentPending : t.sent,
|
|
128
|
+
message: 'Print relayVerbatim word for word.',
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return [
|
|
133
|
+
{
|
|
134
|
+
name: 'prepare_application',
|
|
135
|
+
title: 'See what applying to a project asks',
|
|
136
|
+
description: 'Reads the questions you are asked when you apply to a Shakers project. It sends nothing.',
|
|
137
|
+
guidance: "Read-only first step to apply to a project/Position as an already-registered talent: returns its title, company, the main motivation question and the extra questions (OPEN or MULTIPLE_CHOICE with options), numbered. If reason is not-open, the talent already applied or the project is closed: print relayVerbatim and stop. Then ask the talent each question in the chat and call apply_to_project with their own words. Never invent or complete answers. Requires an active session and the position id.",
|
|
138
|
+
inputSchema: PREPARE_APPLICATION_SCHEMA,
|
|
139
|
+
handler: prepareApplication,
|
|
140
|
+
},
|
|
141
|
+
{
|
|
142
|
+
name: 'apply_to_project',
|
|
143
|
+
title: 'Apply to a project',
|
|
144
|
+
description: 'Shows you exactly what your application to a Shakers project will send and sends it only after you say yes.',
|
|
145
|
+
guidance: "Apply to a project/Position for the talent with their message and their answers to the prepare_application questions. Only the message and the answers are sent: never budget, phone, location or availability. Call it first without confirm: it returns `say`, the literal summary and the question, to print word for word. Only when the talent explicitly answers yes, call it again with exactly the same arguments and confirm:true; a confirm without that summary shown counts for nothing. Never confirm on the talent's behalf and never invent, complete or improve their message or answers. Print relayVerbatim word for word. Requires an active session and the position id, which is an internal handle never shown to the talent.",
|
|
146
|
+
inputSchema: APPLY_TO_PROJECT_SCHEMA,
|
|
147
|
+
handler: applyToProject,
|
|
148
|
+
},
|
|
149
|
+
];
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
module.exports = { makeApplyTools, parseAnswers, PREPARE_APPLICATION_SCHEMA, APPLY_TO_PROJECT_SCHEMA };
|
|
@@ -42,6 +42,31 @@ const ADD_AGENT_SCHEMA = {
|
|
|
42
42
|
required: ['agentName'],
|
|
43
43
|
};
|
|
44
44
|
|
|
45
|
+
const ADD_DETECTED_AGENTS_SCHEMA = {
|
|
46
|
+
type: 'object',
|
|
47
|
+
properties: {
|
|
48
|
+
selection: { type: 'string', description: 'Exactly what the talent typed in reply to the numbered list this tool returned: numbers such as "1 3", "all" or "none", in their words. Never set it yourself.' },
|
|
49
|
+
},
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
const ALL_WORDS = new Set(['all', 'todos', 'todas', 'tutti', 'tutte', 'tudo']);
|
|
53
|
+
const NONE_WORDS = new Set(['none', 'ninguno', 'ninguna', 'nada', 'nessuno', 'nessuna', 'nenhum', 'nenhuma', '0']);
|
|
54
|
+
|
|
55
|
+
function parseSelection(raw, agents) {
|
|
56
|
+
const said = typeof raw === 'string' ? raw.normalize('NFD').replace(/\p{M}/gu, '').toLowerCase().replace(/[«»"'.]/g, ' ').trim() : '';
|
|
57
|
+
if (!said) return null;
|
|
58
|
+
if (ALL_WORDS.has(said)) return agents;
|
|
59
|
+
if (NONE_WORDS.has(said)) return [];
|
|
60
|
+
const tokens = said.split(/[\s,;]+|\b(?:y|and|e)\b/).map((t) => t.trim()).filter(Boolean);
|
|
61
|
+
const picked = new Set();
|
|
62
|
+
for (const token of tokens) {
|
|
63
|
+
const n = /^\d+$/.test(token) ? Number(token) : NaN;
|
|
64
|
+
if (!Number.isInteger(n) || n < 1 || n > agents.length) return null;
|
|
65
|
+
picked.add(n - 1);
|
|
66
|
+
}
|
|
67
|
+
return picked.size ? agents.filter((_, i) => picked.has(i)) : null;
|
|
68
|
+
}
|
|
69
|
+
|
|
45
70
|
const ADD_PROJECT_SCHEMA = {
|
|
46
71
|
type: 'object',
|
|
47
72
|
properties: {
|
|
@@ -79,6 +104,9 @@ function makeOnboardingTools(deps = {}) {
|
|
|
79
104
|
requestDeclareAgent = require('./agents-client').requestDeclareAgent,
|
|
80
105
|
requestListAgents = require('./agents-client').requestListAgents,
|
|
81
106
|
requestRelateAgentPortfolios = require('./agents-client').requestRelateAgentPortfolios,
|
|
107
|
+
newDetectedAgents = require('./detected-agents').newDetectedAgents,
|
|
108
|
+
declareDetectedAgents = require('./detected-agents').declareDetectedAgents,
|
|
109
|
+
detectedAgentsCopy = () => require('./signup-copy').detectedAgentsCopy(require('./signup-language').signupLanguage(require('./i18n').detectFlowLang())),
|
|
82
110
|
requestDeclarePortfolio = require('./portfolio-client').requestDeclarePortfolio,
|
|
83
111
|
requestListPortfolios = require('./portfolio-client').requestListPortfolios,
|
|
84
112
|
requestUpdatePortfolioSkills = require('./portfolio-client').requestUpdatePortfolioSkills,
|
|
@@ -275,6 +303,57 @@ function makeOnboardingTools(deps = {}) {
|
|
|
275
303
|
return { ...base, portfolios, related, relateFailures };
|
|
276
304
|
}
|
|
277
305
|
|
|
306
|
+
// The detected agents offered last, so a selection counts only for the list the talent was shown.
|
|
307
|
+
let offeredAgents = null;
|
|
308
|
+
|
|
309
|
+
async function addDetectedAgents(args = {}) {
|
|
310
|
+
const session = requireSession();
|
|
311
|
+
const inventoryEndpoint = requireEndpoint(getUsageDiscoveredInventoryEndpoint, 'discovered-inventory');
|
|
312
|
+
const listEndpoint = requireEndpoint(getAgentsListEndpoint, 'agents-list');
|
|
313
|
+
const declareEndpoint = requireEndpoint(getAgentsDeclareEndpoint, 'agents-declare');
|
|
314
|
+
const c = detectedAgentsCopy();
|
|
315
|
+
const inv = await requestDiscoveredInventoryWithRefresh(
|
|
316
|
+
{ accessToken: session.accessToken, refreshToken: refreshTokenFor },
|
|
317
|
+
{ endpoint: inventoryEndpoint, request: requestDiscoveredInventory },
|
|
318
|
+
);
|
|
319
|
+
if (!inv.ok) return { ok: false, reason: inv.reason || 'inventory-failed', message: 'Nothing was added: the detected agents could not be read.' };
|
|
320
|
+
const listed = await requestListAgents(
|
|
321
|
+
{ accessToken: session.accessToken, hubAccessToken: session.hubAccessToken },
|
|
322
|
+
{ endpoint: listEndpoint },
|
|
323
|
+
);
|
|
324
|
+
if (!listed.ok) return { ok: false, reason: listed.reason, message: 'Nothing was added: the agents already on the profile could not be checked, so none are added to avoid duplicates.' };
|
|
325
|
+
const agents = newDetectedAgents(inv.agents, listed.agents);
|
|
326
|
+
if (agents.length === 0) {
|
|
327
|
+
offeredAgents = null;
|
|
328
|
+
return { ok: true, added: [], relayVerbatim: c.allOnProfile, message: 'Nothing to add. Print relayVerbatim word for word.' };
|
|
329
|
+
}
|
|
330
|
+
const offered = offeredAgents && JSON.stringify(offeredAgents.map((a) => a.name)) === JSON.stringify(agents.map((a) => a.name));
|
|
331
|
+
const chosen = offered ? parseSelection(args.selection, agents) : null;
|
|
332
|
+
if (!chosen) {
|
|
333
|
+
offeredAgents = agents;
|
|
334
|
+
return {
|
|
335
|
+
ok: false,
|
|
336
|
+
reason: 'selection-required',
|
|
337
|
+
say: [c.intro, agents.map((a, i) => `${i + 1}. ${a.name}`).join('\n'), c.question].join('\n\n'),
|
|
338
|
+
message: 'Nothing has been added. Print say word for word and stop: the talent replies with numbers (such as "1 3"), "all" or "none". Then call add_detected_agents again with selection = exactly what the talent typed. Only the talent decides: never choose, confirm or fill in selection for them, and never call it with a selection they did not type.',
|
|
339
|
+
};
|
|
340
|
+
}
|
|
341
|
+
offeredAgents = null;
|
|
342
|
+
if (chosen.length === 0) return { ok: true, added: [], relayVerbatim: c.noneChosen, message: 'Nothing was added. Print relayVerbatim word for word.' };
|
|
343
|
+
const { added, failed } = await declareDetectedAgents(chosen, session, { endpoint: declareEndpoint, declare: requestDeclareAgent });
|
|
344
|
+
const expired = failed.some((f) => f.reason === 'hub-session-expired');
|
|
345
|
+
return {
|
|
346
|
+
ok: added.length > 0,
|
|
347
|
+
added: added.map((a) => a.name),
|
|
348
|
+
failed: failed.map((f) => ({ name: f.agent.name, reason: f.reason })),
|
|
349
|
+
relayVerbatim: [
|
|
350
|
+
added.length ? c.added(added.map((a) => a.name).join(', ')) : null,
|
|
351
|
+
failed.length ? c.failed(failed.map((f) => f.agent.name).join(', ')) : null,
|
|
352
|
+
].filter(Boolean).join('\n'),
|
|
353
|
+
message: `Print relayVerbatim word for word.${expired ? ' The Shakers session expired: ask the talent to sign in again (social_signin_start), then call add_detected_agents again to offer the ones still missing.' : ''}`,
|
|
354
|
+
};
|
|
355
|
+
}
|
|
356
|
+
|
|
278
357
|
// Sends the project only after the talent says yes to exactly what goes: name, web address, dates, client.
|
|
279
358
|
async function addProject(args = {}, ctx = {}) {
|
|
280
359
|
const session = requireSession();
|
|
@@ -356,6 +435,14 @@ function makeOnboardingTools(deps = {}) {
|
|
|
356
435
|
inputSchema: ADD_AGENT_SCHEMA,
|
|
357
436
|
handler: addAgent,
|
|
358
437
|
},
|
|
438
|
+
{
|
|
439
|
+
name: 'add_detected_agents',
|
|
440
|
+
title: 'Add your detected agents',
|
|
441
|
+
description: 'Lists the AI agents found in your analysis that are not on your Shakers profile yet and adds the ones you choose, only their name and description.',
|
|
442
|
+
guidance: "Offer the talent to add, in one step, the AI agents detected by their submitted ai_usage report that are not on their Shakers profile yet (agents already there are skipped by name). No experience or portfolio is needed or related. Call it without selection: it returns `say`, a numbered list and the question, to print word for word; the talent replies with numbers, all or none. Then call it again with selection set to exactly what the talent typed. The talent's reply is the only confirmation: never pick, confirm or fill in selection on their behalf, and a selection counts only after the list was shown. Print relayVerbatim word for word. Offer it after an ai_usage_upload that sent agents. Requires an active session.",
|
|
443
|
+
inputSchema: ADD_DETECTED_AGENTS_SCHEMA,
|
|
444
|
+
handler: addDetectedAgents,
|
|
445
|
+
},
|
|
359
446
|
{
|
|
360
447
|
name: 'add_project',
|
|
361
448
|
title: 'Add this project to your portfolio',
|
|
@@ -373,5 +460,7 @@ module.exports = {
|
|
|
373
460
|
LIST_ADDABLE_SCHEMA,
|
|
374
461
|
ADD_SKILL_SCHEMA,
|
|
375
462
|
ADD_AGENT_SCHEMA,
|
|
463
|
+
ADD_DETECTED_AGENTS_SCHEMA,
|
|
376
464
|
ADD_PROJECT_SCHEMA,
|
|
465
|
+
parseSelection,
|
|
377
466
|
};
|