shakers-cli-beta 0.12.0 → 0.14.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.
Files changed (44) hide show
  1. package/README.md +28 -3
  2. package/bin/mcp.js +13 -7
  3. package/bin/register.js +5 -2
  4. package/package.json +1 -1
  5. package/src/authorship-mismatch.js +62 -0
  6. package/src/backend-request.js +12 -8
  7. package/src/config.js +27 -6
  8. package/src/consent-flow.js +10 -10
  9. package/src/i18n.js +16 -12
  10. package/src/mcp-ai-usage-result-tool.js +4 -2
  11. package/src/mcp-ai-usage-tool.js +3 -1
  12. package/src/mcp-alma-tools.js +7 -3
  13. package/src/mcp-applications-tools.js +4 -2
  14. package/src/mcp-auth-form.js +39 -10
  15. package/src/mcp-availability-tools.js +4 -2
  16. package/src/mcp-certifications-tools.js +4 -2
  17. package/src/mcp-certify-dimension-tools.js +4 -2
  18. package/src/mcp-choice.js +30 -4
  19. package/src/mcp-device-login-tools.js +6 -2
  20. package/src/mcp-find-projects-tools.js +4 -2
  21. package/src/mcp-instructions.js +6 -6
  22. package/src/mcp-invitations-tools.js +4 -2
  23. package/src/mcp-lang-tools.js +4 -2
  24. package/src/mcp-onboarding-tools.js +41 -44
  25. package/src/mcp-portfolios-tools.js +7 -3
  26. package/src/mcp-profile-tools.js +4 -2
  27. package/src/mcp-prompts.js +3 -3
  28. package/src/mcp-rate-tools.js +4 -2
  29. package/src/mcp-register-tools.js +53 -20
  30. package/src/mcp-report-tools.js +7 -3
  31. package/src/mcp-role-tools.js +36 -13
  32. package/src/mcp-saved-positions-tools.js +7 -3
  33. package/src/mcp-server.js +17 -4
  34. package/src/mcp-show-project-tools.js +4 -2
  35. package/src/mcp-signup-tools.js +229 -62
  36. package/src/mcp-socials-tools.js +4 -2
  37. package/src/onboarding-flow.js +112 -13
  38. package/src/onboarding-funnel.js +272 -0
  39. package/src/profile-client.js +11 -0
  40. package/src/share.js +4 -3
  41. package/src/signup-copy.js +84 -26
  42. package/src/terminal-agents.js +8 -0
  43. package/src/usage-run-steps.js +3 -0
  44. package/src/mcp-ask-tools.js +0 -50
@@ -4,7 +4,7 @@
4
4
 
5
5
  const SIGNUP_LANGUAGES = ['es', 'en', 'it', 'pt'];
6
6
 
