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.
- package/README.md +28 -3
- package/bin/mcp.js +13 -7
- package/bin/register.js +5 -2
- package/package.json +1 -1
- package/src/authorship-mismatch.js +62 -0
- package/src/backend-request.js +12 -8
- package/src/config.js +27 -6
- package/src/consent-flow.js +10 -10
- package/src/i18n.js +16 -12
- package/src/mcp-ai-usage-result-tool.js +4 -2
- package/src/mcp-ai-usage-tool.js +3 -1
- package/src/mcp-alma-tools.js +7 -3
- package/src/mcp-applications-tools.js +4 -2
- package/src/mcp-auth-form.js +39 -10
- package/src/mcp-availability-tools.js +4 -2
- package/src/mcp-certifications-tools.js +4 -2
- package/src/mcp-certify-dimension-tools.js +4 -2
- package/src/mcp-choice.js +30 -4
- package/src/mcp-device-login-tools.js +6 -2
- package/src/mcp-find-projects-tools.js +4 -2
- package/src/mcp-instructions.js +6 -6
- package/src/mcp-invitations-tools.js +4 -2
- package/src/mcp-lang-tools.js +4 -2
- package/src/mcp-onboarding-tools.js +41 -44
- package/src/mcp-portfolios-tools.js +7 -3
- package/src/mcp-profile-tools.js +4 -2
- package/src/mcp-prompts.js +3 -3
- package/src/mcp-rate-tools.js +4 -2
- package/src/mcp-register-tools.js +53 -20
- package/src/mcp-report-tools.js +7 -3
- package/src/mcp-role-tools.js +36 -13
- package/src/mcp-saved-positions-tools.js +7 -3
- package/src/mcp-server.js +17 -4
- package/src/mcp-show-project-tools.js +4 -2
- package/src/mcp-signup-tools.js +229 -62
- package/src/mcp-socials-tools.js +4 -2
- package/src/onboarding-flow.js +112 -13
- package/src/onboarding-funnel.js +272 -0
- package/src/profile-client.js +11 -0
- package/src/share.js +4 -3
- package/src/signup-copy.js +84 -26
- package/src/terminal-agents.js +8 -0
- package/src/usage-run-steps.js +3 -0
- package/src/mcp-ask-tools.js +0 -50
package/src/signup-copy.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
const SIGNUP_LANGUAGES = ['es', 'en', 'it', 'pt'];
|
|
6
6
|
|
|
7
|
-
// Legal texts.
|
|
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?
|
|
63
|
-
cvSearch: '
|
|
64
|
-
cvAttach: 'Te lo
|
|
65
|
-
cvAttachAsk: 'Perfecto,
|
|
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
|
-
|
|
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?
|
|
136
|
-
cvSearch: '
|
|
137
|
-
cvAttach: "I'll
|
|
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
|
-
|
|
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?
|
|
209
|
-
cvSearch: '
|
|
210
|
-
cvAttach: 'Te lo
|
|
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
|
-
|
|
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?
|
|
282
|
-
cvSearch: '
|
|
283
|
-
cvAttach: '
|
|
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
|
-
|
|
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,
|
|
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) : '',
|
package/src/terminal-agents.js
CHANGED
|
@@ -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)}`,
|
package/src/usage-run-steps.js
CHANGED
|
@@ -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);
|
package/src/mcp-ask-tools.js
DELETED
|
@@ -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
|
-
};
|