shakers-cli 0.0.0-stage → 0.14.1

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 (237) hide show
  1. package/LICENSE +59 -0
  2. package/README.md +1233 -2
  3. package/bin/add-agent.js +88 -0
  4. package/bin/add-project.js +88 -0
  5. package/bin/add-role.js +61 -0
  6. package/bin/add-skill.js +88 -0
  7. package/bin/ai-usage.js +99 -0
  8. package/bin/alma.js +99 -0
  9. package/bin/applications.js +69 -0
  10. package/bin/availability.js +70 -0
  11. package/bin/certifications.js +69 -0
  12. package/bin/certify.js +73 -0
  13. package/bin/change-role.js +61 -0
  14. package/bin/config.js +124 -0
  15. package/bin/experiences.js +69 -0
  16. package/bin/find-projects.js +89 -0
  17. package/bin/invitations.js +69 -0
  18. package/bin/lang.js +70 -0
  19. package/bin/login.js +216 -0
  20. package/bin/mcp.js +124 -0
  21. package/bin/mcpb.js +5 -0
  22. package/bin/me.js +69 -0
  23. package/bin/onboarding.js +134 -0
  24. package/bin/portfolios.js +69 -0
  25. package/bin/rate.js +70 -0
  26. package/bin/register.js +337 -0
  27. package/bin/report-html.js +134 -0
  28. package/bin/save.js +77 -0
  29. package/bin/shakers.js +363 -0
  30. package/bin/share.js +72 -0
  31. package/bin/show-project.js +72 -0
  32. package/bin/socials.js +70 -0
  33. package/bin/superadmin.js +351 -0
  34. package/bin/update-check-worker.js +70 -0
  35. package/bin/update.js +111 -0
  36. package/package.json +40 -4
  37. package/src/agent-category.js +13 -0
  38. package/src/agent-evaluation.js +177 -0
  39. package/src/agent-org-chart.js +355 -0
  40. package/src/agent-synthesis.js +132 -0
  41. package/src/agent-usage.js +118 -0
  42. package/src/agents-client.js +165 -0
  43. package/src/ai-fluency-cell.js +72 -0
  44. package/src/ai-profile-client.js +45 -0
  45. package/src/ai-profile-preview.js +154 -0
  46. package/src/ai.js +66 -0
  47. package/src/alma-client.js +123 -0
  48. package/src/alma-flow.js +32 -0
  49. package/src/alma-repo-context.js +47 -0
  50. package/src/alma-shell.js +161 -0
  51. package/src/ansi.js +62 -0
  52. package/src/applications-client.js +79 -0
  53. package/src/applications-flow.js +58 -0
  54. package/src/auth-client.js +144 -0
  55. package/src/auth-device-client.js +118 -0
  56. package/src/auth-method-picker.js +44 -0
  57. package/src/auth-methods.js +35 -0
  58. package/src/auth-session-store.js +215 -0
  59. package/src/authorship-mismatch.js +62 -0
  60. package/src/authorship.js +204 -0
  61. package/src/automations-detector.js +170 -0
  62. package/src/availability-client.js +93 -0
  63. package/src/availability-flow.js +124 -0
  64. package/src/backend-request.js +191 -0
  65. package/src/blind-approval-extract.js +48 -0
  66. package/src/brand-ansi.js +14 -0
  67. package/src/brand-svg.js +9 -0
  68. package/src/browser-tools-detector.js +21 -0
  69. package/src/build-next-level.js +56 -0
  70. package/src/certifications-client.js +89 -0
  71. package/src/certifications-flow.js +75 -0
  72. package/src/certify-dimension-client.js +163 -0
  73. package/src/certify-dimension-flow.js +135 -0
  74. package/src/certify-disclaimer.js +51 -0
  75. package/src/certify-remediation-prompt.js +23 -0
  76. package/src/cli-args.js +83 -0
  77. package/src/command-audit.js +105 -0
  78. package/src/config-dir.js +120 -0
  79. package/src/config.js +877 -0
  80. package/src/consent-flow.js +115 -0
  81. package/src/consent-skip.js +27 -0
  82. package/src/cv-file.js +62 -0
  83. package/src/cv-text.js +92 -0
  84. package/src/decisions-extract.js +43 -0
  85. package/src/detect-repos.js +145 -0
  86. package/src/detected-tools.js +15 -0
  87. package/src/detection-evidence.js +105 -0
  88. package/src/detection-noise.js +132 -0
  89. package/src/detectors.js +221 -0
  90. package/src/device-auth-store.js +100 -0
  91. package/src/device-login.js +120 -0
  92. package/src/email-verification.js +152 -0
  93. package/src/env-paths.js +30 -0
  94. package/src/find-projects-cache.js +89 -0
  95. package/src/find-projects-client.js +148 -0
  96. package/src/find-projects-flow.js +164 -0
  97. package/src/git-activity.js +124 -0
  98. package/src/git-log-prefetch.js +53 -0
  99. package/src/graph-certs.js +58 -0
  100. package/src/graph-generator.js +230 -0
  101. package/src/graph-scan.js +239 -0
  102. package/src/i18n.js +3341 -0
  103. package/src/inter-fonts.js +6 -0
  104. package/src/interactive-select.js +183 -0
  105. package/src/interview-client.js +226 -0
  106. package/src/interview-transcript.js +56 -0
  107. package/src/inventory-client.js +90 -0
  108. package/src/invitations-client.js +66 -0
  109. package/src/invitations-flow.js +50 -0
  110. package/src/lang-client.js +147 -0
  111. package/src/lang-codes.js +14 -0
  112. package/src/lang-flow.js +85 -0
  113. package/src/legal-notice.js +53 -0
  114. package/src/locale.js +72 -0
  115. package/src/markdown-ansi.js +50 -0
  116. package/src/maturity.js +111 -0
  117. package/src/mcp-ai-usage-result-tool.js +111 -0
  118. package/src/mcp-ai-usage-tool.js +401 -0
  119. package/src/mcp-alma-tools.js +95 -0
  120. package/src/mcp-applications-tools.js +44 -0
  121. package/src/mcp-auth-form.js +480 -0
  122. package/src/mcp-availability-tools.js +45 -0
  123. package/src/mcp-certifications-tools.js +44 -0
  124. package/src/mcp-certify-dimension-tools.js +66 -0
  125. package/src/mcp-choice.js +95 -0
  126. package/src/mcp-detector.js +223 -0
  127. package/src/mcp-device-login-tools.js +177 -0
  128. package/src/mcp-find-projects-tools.js +69 -0
  129. package/src/mcp-install.js +275 -0
  130. package/src/mcp-instructions.js +27 -0
  131. package/src/mcp-invitations-tools.js +44 -0
  132. package/src/mcp-lang-tools.js +41 -0
  133. package/src/mcp-onboarding-tools.js +379 -0
  134. package/src/mcp-portfolios-tools.js +50 -0
  135. package/src/mcp-profile-tools.js +48 -0
  136. package/src/mcp-prompts.js +73 -0
  137. package/src/mcp-rate-tools.js +44 -0
  138. package/src/mcp-register-tools.js +441 -0
  139. package/src/mcp-report-tools.js +105 -0
  140. package/src/mcp-role-tools.js +372 -0
  141. package/src/mcp-saved-positions-tools.js +59 -0
  142. package/src/mcp-server.js +290 -0
  143. package/src/mcp-show-project-tools.js +52 -0
  144. package/src/mcp-signup-client.js +118 -0
  145. package/src/mcp-signup-tools.js +1171 -0
  146. package/src/mcp-socials-tools.js +41 -0
  147. package/src/memory-structure-detector.js +108 -0
  148. package/src/model-call-record.js +198 -0
  149. package/src/onboarding-availability.js +26 -0
  150. package/src/onboarding-client.js +374 -0
  151. package/src/onboarding-flow.js +1162 -0
  152. package/src/onboarding-funnel.js +272 -0
  153. package/src/onboarding-livekit-client.js +102 -0
  154. package/src/onboarding-status-client.js +57 -0
  155. package/src/open-file.js +32 -0
  156. package/src/osc-link.js +16 -0
  157. package/src/portfolio-client.js +164 -0
  158. package/src/portfolio-git-info.js +61 -0
  159. package/src/portfolios-client.js +85 -0
  160. package/src/portfolios-flow.js +79 -0
  161. package/src/profile-client.js +133 -0
  162. package/src/profile-flow.js +115 -0
  163. package/src/profile-switch.js +46 -0
  164. package/src/prompt-select.js +66 -0
  165. package/src/rate-client.js +81 -0
  166. package/src/rate-flow.js +137 -0
  167. package/src/register-context.js +145 -0
  168. package/src/render-ai-fluency.js +266 -0
  169. package/src/render-certification.js +177 -0
  170. package/src/render-html.js +140 -0
  171. package/src/render-report-text.js +27 -0
  172. package/src/render-sheet.js +261 -0
  173. package/src/render-terminal.js +109 -0
  174. package/src/render-usage-report-text.js +155 -0
  175. package/src/repo-context.js +215 -0
  176. package/src/report-gating.js +49 -0
  177. package/src/report-store.js +226 -0
  178. package/src/report-theme.js +177 -0
  179. package/src/report-view-client.js +50 -0
  180. package/src/reveal-output.js +29 -0
  181. package/src/roadmap-content.js +453 -0
  182. package/src/roadmap-personalization.js +132 -0
  183. package/src/roadmap-prompt.js +154 -0
  184. package/src/roles-client.js +112 -0
  185. package/src/roles-flow.js +192 -0
  186. package/src/sanitize-network-text.js +36 -0
  187. package/src/saved-positions-client.js +71 -0
  188. package/src/saved-positions-flow.js +50 -0
  189. package/src/scan-exclusions.js +50 -0
  190. package/src/scanner.js +624 -0
  191. package/src/scope-signals.js +148 -0
  192. package/src/secret-prompt.js +72 -0
  193. package/src/session-gate.js +30 -0
  194. package/src/session-normalizers.js +174 -0
  195. package/src/session-refresh.js +51 -0
  196. package/src/session-scan.js +475 -0
  197. package/src/session-signals.js +36 -0
  198. package/src/shakers-wordmark.js +17 -0
  199. package/src/share-card.js +362 -0
  200. package/src/share-certification.js +111 -0
  201. package/src/share-consent-state.js +157 -0
  202. package/src/share-payload.js +300 -0
  203. package/src/share.js +185 -0
  204. package/src/show-project-client.js +58 -0
  205. package/src/show-project-flow.js +119 -0
  206. package/src/signup-client.js +103 -0
  207. package/src/signup-copy.js +450 -0
  208. package/src/signup-language.js +21 -0
  209. package/src/skills-client.js +102 -0
  210. package/src/socials-client.js +91 -0
  211. package/src/socials-flow.js +83 -0
  212. package/src/specificity-extract.js +41 -0
  213. package/src/start-add-agents.js +306 -0
  214. package/src/start-add-portfolio.js +388 -0
  215. package/src/start-add-skills.js +260 -0
  216. package/src/stdin-ask.js +129 -0
  217. package/src/steering-extract.js +43 -0
  218. package/src/strip-internal-comments.js +45 -0
  219. package/src/tech-detector.js +295 -0
  220. package/src/templates/report-sheet.html +572 -0
  221. package/src/terminal-agents.js +207 -0
  222. package/src/terminal-format.js +85 -0
  223. package/src/terminal-progress.js +106 -0
  224. package/src/terminal-sections.js +219 -0
  225. package/src/tier-analysis.js +126 -0
  226. package/src/tier-engine.js +114 -0
  227. package/src/tier-projection.js +117 -0
  228. package/src/transcript-turns.js +49 -0
  229. package/src/translations-client.js +84 -0
  230. package/src/typewriter.js +154 -0
  231. package/src/update-flow.js +223 -0
  232. package/src/update-notifier.js +113 -0
  233. package/src/usage-model-calls.js +119 -0
  234. package/src/usage-oneshot-commands.js +180 -0
  235. package/src/usage-repo-scope.js +162 -0
  236. package/src/usage-run-steps.js +354 -0
  237. package/src/work-streams.js +79 -0
