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.
@@ -56,6 +56,7 @@ const TRUST = {
56
56
  list_addable: 'lee de tu cuenta de Shakers las skills y agentes de IA encontrados en el análisis que enviaste. No cambia nada.',
57
57
  add_skill: 'añade a tu perfil de Shakers la skill que elijas. No lee nada de este ordenador.',
58
58
  add_agent: 'añade a tu perfil de Shakers el agente de IA que elijas, con la descripción que acordéis. No lee nada más de este ordenador.',
59
+ add_detected_agents: 'añade a tu perfil de Shakers los agentes de IA detectados que elijas, solo su nombre y su descripción, y los clientes los verán en tu perfil. No lee ni sube nada más de este ordenador. Puedes quitarlos cuando quieras desde tu perfil en la web de Shakers.',
59
60
  add_project: 'te pregunta antes y, si dices que sí, lee el nombre de la carpeta del proyecto, la dirección web de su código y las fechas de su primer y último cambio, y los envía a tu portfolio de Shakers. No envía tu código.',
60
61
  suggest_register_context: 'te pregunta antes y, si dices que sí, mira los nombres de los PDF y Word de Escritorio, Documentos, Descargas e iCloud Drive que parezcan un CV, tu zona horaria y el nombre, email y usuario de GitHub de tus herramientas de programación. No abre esos archivos ni envía nada.',
61
62
  read_cv: 'te pregunta antes y, si dices que sí, lee el texto del archivo de CV que permitas para preparar el borrador de tu perfil. No envía nada a Shakers.',
@@ -86,6 +87,8 @@ const TRUST = {
86
87
  list_certifiable_dimensions: 'lee de Shakers qué áreas de tu rol principal puedes certificar. No cambia nada.',
87
88
  find_projects: 'lee de Shakers los proyectos abiertos que mejor encajan contigo. No cambia nada.',
88
89
  show_project: 'lee de Shakers el detalle de un proyecto. No cambia nada.',
90
+ prepare_application: 'lee de Shakers las preguntas de la candidatura a un proyecto. No envía ni cambia nada.',
91
+ apply_to_project: 'envía a Shakers tu mensaje y tus respuestas para el proyecto que elijas, solo después de enseñarte el resumen y de que digas que sí: 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 cambia tu perfil.',
89
92
  save_project: 'guarda ese proyecto en tus proyectos guardados de Shakers.',
90
93
  unsave_project: 'quita ese proyecto de tus proyectos guardados de Shakers.',
91
94
  list_invitations: 'lee tus invitaciones a proyectos en Shakers. No cambia nada.',
@@ -113,6 +116,7 @@ const TRUST = {
113
116
  list_addable: 'reads from your Shakers account the skills and AI agents found in the analysis you sent. It changes nothing.',
114
117
  add_skill: 'adds the skill you pick to your Shakers profile. It reads nothing on this computer.',
115
118
  add_agent: 'adds the AI agent you pick, with the description you agree on, to your Shakers profile. It reads nothing else on this computer.',
119
+ add_detected_agents: 'adds the detected AI agents you pick to your Shakers profile, only their name and description, and clients will see them on your profile. It reads or uploads nothing else from this computer. You can remove them any time from your profile on the Shakers website.',
116
120
  add_project: 'asks you first and, if you say yes, reads the project folder name, the web address of its code and the dates of its first and last change, and sends them to your Shakers portfolio. It does not send your code.',
117
121
  suggest_register_context: 'asks you first and, if you say yes, looks at the names of PDF and Word files in Desktop, Documents, Downloads and iCloud Drive that look like a CV, your time zone and the name, email and GitHub user saved by your coding tools. It does not open those files or send anything.',
118
122
  read_cv: 'asks you first and, if you say yes, reads the text of the CV file you allow to prepare your profile draft. It sends nothing to Shakers.',
@@ -143,6 +147,8 @@ const TRUST = {
143
147
  list_certifiable_dimensions: 'reads from Shakers which areas of your main role you can certify. It changes nothing.',
144
148
  find_projects: 'reads from Shakers the open projects that fit you best. It changes nothing.',
145
149
  show_project: "reads a project's details from Shakers. It changes nothing.",
150
+ prepare_application: "reads the application questions of a project from Shakers. It sends or changes nothing.",
151
+ apply_to_project: 'sends your message and your answers for the project you pick to Shakers, only after showing you the summary and you saying yes: 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. It does not change your profile.',
146
152
  save_project: 'saves that project to your saved projects on Shakers.',
147
153
  unsave_project: 'removes that project from your saved projects on Shakers.',
148
154
  list_invitations: 'reads your project invitations on Shakers. It changes nothing.',
@@ -170,6 +176,7 @@ const TRUST = {
170
176
  list_addable: "legge dal tuo account Shakers le skill e gli agenti di IA trovati nell'analisi che hai inviato. Non cambia nulla.",
171
177
  add_skill: 'aggiunge al tuo profilo Shakers la skill che scegli. Non legge nulla di questo computer.',
172
178
  add_agent: "aggiunge al tuo profilo Shakers l'agente di IA che scegli, con la descrizione concordata. Non legge nient'altro di questo computer.",
179
+ add_detected_agents: "aggiunge al tuo profilo Shakers gli agenti di IA rilevati che scegli, solo il nome e la descrizione, e i clienti li vedranno nel tuo profilo. Non legge né carica nient'altro da questo computer. Puoi toglierli quando vuoi dal tuo profilo sul sito di Shakers.",
173
180
  add_project: "ti chiede prima e, se dici di sì, legge il nome della cartella del progetto, l'indirizzo web del suo codice e le date della prima e dell'ultima modifica, e li invia al tuo portfolio Shakers. Non invia il tuo codice.",
174
181
  suggest_register_context: "ti chiede prima e, se dici di sì, guarda i nomi dei PDF e Word in Scrivania, Documenti, Download e iCloud Drive che sembrano un CV, il tuo fuso orario e il nome, l'email e l'utente GitHub dei tuoi strumenti di programmazione. Non apre quei file e non invia nulla.",
175
182
  read_cv: 'ti chiede prima e, se dici di sì, legge il testo del CV che permetti per preparare la bozza del tuo profilo. Non invia nulla a Shakers.',
@@ -200,6 +207,8 @@ const TRUST = {
200
207
  list_certifiable_dimensions: 'legge da Shakers quali aree del tuo ruolo principale puoi certificare. Non cambia nulla.',
201
208
  find_projects: 'legge da Shakers i progetti aperti più adatti a te. Non cambia nulla.',
202
209
  show_project: 'legge da Shakers il dettaglio di un progetto. Non cambia nulla.',
210
+ prepare_application: 'legge da Shakers le domande della candidatura a un progetto. Non invia né cambia nulla.',
211
+ apply_to_project: "invia a Shakers il tuo messaggio e le tue risposte per il progetto che scegli, solo dopo averti mostrato il riepilogo e che tu abbia detto sì: il cliente vedrà il tuo profilo Shakers e si apre una chat con il team. Se il progetto lo richiede, Shakers rivede la tua candidatura prima che la veda il cliente. Non cambia il tuo profilo.",
203
212
  save_project: 'salva quel progetto tra i tuoi progetti salvati su Shakers.',
204
213
  unsave_project: 'toglie quel progetto dai tuoi progetti salvati su Shakers.',
205
214
  list_invitations: 'legge i tuoi inviti a progetti su Shakers. Non cambia nulla.',
@@ -227,6 +236,7 @@ const TRUST = {
227
236
  list_addable: 'lê da sua conta Shakers as skills e os agentes de IA encontrados na análise que enviou. Não altera nada.',
228
237
  add_skill: 'adiciona ao seu perfil Shakers a skill que escolher. Não lê nada deste computador.',
229
238
  add_agent: 'adiciona ao seu perfil Shakers o agente de IA que escolher, com a descrição combinada. Não lê mais nada deste computador.',
239
+ add_detected_agents: 'adiciona ao seu perfil Shakers os agentes de IA detetados que escolher, apenas o nome e a descrição, e os clientes vão vê-los no seu perfil. Não lê nem envia mais nada deste computador. Pode removê-los quando quiser a partir do seu perfil na web da Shakers.',
230
240
  add_project: 'pergunta-lhe antes e, se disser que sim, lê o nome da pasta do projeto, o endereço web do código e as datas da primeira e da última alteração, e envia-os para o seu portfolio Shakers. Não envia o seu código.',
231
241
  suggest_register_context: 'pergunta-lhe antes e, se disser que sim, vê os nomes dos PDF e Word em Secretária, Documentos, Transferências e iCloud Drive que pareçam um CV, o seu fuso horário e o nome, email e utilizador GitHub das suas ferramentas de programação. Não abre esses ficheiros nem envia nada.',
232
242
  read_cv: 'pergunta-lhe antes e, se disser que sim, lê o texto do CV que autorizar para preparar o rascunho do seu perfil. Não envia nada para a Shakers.',
@@ -257,6 +267,8 @@ const TRUST = {
257
267
  list_certifiable_dimensions: 'lê da Shakers que áreas da sua função principal pode certificar. Não altera nada.',
258
268
  find_projects: 'lê da Shakers os projetos abertos que mais se ajustam a si. Não altera nada.',
259
269
  show_project: 'lê da Shakers o detalhe de um projeto. Não altera nada.',
270
+ prepare_application: 'lê da Shakers as perguntas da candidatura a um projeto. Não envia nem altera nada.',
271
+ apply_to_project: 'envia à Shakers a sua mensagem e as suas respostas para o projeto que escolher, só depois de lhe mostrar o resumo e de dizer que sim: o cliente verá o seu perfil Shakers e abre-se um chat com a equipa. Se o projeto o pedir, a Shakers revê a sua candidatura antes de o cliente a ver. Não altera o seu perfil.',
260
272
  save_project: 'guarda esse projeto nos seus projetos guardados na Shakers.',
261
273
  unsave_project: 'retira esse projeto dos seus projetos guardados na Shakers.',
262
274
  list_invitations: 'lê os seus convites para projetos na Shakers. Não altera nada.',
@@ -888,6 +900,112 @@ function toSignupLanguage(code) {
888
900
  return SIGNUP_LANGUAGES.includes(base) ? base : null;
889
901
  }
890
902
 
903
+ const DETECTED_AGENTS = {
904
+ es: {
905
+ intro: 'Estos agentes se detectaron en tu análisis y aún no están en tu perfil de Shakers. Los que elijas se añaden con su nombre y su descripción, y los clientes los verán.',
906
+ question: 'Responde con los números de los que quieres añadir (por ejemplo «1 3»), «todos» o «ninguno».',
907
+ allOnProfile: 'Todos los agentes detectados ya están en tu perfil.',
908
+ noneChosen: 'No se ha añadido ningún agente a tu perfil.',
909
+ added: (names) => `Añadidos a tu perfil: ${names}. Puedes quitarlos cuando quieras desde tu perfil en la web de Shakers.`,
910
+ failed: (names) => `No se pudieron añadir: ${names}.`,
911
+ },
912
+ en: {
913
+ intro: 'These agents were detected in your analysis and are not on your Shakers profile yet. The ones you pick are added with their name and description, and clients will see them.',
914
+ question: 'Reply with the numbers of the ones you want to add (for example "1 3"), "all" or "none".',
915
+ allOnProfile: 'Every detected agent is already on your profile.',
916
+ noneChosen: 'No agent was added to your profile.',
917
+ added: (names) => `Added to your profile: ${names}. You can remove them any time from your profile on the Shakers website.`,
918
+ failed: (names) => `Could not be added: ${names}.`,
919
+ },
920
+ it: {
921
+ intro: 'Questi agenti sono stati rilevati nella tua analisi e non sono ancora nel tuo profilo Shakers. Quelli che scegli vengono aggiunti con nome e descrizione, e i clienti li vedranno.',
922
+ question: 'Rispondi con i numeri di quelli che vuoi aggiungere (per esempio «1 3»), «tutti» o «nessuno».',
923
+ allOnProfile: 'Tutti gli agenti rilevati sono già nel tuo profilo.',
924
+ noneChosen: 'Nessun agente è stato aggiunto al tuo profilo.',
925
+ added: (names) => `Aggiunti al tuo profilo: ${names}. Puoi toglierli quando vuoi dal tuo profilo sul sito di Shakers.`,
926
+ failed: (names) => `Non è stato possibile aggiungere: ${names}.`,
927
+ },
928
+ pt: {
929
+ intro: 'Estes agentes foram detetados na sua análise e ainda não estão no seu perfil Shakers. Os que escolher são adicionados com o nome e a descrição, e os clientes vão vê-los.',
930
+ question: 'Responda com os números dos que quer adicionar (por exemplo «1 3»), «todos» ou «nenhum».',
931
+ allOnProfile: 'Todos os agentes detetados já estão no seu perfil.',
932
+ noneChosen: 'Nenhum agente foi adicionado ao seu perfil.',
933
+ added: (names) => `Adicionados ao seu perfil: ${names}. Pode removê-los quando quiser a partir do seu perfil na web da Shakers.`,
934
+ failed: (names) => `Não foi possível adicionar: ${names}.`,
935
+ },
936
+ };
937
+
938
+ const APPLY = {
939
+ es: {
940
+ untitled: 'Sin título',
941
+ mainMotivationFallback: '¿Cuál es tu principal motivación para aplicar?',
942
+ notOpen: 'Este proyecto no admite tu candidatura ahora mismo: ya aplicaste, está cerrado o no está disponible para ti.',
943
+ summaryTitle: 'Esto es lo que se enviará:',
944
+ titleLine: (title) => `Proyecto: ${title}`,
945
+ companyLine: (name) => `Empresa: ${name}`,
946
+ messageLine: (message) => `Mensaje: ${message}`,
947
+ empty: '(vacío)',
948
+ answerLine: (question, answer) => `${question}: ${answer}`,
949
+ 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.`,
950
+ reviewNotice: 'En este proyecto Shakers revisa tu candidatura antes de que la vea el cliente.',
951
+ confirmQuestion: '¿La envío? Responde sí o no.',
952
+ sent: 'Candidatura enviada. Puedes seguirla en tus candidaturas.',
953
+ sentPending: 'Candidatura enviada. Shakers la revisa antes de que la vea el cliente.',
954
+ alreadyApplied: (status) => (status ? `Ya aplicaste a este proyecto (estado: ${status}).` : 'Ya aplicaste a este proyecto.'),
955
+ },
956
+ en: {
957
+ untitled: 'Untitled',
958
+ mainMotivationFallback: "What's your main motivation to apply?",
959
+ notOpen: 'This project does not take your application right now: you already applied, it is closed or it is not available to you.',
960
+ summaryTitle: 'This is what will be sent:',
961
+ titleLine: (title) => `Project: ${title}`,
962
+ companyLine: (name) => `Company: ${name}`,
963
+ messageLine: (message) => `Message: ${message}`,
964
+ empty: '(empty)',
965
+ answerLine: (question, answer) => `${question}: ${answer}`,
966
+ 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.`,
967
+ reviewNotice: 'On this project Shakers reviews your application before the client sees it.',
968
+ confirmQuestion: 'Shall I send it? Answer yes or no.',
969
+ sent: 'Application sent. You can follow it in your applications.',
970
+ sentPending: 'Application sent. Shakers reviews it before the client sees it.',
971
+ alreadyApplied: (status) => (status ? `You already applied to this project (status: ${status}).` : 'You already applied to this project.'),
972
+ },
973
+ it: {
974
+ untitled: 'Senza titolo',
975
+ mainMotivationFallback: 'Qual è la tua principale motivazione per candidarti?',
976
+ notOpen: 'Questo progetto non accetta la tua candidatura in questo momento: ti sei già candidato, è chiuso o non è disponibile per te.',
977
+ summaryTitle: 'Ecco cosa verrà inviato:',
978
+ titleLine: (title) => `Progetto: ${title}`,
979
+ companyLine: (name) => `Azienda: ${name}`,
980
+ messageLine: (message) => `Messaggio: ${message}`,
981
+ empty: '(vuoto)',
982
+ answerLine: (question, answer) => `${question}: ${answer}`,
983
+ sendNotice: (title) => `Vengono inviati il tuo messaggio e le tue risposte per «${title}»: il cliente vedrà il tuo profilo Shakers e si apre una chat con il team. Il tuo profilo non cambia.`,
984
+ reviewNotice: 'In questo progetto Shakers rivede la tua candidatura prima che la veda il cliente.',
985
+ confirmQuestion: 'La invio? Rispondi sì o no.',
986
+ sent: 'Candidatura inviata. Puoi seguirla tra le tue candidature.',
987
+ sentPending: 'Candidatura inviata. Shakers la rivede prima che la veda il cliente.',
988
+ alreadyApplied: (status) => (status ? `Ti sei già candidato a questo progetto (stato: ${status}).` : 'Ti sei già candidato a questo progetto.'),
989
+ },
990
+ pt: {
991
+ untitled: 'Sem título',
992
+ mainMotivationFallback: 'Qual é a sua principal motivação para se candidatar?',
993
+ notOpen: 'Este projeto não aceita a sua candidatura neste momento: já se candidatou, está fechado ou não está disponível para si.',
994
+ summaryTitle: 'Isto é o que será enviado:',
995
+ titleLine: (title) => `Projeto: ${title}`,
996
+ companyLine: (name) => `Empresa: ${name}`,
997
+ messageLine: (message) => `Mensagem: ${message}`,
998
+ empty: '(vazio)',
999
+ answerLine: (question, answer) => `${question}: ${answer}`,
1000
+ sendNotice: (title) => `São enviadas a sua mensagem e as suas respostas para «${title}»: o cliente verá o seu perfil Shakers e abre-se um chat com a equipa. O seu perfil não muda.`,
1001
+ reviewNotice: 'Neste projeto a Shakers revê a sua candidatura antes de o cliente a ver.',
1002
+ confirmQuestion: 'Envio-a? Responda sim ou não.',
1003
+ sent: 'Candidatura enviada. Pode acompanhá-la nas suas candidaturas.',
1004
+ sentPending: 'Candidatura enviada. A Shakers revê-a antes de o cliente a ver.',
1005
+ alreadyApplied: (status) => (status ? `Já se candidatou a este projeto (estado: ${status}).` : 'Já se candidatou a este projeto.'),
1006
+ },
1007
+ };
1008
+
891
1009
  function signupCopy(lang) {
892
1010
  return COPY[toSignupLanguage(lang) || 'en'];
893
1011
  }
@@ -904,6 +1022,14 @@ function reviewCopy(lang) {
904
1022
  return REVIEW[toSignupLanguage(lang) || 'en'];
905
1023
  }
906
1024
 
1025
+ function applyCopy(lang) {
1026
+ return APPLY[toSignupLanguage(lang) || 'en'];
1027
+ }
1028
+
1029
+ function detectedAgentsCopy(lang) {
1030
+ return DETECTED_AGENTS[toSignupLanguage(lang) || 'en'];
1031
+ }
1032
+
907
1033
  // The profile draft the talent confirms before the account exists, with every field the account is created with: one line per group, empty parts left out.
908
1034
  function renderDraft(lang, d = {}) {
909
1035
  const c = signupCopy(lang);
@@ -934,4 +1060,4 @@ function renderRole(lang, { name, source, evidence, openProjects, recommended })
934
1060
  ].filter(Boolean).join('\n');
935
1061
  }
936
1062
 
937
- module.exports = { SIGNUP_LANGUAGES, LEGAL, REVIEW, signupCopy, legalCopy, trustCopy, termsText, reviewCopy, toSignupLanguage, renderDraft, renderRole, amount };
1063
+ module.exports = { SIGNUP_LANGUAGES, LEGAL, REVIEW, signupCopy, legalCopy, trustCopy, termsText, reviewCopy, detectedAgentsCopy, applyCopy, toSignupLanguage, renderDraft, renderRole, amount };
@@ -16,6 +16,9 @@ const { requestDiscoveredInventory } = require('./inventory-client');
16
16
  const { ensureFreshSession } = require('./session-refresh');
17
17
  const { confirmDisclaimerAcceptance } = require('./certify-disclaimer');
18
18
  const { runInteractiveMultiSelect } = require('./interactive-select');
19
+ const { promptMultiSelect } = require('./prompt-select');
20
+ const { detectedAgentsFromReport, newDetectedAgents, declareDetectedAgents } = require('./detected-agents');
21
+ const { sanitizeRenderText } = require('./sanitize-network-text');
19
22
  const { loadAuthSession, sessionStatus } = require('./auth-session-store');
20
23
  const { withSpinner } = require('./terminal-progress');
21
24
  const { reasonCopy, pickPortfolios } = require('./start-add-skills');
@@ -162,8 +165,88 @@ async function offerRelateAgentToPortfolios({
162
165
  }
163
166
  }
164
167
 
168
+ async function confirmDetectedAgents({ ask, agents, c, input, output }) {
169
+ const chosen = await promptMultiSelect({
170
+ ask,
171
+ stdinIsTTY: true,
172
+ out: (line) => process.stdout.write(` ${line}\n`),
173
+ items: agents,
174
+ labelFor: (a) => sanitizeRenderText(a.name),
175
+ header: c.detectedAgentsHeading,
176
+ hint: c.detectedAgentsHint,
177
+ initialMarked: agents.map((_, i) => i),
178
+ ...(input ? { input } : {}),
179
+ ...(output ? { output } : {}),
180
+ });
181
+ if (typeof ask.drain === 'function') ask.drain();
182
+ return chosen;
183
+ }
184
+
185
+ async function addDetectedAgents({
186
+ ask, catalog, session, stdinIsTTY, yes = false, agents, input, output, deps = {},
187
+ }) {
188
+ const c = catalog.start;
189
+ if (!stdinIsTTY && !yes) {
190
+ process.stdout.write(`\n ${c.detectedAgentsNeedsYes(agents.length)}\n`);
191
+ return { ok: false, reason: 'non-interactive' };
192
+ }
193
+ const endpoint = getAgentsDeclareEndpoint();
194
+ if (!endpoint) {
195
+ process.stderr.write(`\n ${c.agentErrorNoEndpoint}\n\n`);
196
+ return { ok: false, reason: 'no-endpoint' };
197
+ }
198
+ process.stdout.write(`\n ${catalog.trust.detectedAgents}\n\n`);
199
+ const chosen = stdinIsTTY ? await confirmDetectedAgents({ ask, agents, c, input, output }) : agents;
200
+ if (chosen.length === 0) {
201
+ process.stdout.write(`\n ${c.agentCancelled}\n\n`);
202
+ return { ok: true, cancelled: true };
203
+ }
204
+
205
+ const declareAll = (list, s) => withSpinner(c.detectedAgentsDeclaring, () => declareDetectedAgents(list, s, { endpoint }));
206
+ let { added, failed } = await declareAll(chosen, session);
207
+ const expired = failed.filter((f) => f.reason === 'hub-session-expired');
208
+ if (expired.length > 0 && stdinIsTTY) {
209
+ process.stdout.write(`\n ${c.detectedAgentsHubSessionExpired}\n`);
210
+ const relogged = await offerRelogin({ ask, c, runLogin: deps.runLogin });
211
+ if (relogged) {
212
+ const retried = await declareAll(expired.map((f) => f.agent), relogged);
213
+ added = [...added, ...retried.added];
214
+ failed = [...failed.filter((f) => f.reason !== 'hub-session-expired'), ...retried.failed];
215
+ }
216
+ }
217
+
218
+ process.stdout.write('\n');
219
+ for (const agent of added) process.stdout.write(` ${styleSuccess(c.agentDeclaredSuccess(sanitizeRenderText(agent.name)))}\n`);
220
+ for (const f of failed) process.stdout.write(` ${c.detectedAgentFailed(sanitizeRenderText(f.agent.name), reasonCopy(f.reason, c))}\n`);
221
+ process.stdout.write(`\n ${c.detectedAgentsSummary(added.length, chosen.length)}\n\n`);
222
+ return {
223
+ ok: added.length > 0,
224
+ added: added.map((a) => a.name),
225
+ failed: failed.map((f) => ({ name: f.agent.name, reason: f.reason })),
226
+ };
227
+ }
228
+
229
+ async function offerDetectedAgentsAfterUsage({
230
+ report, catalog, ask, stdinIsTTY = !!process.stdin.isTTY, deps = {},
231
+ }) {
232
+ const { loadSession = loadAuthSession, listAgents = requestListAgents } = deps;
233
+ const session = loadSession();
234
+ if (sessionStatus(session) !== 'active' || !session.hubAccessToken) return null;
235
+ const candidates = detectedAgentsFromReport(report);
236
+ const listEndpoint = getAgentsListEndpoint();
237
+ if (candidates.length === 0 || !listEndpoint) return null;
238
+ const listed = await listAgents({ accessToken: session.accessToken, hubAccessToken: session.hubAccessToken }, { endpoint: listEndpoint });
239
+ if (!listed.ok) {
240
+ process.stdout.write(`\n ${catalog.start.detectedAgentsCheckFailed(reasonCopy(listed.reason, catalog.start))}\n`);
241
+ return { ok: false, reason: listed.reason };
242
+ }
243
+ const agents = newDetectedAgents(candidates, listed.agents);
244
+ if (agents.length === 0) return null;
245
+ return addDetectedAgents({ ask, catalog, session, stdinIsTTY, agents, deps });
246
+ }
247
+
165
248
  async function runAddAgents({
166
- ask, catalog, root, session, stdinIsTTY, input, output, preAccepted = false, deps = {},
249
+ ask, catalog, root, session, stdinIsTTY, input, output, preAccepted = false, all = false, yes = false, deps = {},
167
250
  } = {}) {
168
251
  const c = catalog.start;
169
252
  const runLogin = deps.runLogin;
@@ -219,12 +302,30 @@ async function runAddAgents({
219
302
  () => requestListAgents({ accessToken: session.accessToken, hubAccessToken }, { endpoint: listEndpoint }),
220
303
  );
221
304
  const existingNames = listed.ok ? listed.agents.map((a) => a.name.toLowerCase()) : null;
305
+ const fresh = listed.ok ? newDetectedAgents(agents, listed.agents) : [];
306
+ const bulk = () => addDetectedAgents({
307
+ ask, catalog, session: { ...session, hubAccessToken }, stdinIsTTY, yes, agents: fresh, input, output, deps,
308
+ });
309
+ if (all) {
310
+ if (!listed.ok) {
311
+ process.stderr.write(`\n ${c.detectedAgentsCheckFailed(reasonCopy(listed.reason, c))}\n\n`);
312
+ return { ok: false, reason: listed.reason };
313
+ }
314
+ if (fresh.length === 0) {
315
+ process.stdout.write(`\n ${c.detectedAgentsAllOnProfile}\n\n`);
316
+ return { ok: true, cancelled: true };
317
+ }
318
+ return bulk();
319
+ }
222
320
  if (!listed.ok) process.stdout.write(`\n ${c.agentDedupCheckFailed(reasonCopy(listed.reason, c))}\n`);
223
321
 
224
322
  process.stdout.write(`\n ${c.agentIntro}\n`);
225
323
 
226
- // (1) Pick a detected agent.
227
- const agent = await chooseAgent({ ask, stdinIsTTY, agents, c, input, output });
324
+ // (1) Pick a detected agent, or all the new ones at once.
325
+ const addAll = { name: null, pickerLabel: c.addAllDetectedEntry(fresh.length) };
326
+ const choices = stdinIsTTY && typeof ask.suspend === 'function' && fresh.length > 0 ? [addAll, ...agents] : agents;
327
+ const agent = await chooseAgent({ ask, stdinIsTTY, agents: choices, c, input, output });
328
+ if (agent === addAll) return bulk();
228
329
  if (!agent) {
229
330
  process.stdout.write(`\n ${c.agentCancelled}\n\n`);
230
331
  return;
@@ -300,6 +401,8 @@ async function runAddAgents({
300
401
 
301
402
  module.exports = {
302
403
  runAddAgents,
404
+ addDetectedAgents,
405
+ offerDetectedAgentsAfterUsage,
303
406
  chooseAgent,
304
407
  resolveAgentFields,
305
408
  offerRelateAgentToPortfolios,
@@ -28,6 +28,7 @@ const { reviewCopy } = require('./signup-copy');
28
28
  const { getCatalog } = require('./i18n');
29
29
  const { askYesNo } = require('./consent-flow');
30
30
  const { currentRunId } = require('./command-audit');
31
+ const { offerDetectedAgentsAfterUsage } = require('./start-add-agents');
31
32
 
32
33
  async function maybeAutoShare(report, maturity, root, catalog, { bypassThrottle = false } = {}) {
33
34
  try {
@@ -161,7 +162,7 @@ async function reviewUpload({ report, maturity, repos = [], lang, catalog, injec
161
162
  }
162
163
  }
163
164
 
164
- async function renderAndFinalize({ report, maturity, lang, opts, root, gate, catalog, bypassThrottle = false, repos = [], injectedAsk = null }) {
165
+ async function renderAndFinalize({ report, maturity, lang, opts, root, gate, catalog, bypassThrottle = false, repos = [], injectedAsk = null, offerAgents = null }) {
165
166
  if (opts.json) {
166
167
  process.stdout.write(JSON.stringify({ report, maturity }, null, 2) + '\n');
167
168
  return;
@@ -198,6 +199,8 @@ async function renderAndFinalize({ report, maturity, lang, opts, root, gate, cat
198
199
 
199
200
  // "My work with AI" preview, best-effort after the send; never blocks/breaks the run.
200
201
  await maybeRenderAiProfile({ report, maturity, lang, root, save: opts.save, submitOk: !!(shareResult && shareResult.ok), catalog });
202
+
203
+ if (offerAgents && injectedAsk) await offerDetectedAgentsAfterUsage({ report: reviewed.report, catalog, ask: injectedAsk, deps: offerAgents });
201
204
  }
202
205
 
203
206
  // A `ready` profile always shows; otherwise the "being evaluated"/setup-only note only shows when this run's submit succeeded.