7
- // Legal texts. PENDING legal/GDPR validation: not final copy. The CLI onboarding reads the es/en ones too.
7
+ // Legal texts. The CLI onboarding reads the es/en ones too.
8
8
  const LEGAL = {
9
9
  es: {
10
10
  signupNotice: (sources) => `Antes de crear tu cuenta: guardamos los datos que me des (${listOf('es', [...named(sources, COPY.es.importSources), 'lo que sé de ti'])}) y podemos contactarte. Te completo el perfil automáticamente y cualquier campo lo puedes cambiar después en la web. La entrevista de onboarding la puedes hacer luego aquí mismo en el chat o en la web.`,
@@ -52,6 +52,16 @@ function joinParts(parts) {
52
52
  return parts.filter((p) => p !== null && p !== undefined && String(p).trim() !== '').join(' · ');
53
53
  }
54
54
 
55
+ function countryName(lang, code) {
56
+ if (typeof code !== 'string' || !/^[A-Za-z]{2}$/.test(code.trim())) return null;
57
+ const upper = code.trim().toUpperCase();
58
+ try {
59
+ return new Intl.DisplayNames([LOCALES[toSignupLanguage(lang) || 'en']], { type: 'region' }).of(upper) || upper;
60
+ } catch {
61
+ return upper;
62
+ }
63
+ }
64
+
55
65
  function amount(lang, value) {
56
66
  return new Intl.NumberFormat(LOCALES[lang], { maximumFractionDigits: 0 }).format(Number(value));
57
67
  }
@@ -59,10 +69,10 @@ function amount(lang, value) {
59
69
  const COPY = {
60
70
  es: {
61
71
  welcome: (name) => `¡Hola${name ? `, ${name}` : ''}! Gracias por querer unirte a Shakers 🙌\n\nSomos una comunidad de profesionales tech freelance. Nuestro objetivo es tener siempre tu versión más actual como profesional (lo que sabes hacer hoy, cómo trabajas con IA y qué buscas) para conectarte con proyectos que de verdad encajen contigo.\n\nEl alta son tres pasos y la mayor parte la hago yo por ti:\n1. Tu perfil (2 min). Lo relleno a partir de tu CV y LinkedIn, tú solo revisas y confirmas.\n2. Cómo trabajas con IA (1-2 min, opcional). Analizo qué herramientas usas en este equipo. Tu código y tus prompts nunca salen de tu máquina.\n3. Una breve entrevista (opcional). Para afinar tu rol y lo que buscas. Puedes hacerla ahora o más tarde.\n\nTodo lo puedes editar después desde la web.`,
62
- welcomeQuestion: '¿Empezamos? Si me das permiso, busco tu CV en este equipo y te preparo un borrador para que solo tengas que confirmarlo.',
63
- cvSearch: 'Sí, búscalo',
64
- cvAttach: 'Te lo adjunto yo',
65
- cvAttachAsk: 'Perfecto, adjúntamelo aquí.',
72
+ welcomeQuestion: '¿Empezamos? Pásame tu CV, o si lo tienes en este equipo, te lo busco yo. Con él te preparo un borrador para que solo tengas que confirmarlo.',
73
+ cvSearch: 'Búscalo en mi equipo',
74
+ cvAttach: 'Te lo paso',
75
+ cvAttachAsk: 'Perfecto, adjúntalo aquí o dime dónde está en tu equipo.',
66
76
  cvNotFound: 'No he encontrado tu CV en este equipo. ¿Me lo adjuntas aquí?',
67
77
  linkedinAsk: '¿Me pasas tu perfil de LinkedIn?',
68
78
  linkedinOpening: 'Te abro tu perfil de LinkedIn en el navegador para que copies la dirección.',
@@ -87,18 +97,29 @@ const COPY = {
87
97
  existingAnnual: (from, to) => `Tarifa anual: ${from} → ${to}`,
88
98
  existingHours: (from, to) => `Disponibilidad: ${from} → ${to}`,
89
99
  existingEmpty: 'sin indicar',
90
- existingStays: (label, kept, requested) => (kept ? `${label}: se queda en ${kept}, no ${requested}` : `${label}: sin cambios`),
100
+ existingUpdateQuestion: '¿Actualizo tu perfil con esto?',
101
+ existingStays: (label, kept, requested) => (kept ? `${label}: se queda en ${kept}${kept === requested ? '' : `, no ${requested}`}` : `${label}: sin cambios`),
91
102
  existingStaysNote: 'Lo que ya tenías relleno no lo cambio desde aquí: si quieres otros valores, cámbialos en la web.',
92
103
  existingLists: (sources) => `Añado lo nuevo de ${listOf('es', named(sources, COPY.es.importSources))} (experiencias, proyectos, formación) y relleno lo que tengas vacío, sin borrar nada.`,
93
104
  existingYes: 'Sí, actualízalo',
94
105
  existingNo: 'No, déjalo como está',
95
106
  existingKept: 'Perfecto, dejo tu perfil como está.',
96
107
  keptLabels: { workSituation: 'Situación laboral', availability: 'Disponibilidad', languages: 'Idiomas', phone: 'Teléfono' },
108
+ situationLabels: { FREELANCE: 'Freelance', EMPLOYED: 'Empleado/a', BETWEEN_JOBS: 'Entre proyectos', STUDYING: 'Estudiando', OTHER: 'Otro' },
109
+ participationLabels: { FULL_TIME: 'jornada completa', PART_TIME: 'media jornada' },
97
110
  existingUnchanged: (items) => `Estos datos de tu perfil no los he cambiado, se quedan como estaban: ${items.join(' · ')}. Si quieres otros valores, cámbialos en la web.`,
98
- aiUsageQuestion: '¿Quieres que analice cómo trabajas con IA? Es opcional y tarda 1-2 min. Solo veo qué herramientas usas, tu código y tus prompts no salen de tu máquina. Te ayuda a destacar en proyectos de IA.',
111
+ aiUsageQuestion: '¿Quieres que analice cómo trabajas con IA? Es opcional y tarda 1-2 min. Solo veo qué herramientas usas, tu código y tus prompts no salen de tu máquina. Miro el uso de IA de todo este equipo, que doy por tuyo. Te ayuda a destacar en proyectos de IA.',
99
112
  aiUsageYes: 'Sí, analízalo',
100
113
  aiUsageSkip: 'Saltar este paso',
101
114
  aiUsageSkipped: "Sin problema, seguimos sin él. Te aviso de que es lo que más pesa para que te lleguen proyectos: lo puedes lanzar cuando quieras diciéndome 'analiza mi uso de IA'.",
115
+ consent: {
116
+ yes: 'Sí, adelante',
117
+ no: 'No',
118
+ cvSearch: 'Shakers buscará en este ordenador archivos que puedan ser tu CV: mira los nombres de los archivos PDF y Word de tus carpetas Escritorio, Documentos, Descargas e iCloud Drive, y lee tu zona horaria y el nombre, el email y el usuario de GitHub que guardan tus herramientas de programación. No abre esos archivos y no envía nada. ¿Continuamos?',
119
+ cvRead: (file) => `Shakers leerá el archivo ${file} para preparar el borrador de tu perfil. Todavía no se envía nada. ¿Continuamos?`,
120
+ cvSend: (file) => `Shakers enviará el archivo ${file} a tu cuenta de Shakers para completar tu perfil. ¿Continuamos?`,
121
+ project: (p) => `Shakers añadirá el proyecto de la carpeta ${p.folder} a tu portfolio y enviará ${listOf('es', [`el nombre "${p.name}"`, p.url && `la dirección web ${p.url}`, p.startDate && p.endDate ? `las fechas del ${p.startDate} al ${p.endDate}` : (p.startDate || p.endDate) && `la fecha ${p.startDate || p.endDate}`, p.client && `el cliente ${p.client}`, p.description && 'la descripción que acordaste'].filter(Boolean))}. ¿Continuamos?`,
122
+ },
102
123
  importSources: { linkedin: 'tu LinkedIn', cv: 'tu CV', github: 'tu GitHub', web: 'tu web' },
103
124
  importFailed: (names) => `No he podido importar ${names.join(' y ')} ahora mismo. El resto de tu perfil está guardado; lo que falte lo puedes añadir en la web.`,
104
125
  interviewWhereQuestion: '¿Hacemos la entrevista de onboarding aquí ahora o luego en la web?',
@@ -132,10 +153,10 @@ const COPY = {
132
153
  },
133
154
  en: {
134
155
  welcome: (name) => `Hi${name ? `, ${name}` : ''}! Thanks for wanting to join Shakers 🙌\n\nWe are a community of freelance tech professionals. Our goal is to always have the most up-to-date version of you as a professional (what you can do today, how you work with AI and what you are looking for) to connect you with projects that truly fit you.\n\nSigning up takes three steps and I do most of it for you:\n1. Your profile (2 min). I fill it in from your CV and LinkedIn; you just review and confirm.\n2. How you work with AI (1-2 min, optional). I look at which tools you use on this computer. Your code and your prompts never leave your machine.\n3. A short interview (optional). To fine-tune your role and what you are looking for. You can do it now or later.\n\nYou can edit everything later on the web.`,
135
- welcomeQuestion: "Shall we start? With your permission, I'll look for your CV on this computer and prepare a draft so you only have to confirm it.",
136
- cvSearch: 'Yes, look for it',
137
- cvAttach: "I'll attach it",
138
- cvAttachAsk: 'Great, attach it here.',
156
+ welcomeQuestion: "Shall we start? Send me your CV, or if it is on this computer, I'll look for it. With it I'll prepare a draft so you only have to confirm it.",
157
+ cvSearch: 'Look for it on my computer',
158
+ cvAttach: "I'll send it",
159
+ cvAttachAsk: 'Great, attach it here or tell me where it is on your computer.',
139
160
  cvNotFound: "I couldn't find your CV on this computer. Could you attach it here?",
140
161
  linkedinAsk: 'Could you send me your LinkedIn profile?',
141
162
  linkedinOpening: 'I am opening your LinkedIn profile in your browser so you can copy its address.',
@@ -160,18 +181,29 @@ const COPY = {
160
181
  existingAnnual: (from, to) => `Annual rate: ${from} → ${to}`,
161
182
  existingHours: (from, to) => `Availability: ${from} → ${to}`,
162
183
  existingEmpty: 'not set',
163
- existingStays: (label, kept, requested) => (kept ? `${label}: stays at ${kept}, not ${requested}` : `${label}: no change`),
184
+ existingUpdateQuestion: 'Shall I update your profile with this?',
185
+ existingStays: (label, kept, requested) => (kept ? `${label}: stays at ${kept}${kept === requested ? '' : `, not ${requested}`}` : `${label}: no change`),
164
186
  existingStaysNote: 'I do not change from here what you had already filled in: if you want other values, change them on the web.',
165
187
  existingLists: (sources) => `I add what is new from ${listOf('en', named(sources, COPY.en.importSources))} (experience, projects, education) and fill in what is empty, without deleting anything.`,
166
188
  existingYes: 'Yes, update it',
167
189
  existingNo: 'No, leave it as it is',
168
190
  existingKept: 'Sure, I am leaving your profile as it is.',
169
191
  keptLabels: { workSituation: 'Work situation', availability: 'Availability', languages: 'Languages', phone: 'Phone' },
192
+ situationLabels: { FREELANCE: 'Freelance', EMPLOYED: 'Employed', BETWEEN_JOBS: 'Between jobs', STUDYING: 'Studying', OTHER: 'Other' },
193
+ participationLabels: { FULL_TIME: 'full-time', PART_TIME: 'part-time' },
170
194
  existingUnchanged: (items) => `I did not change these details of your profile; they stay as they were: ${items.join(' · ')}. If you want other values, change them on the web.`,
171
- aiUsageQuestion: 'Do you want me to analyse how you work with AI? It is optional and takes 1-2 min. I only see which tools you use; your code and your prompts do not leave your machine. It helps you stand out for AI projects.',
195
+ aiUsageQuestion: 'Do you want me to analyse how you work with AI? It is optional and takes 1-2 min. I only see which tools you use; your code and your prompts do not leave your machine. I look at the AI usage across this whole machine, which I take to be yours. It helps you stand out for AI projects.',
172
196
  aiUsageYes: 'Yes, analyse it',
173
197
  aiUsageSkip: 'Skip this step',
174
198
  aiUsageSkipped: "No problem, we carry on without it. Heads-up: it is what weighs most for projects to reach you. You can run it whenever you like by telling me 'analyse my AI usage'.",
199
+ consent: {
200
+ yes: 'Yes, go ahead',
201
+ no: 'No',
202
+ cvSearch: 'Shakers will look on this computer for files that may be your CV: it lists the names of the PDF and Word files in your Desktop, Documents, Downloads and iCloud Drive folders, and reads your time zone and the name, email and GitHub user saved by your coding tools. It does not open those files and nothing is sent. Continue?',
203
+ cvRead: (file) => `Shakers will read the file ${file} to prepare your profile draft. Nothing is sent yet. Continue?`,
204
+ cvSend: (file) => `Shakers will send the file ${file} to your Shakers account to fill in your profile. Continue?`,
205
+ project: (p) => `Shakers will add the project in the folder ${p.folder} to your portfolio and send ${listOf('en', [`the name "${p.name}"`, p.url && `the web address ${p.url}`, p.startDate && p.endDate ? `the dates ${p.startDate} to ${p.endDate}` : (p.startDate || p.endDate) && `the date ${p.startDate || p.endDate}`, p.client && `the client ${p.client}`, p.description && 'the description you agreed on'].filter(Boolean))}. Continue?`,
206
+ },
175
207
  importSources: { linkedin: 'your LinkedIn', cv: 'your CV', github: 'your GitHub', web: 'your website' },
176
208
  importFailed: (names) => `I couldn't import ${names.join(' and ')} right now. The rest of your profile is saved; you can add what is missing on the web.`,
177
209
  interviewWhereQuestion: 'Shall we do the onboarding interview here now or later on the web?',
@@ -205,10 +237,10 @@ const COPY = {
205
237
  },
206
238
  it: {
207
239
  welcome: (name) => `Ciao${name ? `, ${name}` : ''}! Grazie per voler entrare in Shakers 🙌\n\nSiamo una community di professionisti tech freelance. Il nostro obiettivo è avere sempre la tua versione più aggiornata come professionista (cosa sai fare oggi, come lavori con l'IA e cosa cerchi) per metterti in contatto con progetti che fanno davvero per te.\n\nL'iscrizione richiede tre passaggi e la maggior parte la faccio io per te:\n1. Il tuo profilo (2 min). Lo compilo a partire dal tuo CV e da LinkedIn, tu devi solo rivederlo e confermarlo.\n2. Come lavori con l'IA (1-2 min, facoltativo). Analizzo quali strumenti usi su questo computer. Il tuo codice e i tuoi prompt non escono mai dalla tua macchina.\n3. Un breve colloquio (facoltativo). Per definire meglio il tuo ruolo e cosa cerchi. Lo fai sul sito, quando vuoi.\n\nPuoi modificare tutto in seguito dal sito.`,
208
- welcomeQuestion: 'Iniziamo? Se mi dai il permesso, cerco il tuo CV su questo computer e ti preparo una bozza da confermare e basta.',
209
- cvSearch: 'Sì, cercalo',
210
- cvAttach: 'Te lo allego io',
211
- cvAttachAsk: 'Perfetto, allegalo qui.',
240
+ welcomeQuestion: 'Iniziamo? Mandami il tuo CV, oppure, se ce l’hai su questo computer, lo cerco io. Con quello ti preparo una bozza da confermare e basta.',
241
+ cvSearch: 'Cercalo nel mio computer',
242
+ cvAttach: 'Te lo mando',
243
+ cvAttachAsk: 'Perfetto, allegalo qui o dimmi dove si trova nel tuo computer.',
212
244
  cvNotFound: 'Non ho trovato il tuo CV su questo computer. Me lo alleghi qui?',
213
245
  linkedinAsk: 'Mi mandi il tuo profilo LinkedIn?',
214
246
  linkedinOpening: "Ti apro il tuo profilo LinkedIn nel browser così puoi copiarne l'indirizzo.",
@@ -233,18 +265,29 @@ const COPY = {
233
265
  existingAnnual: (from, to) => `Tariffa annuale: ${from} → ${to}`,
234
266
  existingHours: (from, to) => `Disponibilità: ${from} → ${to}`,
235
267
  existingEmpty: 'non indicata',
236
- existingStays: (label, kept, requested) => (kept ? `${label}: resta ${kept}, non ${requested}` : `${label}: nessuna modifica`),
268
+ existingUpdateQuestion: 'Aggiorno il tuo profilo con questi dati?',
269
+ existingStays: (label, kept, requested) => (kept ? `${label}: resta ${kept}${kept === requested ? '' : `, non ${requested}`}` : `${label}: nessuna modifica`),
237
270
  existingStaysNote: 'Quello che avevi già compilato non lo cambio da qui: se vuoi altri valori, cambiali sul sito.',
238
271
  existingLists: (sources) => `Aggiungo le novità ${listOf('it', named(sources, { cv: 'del tuo CV', linkedin: 'del tuo LinkedIn' }))} (esperienze, progetti, formazione) e compilo quello che hai vuoto, senza cancellare nulla.`,
239
272
  existingYes: 'Sì, aggiornalo',
240
273
  existingNo: 'No, lascialo così',
241
274
  existingKept: 'Perfetto, lascio il tuo profilo così com’è.',
242
275
  keptLabels: { workSituation: 'Situazione lavorativa', availability: 'Disponibilità', languages: 'Lingue', phone: 'Telefono' },
276
+ situationLabels: { FREELANCE: 'Freelance', EMPLOYED: 'Dipendente', BETWEEN_JOBS: "Tra un lavoro e l'altro", STUDYING: 'Studente', OTHER: 'Altro' },
277
+ participationLabels: { FULL_TIME: 'tempo pieno', PART_TIME: 'part-time' },
243
278
  existingUnchanged: (items) => `Questi dati del tuo profilo non li ho cambiati, restano com'erano: ${items.join(' · ')}. Se vuoi altri valori, cambiali sul sito.`,
244
- aiUsageQuestion: "Vuoi che analizzi come lavori con l'IA? È facoltativo e richiede 1-2 min. Vedo solo quali strumenti usi, il tuo codice e i tuoi prompt non escono dalla tua macchina. Ti aiuta a distinguerti nei progetti di IA.",
279
+ aiUsageQuestion: "Vuoi che analizzi come lavori con l'IA? È facoltativo e richiede 1-2 min. Vedo solo quali strumenti usi, il tuo codice e i tuoi prompt non escono dalla tua macchina. Guardo l'uso dell'IA su tutto questo computer, che considero tuo. Ti aiuta a distinguerti nei progetti di IA.",
245
280
  aiUsageYes: 'Sì, analizzalo',
246
281
  aiUsageSkip: 'Salta questo passaggio',
247
282
  aiUsageSkipped: "Nessun problema, andiamo avanti senza. Ti avviso che è ciò che pesa di più perché ti arrivino progetti: puoi lanciarlo quando vuoi dicendomi 'analizza il mio uso dell'IA'.",
283
+ consent: {
284
+ yes: 'Sì, procedi',
285
+ no: 'No',
286
+ cvSearch: "Shakers cercherà su questo computer i file che potrebbero essere il tuo CV: guarda i nomi dei file PDF e Word nelle cartelle Scrivania, Documenti, Download e iCloud Drive, e legge il tuo fuso orario e il nome, l'email e l'utente GitHub salvati dai tuoi strumenti di programmazione. Non apre quei file e non invia nulla. Continuiamo?",
287
+ cvRead: (file) => `Shakers leggerà il file ${file} per preparare la bozza del tuo profilo. Per ora non viene inviato nulla. Continuiamo?`,
288
+ cvSend: (file) => `Shakers invierà il file ${file} al tuo account Shakers per completare il tuo profilo. Continuiamo?`,
289
+ project: (p) => `Shakers aggiungerà il progetto della cartella ${p.folder} al tuo portfolio e invierà ${listOf('it', [`il nome "${p.name}"`, p.url && `l'indirizzo web ${p.url}`, p.startDate && p.endDate ? `le date dal ${p.startDate} al ${p.endDate}` : (p.startDate || p.endDate) && `la data ${p.startDate || p.endDate}`, p.client && `il cliente ${p.client}`, p.description && 'la descrizione che hai concordato'].filter(Boolean))}. Continuiamo?`,
290
+ },
248
291
  importSources: { linkedin: 'il tuo LinkedIn', cv: 'il tuo CV', github: 'il tuo GitHub', web: 'il tuo sito' },
249
292
  importFailed: (names) => `Non sono riuscito a importare ${names.join(' e ')} in questo momento. Il resto del tuo profilo è salvato; quello che manca puoi aggiungerlo sul sito.`,
250
293
  interviewWhereQuestion: 'Facciamo il colloquio di onboarding qui adesso o più tardi sul sito?',
@@ -278,10 +321,10 @@ const COPY = {
278
321
  },
279
322
  pt: {
280
323
  welcome: (name) => `Olá${name ? `, ${name}` : ''}! Obrigado por querer juntar-se à Shakers 🙌\n\nSomos uma comunidade de profissionais tech freelance. O nosso objetivo é ter sempre a sua versão mais atual como profissional (o que sabe fazer hoje, como trabalha com IA e o que procura) para o pôr em contacto com projetos que encaixem mesmo consigo.\n\nO registo tem três passos e a maior parte faço eu por si:\n1. O seu perfil (2 min). Preencho-o a partir do seu CV e do LinkedIn; só tem de rever e confirmar.\n2. Como trabalha com IA (1-2 min, opcional). Analiso que ferramentas usa neste computador. O seu código e os seus prompts nunca saem da sua máquina.\n3. Uma breve entrevista (opcional). Para afinar o seu perfil e o que procura. Faz-se na web, quando quiser.\n\nPode editar tudo depois na web.`,
281
- welcomeQuestion: 'Começamos? Se me der autorização, procuro o seu CV neste computador e preparo um rascunho para que só tenha de o confirmar.',
282
- cvSearch: 'Sim, procure-o',
283
- cvAttach: 'Eu anexo-o',
284
- cvAttachAsk: 'Perfeito, anexe-o aqui.',
324
+ welcomeQuestion: 'Começamos? Envie-me o seu CV ou, se o tiver neste computador, procuro-o eu. Com ele preparo um rascunho para que só tenha de o confirmar.',
325
+ cvSearch: 'Procure-o no meu computador',
326
+ cvAttach: 'Envio-o eu',
327
+ cvAttachAsk: 'Perfeito, anexe-o aqui ou diga-me onde está no seu computador.',
285
328
  cvNotFound: 'Não encontrei o seu CV neste computador. Pode anexá-lo aqui?',
286
329
  linkedinAsk: 'Pode enviar-me o seu perfil do LinkedIn?',
287
330
  linkedinOpening: 'Vou abrir o seu perfil do LinkedIn no navegador para que copie o endereço.',
@@ -306,18 +349,29 @@ const COPY = {
306
349
  existingAnnual: (from, to) => `Tarifa anual: ${from} → ${to}`,
307
350
  existingHours: (from, to) => `Disponibilidade: ${from} → ${to}`,
308
351
  existingEmpty: 'sem indicar',
309
- existingStays: (label, kept, requested) => (kept ? `${label}: fica em ${kept}, não ${requested}` : `${label}: sem alterações`),
352
+ existingUpdateQuestion: 'Atualizo o seu perfil com isto?',
353
+ existingStays: (label, kept, requested) => (kept ? `${label}: fica em ${kept}${kept === requested ? '' : `, não ${requested}`}` : `${label}: sem alterações`),
310
354
  existingStaysNote: 'O que já tinha preenchido não o altero daqui: se quiser outros valores, altere-os na web.',
311
355
  existingLists: (sources) => `Acrescento o que há de novo ${listOf('pt', named(sources, { cv: 'no seu CV', linkedin: 'no seu LinkedIn' }))} (experiências, projetos, formação) e preencho o que tiver vazio, sem apagar nada.`,
312
356
  existingYes: 'Sim, atualize',
313
357
  existingNo: 'Não, deixe como está',
314
358
  existingKept: 'Perfeito, deixo o seu perfil como está.',
315
359
  keptLabels: { workSituation: 'Situação profissional', availability: 'Disponibilidade', languages: 'Idiomas', phone: 'Telefone' },
360
+ situationLabels: { FREELANCE: 'Freelancer', EMPLOYED: 'Por conta de outrem', BETWEEN_JOBS: 'Entre trabalhos', STUDYING: 'A estudar', OTHER: 'Outra' },
361
+ participationLabels: { FULL_TIME: 'tempo inteiro', PART_TIME: 'tempo parcial' },
316
362
  existingUnchanged: (items) => `Estes dados do seu perfil não os alterei, ficam como estavam: ${items.join(' · ')}. Se quiser outros valores, altere-os na web.`,
317
- aiUsageQuestion: 'Quer que analise como trabalha com IA? É opcional e demora 1-2 min. Só vejo que ferramentas usa; o seu código e os seus prompts não saem da sua máquina. Ajuda-o a destacar-se em projetos de IA.',
363
+ aiUsageQuestion: 'Quer que analise como trabalha com IA? É opcional e demora 1-2 min. Só vejo que ferramentas usa; o seu código e os seus prompts não saem da sua máquina. Analiso o uso de IA em todo este computador, que considero seu. Ajuda-o a destacar-se em projetos de IA.',
318
364
  aiUsageYes: 'Sim, analise',
319
365
  aiUsageSkip: 'Saltar este passo',
320
366
  aiUsageSkipped: "Sem problema, seguimos sem ele. Aviso-o de que é o que mais pesa para lhe chegarem projetos: pode lançá-lo quando quiser dizendo-me 'analisa o meu uso de IA'.",
367
+ consent: {
368
+ yes: 'Sim, avançar',
369
+ no: 'Não',
370
+ cvSearch: 'A Shakers vai procurar neste computador ficheiros que possam ser o seu CV: vê os nomes dos ficheiros PDF e Word das pastas Secretária, Documentos, Transferências e iCloud Drive, e lê o seu fuso horário e o nome, o email e o utilizador de GitHub guardados pelas suas ferramentas de programação. Não abre esses ficheiros e não envia nada. Continuamos?',
371
+ cvRead: (file) => `A Shakers vai ler o ficheiro ${file} para preparar o rascunho do seu perfil. Ainda não é enviado nada. Continuamos?`,
372
+ cvSend: (file) => `A Shakers vai enviar o ficheiro ${file} para a sua conta Shakers para completar o seu perfil. Continuamos?`,
373
+ project: (p) => `A Shakers vai adicionar o projeto da pasta ${p.folder} ao seu portfólio e enviar ${listOf('pt', [`o nome "${p.name}"`, p.url && `o endereço web ${p.url}`, p.startDate && p.endDate ? `as datas de ${p.startDate} a ${p.endDate}` : (p.startDate || p.endDate) && `a data ${p.startDate || p.endDate}`, p.client && `o cliente ${p.client}`, p.description && 'a descrição que combinou'].filter(Boolean))}. Continuamos?`,
374
+ },
321
375
  importSources: { linkedin: 'o seu LinkedIn', cv: 'o seu CV', github: 'o seu GitHub', web: 'o seu site' },
322
376
  importFailed: (names) => `Não consegui importar ${names.join(' e ')} neste momento. O resto do seu perfil está guardado; o que faltar pode acrescentá-lo na web.`,
323
377
  interviewWhereQuestion: 'Fazemos a entrevista de onboarding aqui agora ou mais tarde na web?',
@@ -368,8 +422,12 @@ function legalCopy(lang) {
368
422
  function renderDraft(lang, d = {}) {
369
423
  const c = signupCopy(lang);
370
424
  const has = (v) => v !== null && v !== undefined && String(v).trim() !== '';
425
+ const place = [d.city, countryName(lang, d.country)].filter(has).join(', ');
426
+ const situation = c.situationLabels[d.situation];
427
+ const participation = d.situation === 'EMPLOYED' ? c.participationLabels[d.participation] : null;
371
428
  const lines = [
372
- joinParts([d.name, d.role, d.city]),
429
+ joinParts([d.name, d.role, place]),
430
+ situation ? `${c.keptLabels.workSituation}: ${participation ? `${situation} (${participation})` : situation}` : '',
373
431
  joinParts([has(d.yearsOfExperience) ? c.years(d.yearsOfExperience) : null, d.stack]),
374
432
  joinParts([d.languages, d.workMode, has(d.monthlyHours) ? c.hours(d.monthlyHours) : null]),
375
433
  has(d.hourlyRate) || has(d.annualRate) ? c.rates(has(d.hourlyRate) ? d.hourlyRate : null, has(d.annualRate) ? d.annualRate : null, d.ratesFromTalent !== true) : '',
@@ -114,6 +114,14 @@ function printActivity(report, t, p) {
114
114
  ` ${c.success}●${c.reset} ${a.commits(g.authoredCommitCount, g.commitCount)} ${c.muted}·${c.reset} ${a.lines(g.linesAdded, g.linesDeleted)} ${c.muted}·${c.reset} ${a.velocity(g.velocity)}`,
115
115
  );
116
116
  }
117
+ // Transparency warning: the talent's own commits are going unattributed because
118
+ // the repo's git identity is not their Shakers email (display-only, never scored).
119
+ const mm = report && report.authorshipMismatch;
120
+ if (mm && Array.isArray(mm.emails) && mm.emails.length && mm.accountEmail) {
121
+ for (const line of wrap(a.authorshipMismatch(mm.emails, mm.accountEmail))) {
122
+ p(` ${c.warning}${line}${c.reset}`);
123
+ }
124
+ }
117
125
  if (w) {
118
126
  p(
119
127
  ` ${c.success}●${c.reset} ${a.workStreams(w.streamCount, w.multiDayStreams, w.maxStreamSpanDays)}`,
@@ -30,6 +30,7 @@ const {
30
30
  collectScopedWorkStreams,
31
31
  collectScopedDetectionEvidence,
32
32
  } = require('./scope-signals');
33
+ const { detectAuthorshipMismatch } = require('./authorship-mismatch');
33
34
  const { MODEL_CALL, REASON, skipped, attachModelCall } = require('./model-call-record');
34
35
  const {
35
36
  getSynthesisEndpoint,
@@ -164,6 +165,8 @@ async function resolveAiConsent({ opts, catalog, gate, injectedAsk }) {
164
165
  function runAggregation({ report, scope, consentEmail, agentRoots, root, catalog }) {
165
166
  withStaticStatus(catalog.cli.aggregatingLabel, () => {
166
167
  report.gitActivity = collectScopedGitActivity(scope.toplevels, consentEmail);
168
+ // Display-only transparency warning (never feeds scoring, see authorship-mismatch.js).
169
+ report.authorshipMismatch = detectAuthorshipMismatch(scope.toplevels, consentEmail, report.gitActivity);
167
170
  const sessionSignals = collectSessionSignals(process.env, scope.selectedCwds);
168
171
  report.sessions = sessionSignals.sessions;
169
172
  report.workStreams = collectScopedWorkStreams(scope.toplevels);
@@ -1,50 +0,0 @@
1
- 'use strict';
2
-
3
- // MCP: ask Alma. Returns the AGGREGATED answer (MCP does not stream).
4
-
5
- const ASK_SCHEMA = {
6
- type: 'object',
7
- properties: {
8
- message: { type: 'string', description: 'The question / message for Alma.' },
9
- },
10
- required: ['message'],
11
- };
12
-
13
- function makeAskTools(deps = {}) {
14
- const {
15
- askAlma = (opts) => require('./alma-client').askAlma({}, opts),
16
- loadAuthSession = require('./auth-session-store').loadAuthSession,
17
- sessionStatus = require('./auth-session-store').sessionStatus,
18
- } = deps;
19
-
20
- const requireLoginSession = () => {
21
- const session = loadAuthSession();
22
- if (sessionStatus(session) !== 'active') {
23
- throw new Error('no active Shakers session — call the login tool (or run `shakers login`) first (ask is session-gated).');
24
- }
25
- return session;
26
- };
27
-
28
- async function ask(args = {}) {
29
- const session = requireLoginSession();
30
- const message = typeof args.message === 'string' ? args.message.trim() : '';
31
- if (!message) return { ok: false, reason: 'no-message' };
32
- const res = await askAlma({ hubAccessToken: session.hubAccessToken, message });
33
- if (!res.ok) return { ok: false, reason: res.reason };
34
- return { ok: true, answer: res.text };
35
- }
36
-
37
- return [
38
- {
39
- name: 'ask',
40
- description: 'Ask Alma, the Shakers AI assistant, a question (about your profile, rates, availability, skills, open positions, applications). Returns Alma\'s full answer — relay it as-is; do not surface any internal Shakers ids/codes to the talent.',
41
- inputSchema: ASK_SCHEMA,
42
- handler: ask,
43
- },
44
- ];
45
- }
46
-
47
- module.exports = {
48
- makeAskTools,
49
- ASK_SCHEMA,
50
- };