@@ -0,0 +1,102 @@
1
+ 'use strict';
2
+
3
+ const { postJsonWithTimeout } = require('./backend-request');
4
+
5
+ // Talent-skills client (talents-ai-score, ADR-054/055 — `start`'s "Añadir skills" route, src/start-add-skills.js).
6
+
7
+ const DEFAULT_TIMEOUT_MS = 20000;
8
+
9
+ // `works.*` business codes (WorksExceptionFilter's `codeMap`) -> this client's own reason vocabulary.
10
+ const WORKS_CODE_TO_REASON = {
11
+ 'certification.dimension_on_cooldown': 'dimension-on-cooldown',
12
+ 'interview.non_conversational_blocks_in_text': 'needs-web',
13
+ 'works.hub_token_missing': 'no-hub-token',
14
+ 'works.hub_session_expired': 'hub-session-expired',
15
+ 'works.hub_upstream_error': 'hub-upstream-error',
16
+ 'works.hub_unavailable': 'hub-unavailable',
17
+ 'works.ai_consent_required': 'ai-consent-required',
18
+ 'interview.already_started': 'interview-already-started',
19
+ 'interview.onboarding_not_found': 'onboarding-not-found',
20
+ 'assigned-clusters.already_assigned': 'already-assigned',
21
+ 'assigned-clusters.cap_reached': 'role-cap-reached',
22
+ 'assigned-clusters.invalid_cluster': 'invalid-cluster',
23
+ 'assigned-clusters.not_assigned': 'not-assigned',
24
+ };
25
+
26
+ function parseJson(raw) {
27
+ try {
28
+ return raw ? JSON.parse(raw) : null;
29
+ } catch {
30
+ return null;
31
+ }
32
+ }
33
+
34
+ // The certs session Bearer (standard `AuthGuard`, required on ALL THREE talent-skills routes — none of them is `@Public()`) plus, only when present, the Talent's own Hub token.
35
+ function authHeaders({ accessToken, hubAccessToken } = {}) {
36
+ const headers = {};
37
+ if (typeof accessToken === 'string' && accessToken) headers.Authorization = `Bearer ${accessToken}`;
38
+ if (typeof hubAccessToken === 'string' && hubAccessToken) headers['X-Hub-Token'] = hubAccessToken;
39
+ return headers;
40
+ }
41
+
42
+ function reasonForError(status, raw) {
43
+ const body = parseJson(raw);
44
+ const code = body && typeof body.code === 'string' ? body.code : null;
45
+ return (code && WORKS_CODE_TO_REASON[code]) || `http-${status}`;
46
+ }
47
+
48
+ // `POST .../skills/resolve-addable`.
49
+ async function requestResolveAddableSkills({ technologies, accessToken } = {}, { endpoint, timeoutMs = DEFAULT_TIMEOUT_MS } = {}) {
50
+ if (!endpoint) return { ok: false, reason: 'no-endpoint' };
51
+
52
+ const body = { technologies: Array.isArray(technologies) ? technologies.filter((t) => typeof t === 'string' && t) : [] };
53
+ let res;
54
+ try {
55
+ res = await postJsonWithTimeout(endpoint, body, timeoutMs, 'POST', null, authHeaders({ accessToken }));
56
+ } catch (e) {
57
+ return { ok: false, reason: (e && e.kind) || 'network-error' };
58
+ }
59
+
60
+ if (res.status < 200 || res.status >= 300) {
61
+ return { ok: false, reason: reasonForError(res.status, res.raw) };
62
+ }
63
+
64
+ const json = parseJson(res.raw);
65
+ const data = json && typeof json === 'object' && Array.isArray(json.data) ? json.data : null;
66
+ if (!data) return { ok: false, reason: 'bad-response' };
67
+
68
+ const addable = data
69
+ .map((entry) => ({
70
+ skillId: entry && typeof entry.skillId === 'number' ? entry.skillId : null,
71
+ skillName: entry && typeof entry.skillName === 'string' ? entry.skillName : null,
72
+ technology: entry && typeof entry.technology === 'string' ? entry.technology : null,
73
+ }))
74
+ .filter((s) => s.skillId !== null);
75
+ return { ok: true, addable };
76
+ }
77
+
78
+ // `POST .../skills/declare`.
79
+ async function requestDeclareSkill({ skillId, accessToken, hubAccessToken } = {}, { endpoint, timeoutMs = DEFAULT_TIMEOUT_MS } = {}) {
80
+ if (!endpoint) return { ok: false, reason: 'no-endpoint' };
81
+ if (!hubAccessToken) return { ok: false, reason: 'no-hub-token' };
82
+
83
+ let res;
84
+ try {
85
+ res = await postJsonWithTimeout(endpoint, { skillId }, timeoutMs, 'POST', null, authHeaders({ accessToken, hubAccessToken }));
86
+ } catch (e) {
87
+ return { ok: false, reason: (e && e.kind) || 'network-error' };
88
+ }
89
+
90
+ if (res.status < 200 || res.status >= 300) {
91
+ if (res.status === 409) return { ok: false, reason: 'skill-exists' };
92
+ return { ok: false, reason: reasonForError(res.status, res.raw) };
93
+ }
94
+
95
+ const json = parseJson(res.raw);
96
+ if (!json || json.status !== 'OK' || !Array.isArray(json.data)) {
97
+ return { ok: false, reason: 'bad-response' };
98
+ }
99
+ return { ok: true };
100
+ }
101
+
102
+ module.exports = { requestResolveAddableSkills, requestDeclareSkill, reasonForError, WORKS_CODE_TO_REASON };
@@ -0,0 +1,91 @@
1
+ 'use strict';
2
+
3
+ // Social links client. VIEW = GET `works/me/social`; SET = PUT the same endpoint
4
+ // (upsert of the WHOLE object — a fixed set of named URL fields, not per-link rows).
5
+
6
+ const { postJsonWithTimeout } = require('./backend-request');
7
+ const { reasonForError } = require('./onboarding-client');
8
+
9
+ const DEFAULT_TIMEOUT_MS = 20000;
10
+ const NETWORKS = ['linkedin', 'github', 'website', 'twitter', 'instagram', 'facebook', 'dribbble', 'behance'];
11
+
12
+ function parseJson(raw) {
13
+ try {
14
+ return raw ? JSON.parse(raw) : null;
15
+ } catch {
16
+ return null;
17
+ }
18
+ }
19
+
20
+ function bearerHeaders(accessToken) {
21
+ const headers = {};
22
+ if (typeof accessToken === 'string' && accessToken) headers.Authorization = `Bearer ${accessToken}`;
23
+ return headers;
24
+ }
25
+
26
+ function normalizeSocial(data) {
27
+ const it = data && typeof data === 'object' ? data : {};
28
+ const out = {};
29
+ for (const k of NETWORKS) out[k] = typeof it[k] === 'string' && it[k] ? it[k] : null;
30
+ return out;
31
+ }
32
+
33
+ async function requestSocials({ hubAccessToken } = {}, { endpoint, timeoutMs = DEFAULT_TIMEOUT_MS } = {}) {
34
+ if (!endpoint) return { ok: false, reason: 'no-endpoint' };
35
+ if (!hubAccessToken) return { ok: false, reason: 'no-hub-token' };
36
+ let res;
37
+ try {
38
+ res = await postJsonWithTimeout(endpoint, null, timeoutMs, 'GET', null, bearerHeaders(hubAccessToken));
39
+ } catch (e) {
40
+ return { ok: false, reason: (e && e.kind) || 'network-error' };
41
+ }
42
+ // No social section yet is a normal empty state, not an error.
43
+ if (res.status === 404) return { ok: true, social: normalizeSocial({}) };
44
+ if (res.status < 200 || res.status >= 300) return { ok: false, reason: reasonForError(res.status, res.raw) };
45
+ const env = parseJson(res.raw);
46
+ const data = env && env.status === 'OK' ? env.data : env;
47
+ if (!data || typeof data !== 'object') return { ok: false, reason: 'bad-response' };
48
+ return { ok: true, social: normalizeSocial(data) };
49
+ }
50
+
51
+ // Upsert the WHOLE object: `social` carries every network (value or null).
52
+ async function requestSetSocials({ hubAccessToken, social } = {}, { endpoint, timeoutMs = DEFAULT_TIMEOUT_MS } = {}) {
53
+ if (!endpoint) return { ok: false, reason: 'no-endpoint' };
54
+ if (!hubAccessToken) return { ok: false, reason: 'no-hub-token' };
55
+ if (!social || typeof social !== 'object') return { ok: false, reason: 'no-social' };
56
+ const body = {};
57
+ for (const k of NETWORKS) body[k] = typeof social[k] === 'string' && social[k] ? social[k] : null;
58
+ let res;
59
+ try {
60
+ res = await postJsonWithTimeout(endpoint, body, timeoutMs, 'PUT', null, bearerHeaders(hubAccessToken));
61
+ } catch (e) {
62
+ return { ok: false, reason: (e && e.kind) || 'network-error' };
63
+ }
64
+ if (res.status < 200 || res.status >= 300) return { ok: false, reason: reasonForError(res.status, res.raw) };
65
+ return { ok: true };
66
+ }
67
+
68
+ async function fetchSocials(deps = {}, { hubAccessToken, timeoutMs = DEFAULT_TIMEOUT_MS } = {}) {
69
+ const getEndpoint = deps.getMeSocialEndpoint || require('./config').getMeSocialEndpoint;
70
+ const request = deps.requestSocials || requestSocials;
71
+ const endpoint = getEndpoint();
72
+ if (!endpoint) return { ok: false, reason: 'no-endpoint' };
73
+ return request({ hubAccessToken }, { endpoint, timeoutMs });
74
+ }
75
+
76
+ async function saveSocials(deps = {}, { hubAccessToken, social, timeoutMs = DEFAULT_TIMEOUT_MS } = {}) {
77
+ const getEndpoint = deps.getMeSocialEndpoint || require('./config').getMeSocialEndpoint;
78
+ const request = deps.requestSetSocials || requestSetSocials;
79
+ const endpoint = getEndpoint();
80
+ if (!endpoint) return { ok: false, reason: 'no-endpoint' };
81
+ return request({ hubAccessToken, social }, { endpoint, timeoutMs });
82
+ }
83
+
84
+ module.exports = {
85
+ NETWORKS,
86
+ normalizeSocial,
87
+ requestSocials,
88
+ requestSetSocials,
89
+ fetchSocials,
90
+ saveSocials,
91
+ };
@@ -0,0 +1,83 @@
1
+ 'use strict';
2
+
3
+ // `socials` flow: view the talent's social links, and `--set` (interactive,
4
+ // confirmation) to add/update/clear one. The write upserts the WHOLE object, so
5
+ // we merge the chosen field into the current object and PUT it.
6
+
7
+ const { styleLabelPrefix } = require('./ansi');
8
+ const { NETWORKS } = require('./socials-client');
9
+
10
+ function makeSocialsDeps(overrides = {}) {
11
+ const client = require('./socials-client');
12
+ return {
13
+ fetchSocials: (opts) => client.fetchSocials({}, opts),
14
+ saveSocials: (opts) => client.saveSocials({}, opts),
15
+ promptSelect: (opts) => require('./prompt-select').promptSelect(opts),
16
+ ...overrides,
17
+ };
18
+ }
19
+
20
+ function setNetworks(social) {
21
+ return NETWORKS.filter((k) => social && social[k]);
22
+ }
23
+
24
+ function renderView(io, fp, social) {
25
+ io.section(fp.title);
26
+ const present = setNetworks(social);
27
+ if (!present.length) { io.notify(fp.none); io.success(fp.done); return; }
28
+ present.forEach((k) => io.notify(styleLabelPrefix(fp.line(fp.networkLabel(k), social[k]))));
29
+ io.success(fp.done);
30
+ }
31
+
32
+ async function runSet({ io, ask, session, catalog, opts, deps }) {
33
+ const fp = catalog.socials;
34
+ const hubAccessToken = session ? session.hubAccessToken : null;
35
+ if (!opts.stdinIsTTY) { io.error(fp.setNeedsInteractive); return { ok: false, reason: 'non-interactive' }; }
36
+ const selOut = (s) => io.notify(s);
37
+
38
+ const cur = await io.withProgress(fp.loading, () => deps.fetchSocials({ hubAccessToken }));
39
+ if (!cur.ok) { io.error(fp.fetchFailed(cur.reason)); return { ok: false, reason: cur.reason }; }
40
+ const social = { ...cur.social };
41
+
42
+ io.section(fp.setTitle);
43
+ const present = setNetworks(social);
44
+ if (present.length) present.forEach((k) => io.notify(styleLabelPrefix(fp.line(fp.networkLabel(k), social[k]))));
45
+
46
+ const net = await deps.promptSelect({
47
+ ask, stdinIsTTY: opts.stdinIsTTY, out: selOut, header: fp.pickNetwork,
48
+ items: NETWORKS.map((k) => ({ label: fp.networkLabel(k), value: k })), labelFor: (x) => x.label, input: opts.input, output: opts.output,
49
+ });
50
+ if (!net) { io.notify(fp.setCancelled); return { ok: true, cancelled: true }; }
51
+ const key = net.value;
52
+
53
+ const raw = (await io.ask(fp.askUrl(fp.networkLabel(key)))).trim();
54
+ const value = raw === '' ? null : raw;
55
+ social[key] = value;
56
+
57
+ io.notify(value ? fp.setDiffSet(fp.networkLabel(key), value) : fp.setDiffClear(fp.networkLabel(key)));
58
+ const confirmed = await io.confirm(fp.setConfirm);
59
+ if (!confirmed) { io.notify(fp.setCancelled); return { ok: true, cancelled: true }; }
60
+
61
+ const res = await io.withProgress(fp.setSaving, () => deps.saveSocials({ hubAccessToken, social }));
62
+ if (!res.ok) { io.error(fp.setFailed(res.reason)); return { ok: false, reason: res.reason }; }
63
+ io.success(value ? fp.setDone(fp.networkLabel(key)) : fp.setCleared(fp.networkLabel(key)));
64
+ return { ok: true, set: { key, value } };
65
+ }
66
+
67
+ async function runSocials({ io, rawOut, ask, session, catalog, opts = {}, deps = makeSocialsDeps() }) {
68
+ const fp = catalog.socials;
69
+ if (opts.set) return runSet({ io, ask, session, catalog, opts, deps });
70
+
71
+ const hubAccessToken = session ? session.hubAccessToken : null;
72
+ const doFetch = () => deps.fetchSocials({ hubAccessToken });
73
+ const res = opts.json ? await doFetch() : await io.withProgress(fp.loading, doFetch);
74
+ if (!res.ok) { io.error(fp.fetchFailed(res.reason)); return { ok: false, reason: res.reason }; }
75
+ if (opts.json) {
76
+ (rawOut || ((s) => process.stdout.write(s)))(`${JSON.stringify({ ok: true, social: res.social }, null, 2)}\n`);
77
+ return { ok: true, social: res.social };
78
+ }
79
+ renderView(io, fp, res.social);
80
+ return { ok: true, social: res.social };
81
+ }
82
+
83
+ module.exports = { makeSocialsDeps, runSocials, setNetworks, renderView };
@@ -0,0 +1,41 @@
1
+ 'use strict';
2
+
3
+ const { eachHumanText } = require('./transcript-turns');
4
+
5
+ const MIN_INSTRUCTION_LEN = 12;
6
+
7
+ const CONCRETE_RE =
8
+ /(`[^`]+`|\b[\w-]+\.(ts|tsx|js|jsx|mjs|cjs|py|go|rs|java|rb|php|css|scss|html|json|ya?ml|md|sql|sh|toml)\b|\/[\w./-]{2,}|"[^"]{2,}"|\d|\b(must|should|shouldn'?t|don'?t|do not|ensure|avoid|instead|because|so that|only|never|always|exactly|specifically|at least|at most|no more than|make sure|asegúrate|debe|deber[íi]as?|no (uses?|hagas|pongas)|en vez de|en lugar de|porque|exactamente|específicamente|solo|nunca|siempre)\b)/i;
9
+
10
+ function makeSpecificityAccumulator() {
11
+ let instructionTurnCount = 0;
12
+ let specificInstructionCount = 0;
13
+ let totalInstructionLength = 0;
14
+ return {
15
+ add(text) {
16
+ const trimmed = typeof text === 'string' ? text.trim() : '';
17
+ if (trimmed.length < MIN_INSTRUCTION_LEN) return;
18
+ instructionTurnCount += 1;
19
+ totalInstructionLength += trimmed.length;
20
+ if (CONCRETE_RE.test(trimmed)) specificInstructionCount += 1;
21
+ },
22
+ result() {
23
+ if (instructionTurnCount === 0) return null;
24
+ return {
25
+ specificInstructionCount,
26
+ instructionTurnCount,
27
+ avgInstructionLength: Math.round(
28
+ totalInstructionLength / instructionTurnCount,
29
+ ),
30
+ };
31
+ },
32
+ };
33
+ }
34
+
35
+ function collectSpecificity(env = process.env, selectedCwds = null) {
36
+ const acc = makeSpecificityAccumulator();
37
+ for (const text of eachHumanText(env, { selectedCwds })) acc.add(text);
38
+ return acc.result();
39
+ }
40
+
41
+ module.exports = { collectSpecificity, makeSpecificityAccumulator };
@@ -0,0 +1,306 @@
1
+ 'use strict';
2
+
3
+ const {
4
+ getUsageDiscoveredInventoryEndpoint,
5
+ getAgentsListEndpoint,
6
+ getAgentsDeclareEndpoint,
7
+ getAgentsDraftFieldsEndpoint,
8
+ getPortfoliosListEndpoint,
9
+ getAgentPortfoliosEndpoint,
10
+ } = require('./config');
11
+ const {
12
+ requestListAgents, requestDeclareAgent, requestDraftAgentFields, requestRelateAgentPortfolios,
13
+ } = require('./agents-client');
14
+ const { requestListPortfolios } = require('./portfolio-client');
15
+ const { requestDiscoveredInventory } = require('./inventory-client');
16
+ const { ensureFreshSession } = require('./session-refresh');
17
+ const { confirmDisclaimerAcceptance } = require('./certify-disclaimer');
18
+ const { runInteractiveMultiSelect } = require('./interactive-select');
19
+ const { loadAuthSession, sessionStatus } = require('./auth-session-store');
20
+ const { withSpinner } = require('./terminal-progress');
21
+ const { reasonCopy, pickPortfolios } = require('./start-add-skills');
22
+ // `askWithDefault`: pre-filled line prompt (empty enter accepts the default),
23
+ // reused from the portfolio route.
24
+ const { askWithDefault } = require('./start-add-portfolio');
25
+ const { styleQuestion, styleSuccess } = require('./ansi');
26
+
27
+ // `start`'s "Add agent to profile" route (talents-ai-score Phase 2), mirror of src/start-add-portfolio.js: the IA pre-drafts, the Talent edits.
28
+
29
+ const MAX_DEFINITION_CHARS = 4000;
30
+
31
+ async function askFreeText(ask, promptText) {
32
+ return String(await ask(styleQuestion(promptText))).trim();
33
+ }
34
+
35
+ async function askNumberedChoice(ask, promptText, count) {
36
+ const raw = (await ask(styleQuestion(promptText))).trim();
37
+ if (!raw) return null;
38
+ const n = Number.parseInt(raw, 10);
39
+ if (!Number.isInteger(n) || n < 1 || n > count) return null;
40
+ return n - 1;
41
+ }
42
+
43
+ // Step 1 — pick ONE detected agent (single-select, numbered-prompt fallback).
44
+ // Returns the agent, or null on cancel.
45
+ async function chooseAgent({ ask, stdinIsTTY, agents, c, input, output }) {
46
+ const labelFor = (a) => a.pickerLabel || a.name;
47
+ if (stdinIsTTY && typeof ask.suspend === 'function') {
48
+ ask.suspend();
49
+ const picked = await runInteractiveMultiSelect({
50
+ items: agents,
51
+ labelFor,
52
+ header: c.agentPickHeading,
53
+ hint: c.agentPickHint,
54
+ single: true,
55
+ ...(input ? { input } : {}),
56
+ ...(output ? { output } : {}),
57
+ });
58
+ ask.resume();
59
+ // Drain the picker-residual-Enter (the suspend/resume mode switch can leave a
60
+ // stray Enter that pre-answers the next prompt) — same fix as bin/start.js/certify.js.
61
+ if (typeof ask.drain === 'function') ask.drain();
62
+ return picked && picked.length ? picked[0] : null;
63
+ }
64
+
65
+ process.stdout.write(`\n ${styleQuestion(c.agentPickHeading)}\n`);
66
+ agents.forEach((a, i) => process.stdout.write(` ${i + 1}) ${labelFor(a)}\n`));
67
+ const idx = await askNumberedChoice(ask, ` ${c.agentPickPrompt(agents.length)}`, agents.length);
68
+ return idx === null ? null : agents[idx];
69
+ }
70
+
71
+ // Step 4 — the two AI-drafted fields, consent-gated (ADR-052) before any egress.
72
+ async function resolveAgentFields({ ask, catalog, c, stdinIsTTY, agent, accessToken, preAccepted = false }) {
73
+ const freeText = async () => ({
74
+ whatItDoes: await askFreeText(ask, c.agentWhatItDoesFreeTextPrompt),
75
+ humanDecides: await askFreeText(ask, c.agentHumanDecidesFreeTextPrompt),
76
+ });
77
+
78
+ const draftEndpoint = getAgentsDraftFieldsEndpoint();
79
+ if (!draftEndpoint) return freeText();
80
+
81
+ const acceptance = await confirmDisclaimerAcceptance({
82
+ ask, catalog, stdinIsTTY, preAccepted, text: c.agentFieldsConsentDisclaimer,
83
+ });
84
+ if (!acceptance.accepted) return freeText();
85
+
86
+ const definition = typeof agent.definition === 'string' && agent.definition
87
+ ? agent.definition.slice(0, MAX_DEFINITION_CHARS)
88
+ : null;
89
+ const draft = await withSpinner(c.agentDraftingLabel, () => requestDraftAgentFields(
90
+ { name: agent.name, tools: agent.tools, model: agent.model, parent: agent.parent, definition, accessToken },
91
+ { endpoint: draftEndpoint },
92
+ ));
93
+ if (!draft.ok) return freeText();
94
+
95
+ process.stdout.write(`\n ${styleQuestion(c.agentWhatItDoesDraftLabel)}\n ${draft.whatItDoes}\n`);
96
+ const whatItDoes = await askWithDefault(ask, c.agentWhatItDoesEditPrompt, draft.whatItDoes);
97
+ process.stdout.write(`\n ${styleQuestion(c.agentHumanDecidesDraftLabel)}\n ${draft.humanDecides}\n`);
98
+ const humanDecides = await askWithDefault(ask, c.agentHumanDecidesEditPrompt, draft.humanDecides);
99
+ return { whatItDoes, humanDecides };
100
+ }
101
+
102
+ // Asks to re-login and returns the fresh session, or null. Reuses the portfolio
103
+ // route's generic relogin copys.
104
+ async function offerRelogin({ ask, c, runLogin }) {
105
+ if (typeof runLogin !== 'function') return null;
106
+ const raw = String(await ask(`\n ${styleQuestion(c.portfolioOfferRelogin)} `)).trim();
107
+ if (!/^(y|yes|s|si|sí)$/i.test(raw)) {
108
+ process.stdout.write(`\n ${c.portfolioRelaunchDeclined}\n\n`);
109
+ return null;
110
+ }
111
+ await runLogin([], { ask });
112
+ const fresh = loadAuthSession();
113
+ if (sessionStatus(fresh) !== 'active' || !fresh.hubAccessToken) {
114
+ process.stdout.write(`\n ${c.portfolioReloginFailed}\n\n`);
115
+ return null;
116
+ }
117
+ return fresh;
118
+ }
119
+
120
+ async function offerRelateAgentToPortfolios({
121
+ ask, stdinIsTTY, agentId, agentName, session, c, input, output,
122
+ }) {
123
+ if (!agentId) return;
124
+ const listEndpoint = getPortfoliosListEndpoint();
125
+ const relateEndpoint = getAgentPortfoliosEndpoint(agentId);
126
+ if (!listEndpoint || !relateEndpoint) return;
127
+
128
+ const listed = await requestListPortfolios(
129
+ { accessToken: session.accessToken, hubAccessToken: session.hubAccessToken },
130
+ { endpoint: listEndpoint },
131
+ );
132
+ if (!listed.ok || listed.portfolios.length === 0) {
133
+ process.stdout.write(`\n ${c.agentRelateNoPortfolios}\n`);
134
+ return;
135
+ }
136
+
137
+ const wantsRelate = /^(y|yes|s|si|sí)$/i.test(String(await ask(`\n ${styleQuestion(c.agentRelateAsk(agentName))} `)).trim());
138
+ if (!wantsRelate) {
139
+ process.stdout.write(` ${c.agentRelateSkipped(agentName)}\n`);
140
+ return;
141
+ }
142
+
143
+ const heading = c.agentRelateSelectHeading(agentName);
144
+ const chosen = await pickPortfolios({ ask, stdinIsTTY, portfolios: listed.portfolios, heading, c, input, output });
145
+ if (!chosen || chosen.length === 0) {
146
+ process.stdout.write(` ${c.agentRelateSkipped(agentName)}\n`);
147
+ return;
148
+ }
149
+
150
+ const items = chosen.map((portfolio) => ({ portfolioId: portfolio.id }));
151
+
152
+ const r = await requestRelateAgentPortfolios(
153
+ { agentId, items, hubAccessToken: session.hubAccessToken },
154
+ { endpoint: relateEndpoint },
155
+ );
156
+ if (r.ok) {
157
+ for (const portfolio of chosen) {
158
+ process.stdout.write(` ${styleSuccess(c.agentRelated(agentName, portfolio.name))}\n`);
159
+ }
160
+ } else {
161
+ process.stdout.write(` ${c.agentRelateFailed(agentName, reasonCopy(r.reason, c))}\n`);
162
+ }
163
+ }
164
+
165
+ async function runAddAgents({
166
+ ask, catalog, root, session, stdinIsTTY, input, output, preAccepted = false, deps = {},
167
+ } = {}) {
168
+ const c = catalog.start;
169
+ const runLogin = deps.runLogin;
170
+ const refreshSession = deps.refreshSession || (() => ensureFreshSession(process.env));
171
+
172
+ const inventoryEndpoint = getUsageDiscoveredInventoryEndpoint();
173
+ const listEndpoint = getAgentsListEndpoint();
174
+ const declareEndpoint = getAgentsDeclareEndpoint();
175
+ if (!inventoryEndpoint || !listEndpoint || !declareEndpoint) {
176
+ process.stderr.write(`\n ${c.agentErrorNoEndpoint}\n\n`);
177
+ return;
178
+ }
179
+
180
+ let hubAccessToken = session.hubAccessToken;
181
+ if (!hubAccessToken) {
182
+ process.stderr.write(`\n ${c.agentNoHubToken}\n\n`);
183
+ return;
184
+ }
185
+
186
+ let inventory = await requestDiscoveredInventory(
187
+ { accessToken: session.accessToken },
188
+ { endpoint: inventoryEndpoint },
189
+ );
190
+ // An expired hub JWT (the same 401 the usage poll hits) must not break add-agent:
191
+ // refresh the session once and retry the read (and adopt the fresh tokens downstream).
192
+ if (inventory.reason === 'http-401') {
193
+ let fresh = null;
194
+ try { fresh = await refreshSession(); } catch { fresh = null; }
195
+ if (fresh && (fresh.accessToken || fresh.hubAccessToken)) {
196
+ session = { ...session, accessToken: fresh.accessToken || session.accessToken, hubAccessToken: fresh.hubAccessToken || session.hubAccessToken };
197
+ hubAccessToken = session.hubAccessToken;
198
+ inventory = await requestDiscoveredInventory(
199
+ { accessToken: session.accessToken },
200
+ { endpoint: inventoryEndpoint },
201
+ );
202
+ }
203
+ }
204
+ if (!inventory.ok) {
205
+ process.stderr.write(`\n ${c.agentInventoryError(reasonCopy(inventory.reason, c))}\n\n`);
206
+ return;
207
+ }
208
+ const agents = inventory.agents;
209
+ if (agents.length === 0) {
210
+ process.stdout.write(`\n ${c.agentNoAgentsDetected}\n\n`);
211
+ return;
212
+ }
213
+ for (const a of agents) a.pickerLabel = a.role ? `${a.name} — ${a.role}` : a.name;
214
+
215
+ // DEDUP by name (before asking anything). A failed list is reported as "could
216
+ // not verify" (never "no duplicates") and the flow continues.
217
+ const listed = await withSpinner(
218
+ c.agentDedupCheckingLabel,
219
+ () => requestListAgents({ accessToken: session.accessToken, hubAccessToken }, { endpoint: listEndpoint }),
220
+ );
221
+ const existingNames = listed.ok ? listed.agents.map((a) => a.name.toLowerCase()) : null;
222
+ if (!listed.ok) process.stdout.write(`\n ${c.agentDedupCheckFailed(reasonCopy(listed.reason, c))}\n`);
223
+
224
+ process.stdout.write(`\n ${c.agentIntro}\n`);
225
+
226
+ // (1) Pick a detected agent.
227
+ const agent = await chooseAgent({ ask, stdinIsTTY, agents, c, input, output });
228
+ if (!agent) {
229
+ process.stdout.write(`\n ${c.agentCancelled}\n\n`);
230
+ return;
231
+ }
232
+
233
+ // (3) Name ← detected, editable. (2) dedup applies to the FINAL name.
234
+ const name = await askWithDefault(ask, c.agentNamePrompt(agent.name), agent.name);
235
+ if (existingNames && existingNames.includes(name.toLowerCase())) {
236
+ const existing = listed.ok ? listed.agents.find((a) => a.name.toLowerCase() === name.toLowerCase()) : null;
237
+ const existingId = existing && existing.id ? existing.id : null;
238
+ if (existingId) {
239
+ process.stdout.write(`\n ${c.agentAlreadyAdded(name)}\n`);
240
+ await offerRelateAgentToPortfolios({
241
+ ask, stdinIsTTY, agentId: existingId, agentName: name, session, c, input, output,
242
+ });
243
+ process.stdout.write('\n');
244
+ } else {
245
+ process.stdout.write(`\n ${c.agentDuplicateName(name)}\n\n`);
246
+ }
247
+ return;
248
+ }
249
+
250
+ // (4) whatItDoes + humanDecides — AI draft (consent-gated) or free-text.
251
+ const { whatItDoes, humanDecides } = await resolveAgentFields({
252
+ ask, catalog, c, stdinIsTTY, agent, accessToken: session.accessToken, preAccepted,
253
+ });
254
+
255
+ const matchedRole = agent.role || null;
256
+ if (matchedRole) process.stdout.write(`\n ${c.agentCatalogMatched(matchedRole)}\n`);
257
+
258
+ const payload = { name, whatItDoes: whatItDoes || agent.whatItDoes, humanDecides };
259
+
260
+ // Declare.
261
+ const declared = await withSpinner(
262
+ c.agentDeclaring,
263
+ () => requestDeclareAgent({ ...payload, accessToken: session.accessToken, hubAccessToken }, { endpoint: declareEndpoint }),
264
+ );
265
+ if (declared.ok) {
266
+ process.stdout.write(`\n ${styleSuccess(c.agentDeclaredSuccess(name))}\n`);
267
+ await offerRelateAgentToPortfolios({
268
+ ask, stdinIsTTY, agentId: declared.agentId, agentName: name, session, c, input, output,
269
+ });
270
+ process.stdout.write('\n');
271
+ return;
272
+ }
273
+ if (declared.reason !== 'hub-session-expired') {
274
+ process.stdout.write(`\n ${c.agentDeclareFailed(reasonCopy(declared.reason, c))}\n\n`);
275
+ return;
276
+ }
277
+
278
+ // Hub session expired mid-flight — offer one relogin + retry.
279
+ process.stdout.write(`\n ${c.agentHubSessionExpired}\n`);
280
+ const relogged = await offerRelogin({ ask, c, runLogin });
281
+ if (!relogged) return;
282
+ process.stdout.write(` ${c.portfolioRelaunchAfterRelogin}\n`);
283
+ const retried = await withSpinner(
284
+ c.agentDeclaring,
285
+ () => requestDeclareAgent(
286
+ { ...payload, accessToken: relogged.accessToken, hubAccessToken: relogged.hubAccessToken },
287
+ { endpoint: declareEndpoint },
288
+ ),
289
+ );
290
+ if (retried.ok) {
291
+ process.stdout.write(`\n ${styleSuccess(c.agentDeclaredSuccess(name))}\n`);
292
+ await offerRelateAgentToPortfolios({
293
+ ask, stdinIsTTY, agentId: retried.agentId, agentName: name, session: relogged, c, input, output,
294
+ });
295
+ process.stdout.write('\n');
296
+ } else {
297
+ process.stdout.write(`\n ${c.agentDeclareFailed(reasonCopy(retried.reason, c))}\n\n`);
298
+ }
299
+ }
300
+
301
+ module.exports = {
302
+ runAddAgents,
303
+ chooseAgent,
304
+ resolveAgentFields,
305
+ offerRelateAgentToPortfolios,
306
+ };