shakers-cli-beta 0.8.0-beta.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (218) hide show
  1. package/LICENSE +59 -0
  2. package/README.md +1175 -0
  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 +81 -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 +120 -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 +283 -0
  20. package/bin/mcp.js +98 -0
  21. package/bin/me.js +69 -0
  22. package/bin/onboarding.js +134 -0
  23. package/bin/portfolios.js +69 -0
  24. package/bin/rate.js +70 -0
  25. package/bin/register.js +325 -0
  26. package/bin/report-html.js +134 -0
  27. package/bin/save.js +77 -0
  28. package/bin/shakers.js +344 -0
  29. package/bin/share.js +72 -0
  30. package/bin/show-project.js +72 -0
  31. package/bin/socials.js +70 -0
  32. package/bin/superadmin.js +351 -0
  33. package/package.json +40 -0
  34. package/src/agent-category.js +13 -0
  35. package/src/agent-evaluation.js +177 -0
  36. package/src/agent-org-chart.js +355 -0
  37. package/src/agent-synthesis.js +132 -0
  38. package/src/agent-usage.js +118 -0
  39. package/src/agents-client.js +165 -0
  40. package/src/ai-fluency-cell.js +72 -0
  41. package/src/ai-profile-client.js +45 -0
  42. package/src/ai-profile-preview.js +154 -0
  43. package/src/ai.js +66 -0
  44. package/src/alma-client.js +123 -0
  45. package/src/alma-flow.js +32 -0
  46. package/src/alma-repo-context.js +47 -0
  47. package/src/alma-shell.js +161 -0
  48. package/src/ansi.js +62 -0
  49. package/src/applications-client.js +79 -0
  50. package/src/applications-flow.js +58 -0
  51. package/src/auth-client.js +144 -0
  52. package/src/auth-device-client.js +118 -0
  53. package/src/auth-method-picker.js +44 -0
  54. package/src/auth-methods.js +35 -0
  55. package/src/auth-session-store.js +214 -0
  56. package/src/authorship.js +203 -0
  57. package/src/automations-detector.js +170 -0
  58. package/src/availability-client.js +92 -0
  59. package/src/availability-flow.js +105 -0
  60. package/src/backend-request.js +170 -0
  61. package/src/blind-approval-extract.js +48 -0
  62. package/src/brand-ansi.js +14 -0
  63. package/src/brand-svg.js +9 -0
  64. package/src/browser-tools-detector.js +21 -0
  65. package/src/build-next-level.js +56 -0
  66. package/src/certifications-client.js +89 -0
  67. package/src/certifications-flow.js +75 -0
  68. package/src/certify-dimension-client.js +217 -0
  69. package/src/certify-dimension-flow.js +193 -0
  70. package/src/certify-disclaimer.js +51 -0
  71. package/src/certify-remediation-prompt.js +23 -0
  72. package/src/cli-args.js +83 -0
  73. package/src/config-dir.js +120 -0
  74. package/src/config.js +778 -0
  75. package/src/consent-flow.js +115 -0
  76. package/src/consent-skip.js +27 -0
  77. package/src/decisions-extract.js +43 -0
  78. package/src/detect-repos.js +145 -0
  79. package/src/detected-tools.js +15 -0
  80. package/src/detection-evidence.js +105 -0
  81. package/src/detection-noise.js +132 -0
  82. package/src/detectors.js +221 -0
  83. package/src/device-auth-store.js +100 -0
  84. package/src/device-login.js +118 -0
  85. package/src/email-verification.js +152 -0
  86. package/src/env-paths.js +16 -0
  87. package/src/find-projects-cache.js +89 -0
  88. package/src/find-projects-client.js +138 -0
  89. package/src/find-projects-flow.js +164 -0
  90. package/src/git-activity.js +123 -0
  91. package/src/graph-certs.js +58 -0
  92. package/src/graph-generator.js +230 -0
  93. package/src/graph-scan.js +239 -0
  94. package/src/i18n.js +3264 -0
  95. package/src/inter-fonts.js +6 -0
  96. package/src/interactive-select.js +183 -0
  97. package/src/interview-client.js +226 -0
  98. package/src/interview-transcript.js +56 -0
  99. package/src/inventory-client.js +90 -0
  100. package/src/invitations-client.js +66 -0
  101. package/src/invitations-flow.js +50 -0
  102. package/src/lang-client.js +147 -0
  103. package/src/lang-codes.js +14 -0
  104. package/src/lang-flow.js +85 -0
  105. package/src/legal-notice.js +53 -0
  106. package/src/locale.js +51 -0
  107. package/src/markdown-ansi.js +50 -0
  108. package/src/maturity.js +111 -0
  109. package/src/mcp-ai-usage-result-tool.js +109 -0
  110. package/src/mcp-ai-usage-tool.js +333 -0
  111. package/src/mcp-alma-tools.js +91 -0
  112. package/src/mcp-applications-tools.js +42 -0
  113. package/src/mcp-ask-tools.js +50 -0
  114. package/src/mcp-auth-form.js +282 -0
  115. package/src/mcp-availability-tools.js +43 -0
  116. package/src/mcp-certifications-tools.js +42 -0
  117. package/src/mcp-certify-dimension-tools.js +58 -0
  118. package/src/mcp-detector.js +223 -0
  119. package/src/mcp-device-login-tools.js +173 -0
  120. package/src/mcp-find-projects-tools.js +67 -0
  121. package/src/mcp-install.js +245 -0
  122. package/src/mcp-invitations-tools.js +42 -0
  123. package/src/mcp-lang-tools.js +39 -0
  124. package/src/mcp-onboarding-tools.js +388 -0
  125. package/src/mcp-portfolios-tools.js +46 -0
  126. package/src/mcp-profile-tools.js +46 -0
  127. package/src/mcp-rate-tools.js +42 -0
  128. package/src/mcp-register-tools.js +517 -0
  129. package/src/mcp-report-tools.js +101 -0
  130. package/src/mcp-role-tools.js +112 -0
  131. package/src/mcp-saved-positions-tools.js +55 -0
  132. package/src/mcp-server.js +170 -0
  133. package/src/mcp-show-project-tools.js +50 -0
  134. package/src/mcp-socials-tools.js +39 -0
  135. package/src/memory-structure-detector.js +108 -0
  136. package/src/model-call-record.js +198 -0
  137. package/src/onboarding-availability.js +26 -0
  138. package/src/onboarding-client.js +364 -0
  139. package/src/onboarding-flow.js +1078 -0
  140. package/src/onboarding-livekit-client.js +102 -0
  141. package/src/onboarding-status-client.js +42 -0
  142. package/src/open-file.js +32 -0
  143. package/src/osc-link.js +16 -0
  144. package/src/portfolio-client.js +164 -0
  145. package/src/portfolio-git-info.js +61 -0
  146. package/src/portfolios-client.js +85 -0
  147. package/src/portfolios-flow.js +79 -0
  148. package/src/profile-client.js +125 -0
  149. package/src/profile-flow.js +115 -0
  150. package/src/profile-switch.js +46 -0
  151. package/src/prompt-select.js +29 -0
  152. package/src/rate-client.js +82 -0
  153. package/src/rate-flow.js +137 -0
  154. package/src/render-ai-fluency.js +266 -0
  155. package/src/render-certification.js +177 -0
  156. package/src/render-html.js +140 -0
  157. package/src/render-report-text.js +27 -0
  158. package/src/render-sheet.js +261 -0
  159. package/src/render-terminal.js +109 -0
  160. package/src/render-usage-report-text.js +155 -0
  161. package/src/repo-context.js +215 -0
  162. package/src/report-gating.js +49 -0
  163. package/src/report-store.js +226 -0
  164. package/src/report-theme.js +177 -0
  165. package/src/report-view-client.js +50 -0
  166. package/src/reveal-output.js +29 -0
  167. package/src/roadmap-content.js +453 -0
  168. package/src/roadmap-personalization.js +132 -0
  169. package/src/roadmap-prompt.js +154 -0
  170. package/src/roles-client.js +114 -0
  171. package/src/roles-flow.js +192 -0
  172. package/src/sanitize-network-text.js +36 -0
  173. package/src/saved-positions-client.js +71 -0
  174. package/src/saved-positions-flow.js +50 -0
  175. package/src/scan-exclusions.js +50 -0
  176. package/src/scanner.js +612 -0
  177. package/src/scope-signals.js +148 -0
  178. package/src/secret-prompt.js +72 -0
  179. package/src/session-gate.js +30 -0
  180. package/src/session-refresh.js +51 -0
  181. package/src/session-scan.js +336 -0
  182. package/src/session-signals.js +36 -0
  183. package/src/shakers-wordmark.js +17 -0
  184. package/src/share-card.js +362 -0
  185. package/src/share-certification.js +111 -0
  186. package/src/share-consent-state.js +157 -0
  187. package/src/share-payload.js +300 -0
  188. package/src/share.js +184 -0
  189. package/src/show-project-client.js +58 -0
  190. package/src/show-project-flow.js +119 -0
  191. package/src/signup-client.js +91 -0
  192. package/src/skills-client.js +102 -0
  193. package/src/socials-client.js +91 -0
  194. package/src/socials-flow.js +83 -0
  195. package/src/specificity-extract.js +41 -0
  196. package/src/start-add-agents.js +306 -0
  197. package/src/start-add-portfolio.js +388 -0
  198. package/src/start-add-skills.js +260 -0
  199. package/src/stdin-ask.js +129 -0
  200. package/src/steering-extract.js +43 -0
  201. package/src/strip-internal-comments.js +45 -0
  202. package/src/tech-detector.js +295 -0
  203. package/src/templates/report-sheet.html +572 -0
  204. package/src/terminal-agents.js +199 -0
  205. package/src/terminal-format.js +85 -0
  206. package/src/terminal-progress.js +106 -0
  207. package/src/terminal-sections.js +219 -0
  208. package/src/tier-analysis.js +126 -0
  209. package/src/tier-engine.js +114 -0
  210. package/src/tier-projection.js +117 -0
  211. package/src/transcript-turns.js +49 -0
  212. package/src/translations-client.js +84 -0
  213. package/src/typewriter.js +154 -0
  214. package/src/usage-model-calls.js +119 -0
  215. package/src/usage-oneshot-commands.js +180 -0
  216. package/src/usage-repo-scope.js +162 -0
  217. package/src/usage-run-steps.js +338 -0
  218. package/src/work-streams.js +74 -0
@@ -0,0 +1,165 @@
1
+ 'use strict';
2
+
3
+ const { postJsonWithTimeout } = require('./backend-request');
4
+ const { reasonForError, WORKS_CODE_TO_REASON } = require('./skills-client');
5
+
6
+ // Talent-agent client for `start`'s "Add agent" route (talents-ai-score Phase 2).
7
+
8
+ const DEFAULT_TIMEOUT_MS = 20000;
9
+
10
+ function parseJson(raw) {
11
+ try {
12
+ return raw ? JSON.parse(raw) : null;
13
+ } catch {
14
+ return null;
15
+ }
16
+ }
17
+
18
+ function authHeaders({ accessToken, hubAccessToken } = {}) {
19
+ const headers = {};
20
+ if (typeof accessToken === 'string' && accessToken) headers.Authorization = `Bearer ${accessToken}`;
21
+ if (typeof hubAccessToken === 'string' && hubAccessToken) headers['X-Hub-Token'] = hubAccessToken;
22
+ return headers;
23
+ }
24
+
25
+ // DIRECT-to-Hub headers (the relate route is not a certs relay): the Talent's own hub token IS the Authorization, same shape as portfolio-client's own `hubHeaders`.
26
+ function hubHeaders({ hubAccessToken } = {}) {
27
+ const headers = {};
28
+ if (typeof hubAccessToken === 'string' && hubAccessToken) headers.Authorization = `Bearer ${hubAccessToken}`;
29
+ return headers;
30
+ }
31
+
32
+ // `GET .../agents` -> { ok, agents:[{name}] } | { ok:false, reason }. Used for
33
+ // client-side dedup; a failed list is a named reason, never a silent skip.
34
+ async function requestListAgents({ accessToken, hubAccessToken } = {}, { endpoint, timeoutMs = DEFAULT_TIMEOUT_MS } = {}) {
35
+ if (!endpoint) return { ok: false, reason: 'no-endpoint' };
36
+ if (!hubAccessToken) return { ok: false, reason: 'no-hub-token' };
37
+
38
+ let res;
39
+ try {
40
+ res = await postJsonWithTimeout(endpoint, null, timeoutMs, 'GET', null, authHeaders({ accessToken, hubAccessToken }));
41
+ } catch (e) {
42
+ return { ok: false, reason: (e && e.kind) || 'network-error' };
43
+ }
44
+
45
+ if (res.status < 200 || res.status >= 300) {
46
+ return { ok: false, reason: reasonForError(res.status, res.raw) };
47
+ }
48
+
49
+ const json = parseJson(res.raw);
50
+ const data = json && typeof json === 'object' && Array.isArray(json.data) ? json.data : null;
51
+ if (!data) return { ok: false, reason: 'bad-response' };
52
+
53
+ const agents = data
54
+ .map((entry) => ({
55
+ name: entry && typeof entry.name === 'string' ? entry.name : null,
56
+ id: entry && (typeof entry.id === 'string' || typeof entry.id === 'number') ? String(entry.id) : null,
57
+ }))
58
+ .filter((a) => a.name !== null);
59
+ return { ok: true, agents };
60
+ }
61
+
62
+ // `POST .../agents/declare`. `no-hub-token` fires locally before any request.
63
+ // Optional fields are OMITTED (never null/empty).
64
+ async function requestDeclareAgent(
65
+ { name, catalogId, whatItDoes, humanDecides, accessToken, hubAccessToken } = {},
66
+ { endpoint, timeoutMs = DEFAULT_TIMEOUT_MS } = {},
67
+ ) {
68
+ if (!endpoint) return { ok: false, reason: 'no-endpoint' };
69
+ if (!hubAccessToken) return { ok: false, reason: 'no-hub-token' };
70
+
71
+ const body = { name };
72
+ // Hub's `catalogId` is an Int `num_id` (`@IsInt()` server-side).
73
+ if (typeof catalogId === 'number' && Number.isInteger(catalogId)) body.catalogId = catalogId;
74
+ if (typeof whatItDoes === 'string' && whatItDoes) body.whatItDoes = whatItDoes;
75
+ if (typeof humanDecides === 'string' && humanDecides) body.humanDecides = humanDecides;
76
+
77
+ let res;
78
+ try {
79
+ res = await postJsonWithTimeout(endpoint, body, timeoutMs, 'POST', null, authHeaders({ accessToken, hubAccessToken }));
80
+ } catch (e) {
81
+ return { ok: false, reason: (e && e.kind) || 'network-error' };
82
+ }
83
+
84
+ if (res.status < 200 || res.status >= 300) {
85
+ if (res.status === 409) return { ok: false, reason: 'agent-exists' };
86
+ return { ok: false, reason: reasonForError(res.status, res.raw) };
87
+ }
88
+
89
+ const json = parseJson(res.raw);
90
+ if (!json || json.status !== 'OK') return { ok: false, reason: 'bad-response' };
91
+ // Thread the created agent's id back (relayed from Hub's declare response) so the relate step can PATCH `.../agents/:id/portfolios` (ADR-041).
92
+ const data = json.data && typeof json.data === 'object' ? json.data : null;
93
+ const rawId = data ? data.id : null;
94
+ const agentId = typeof rawId === 'string' || typeof rawId === 'number' ? String(rawId) : null;
95
+ return { ok: true, agentId };
96
+ }
97
+
98
+ // `PATCH {hub}/works/me/agents/:id/portfolios` — DIRECT to Hub (ADR-041), replace-set.
99
+ async function requestRelateAgentPortfolios(
100
+ { agentId, items, hubAccessToken } = {},
101
+ { endpoint, timeoutMs = DEFAULT_TIMEOUT_MS } = {},
102
+ ) {
103
+ if (!endpoint) return { ok: false, reason: 'no-endpoint' };
104
+ if (!hubAccessToken) return { ok: false, reason: 'no-hub-token' };
105
+ if (!agentId) return { ok: false, reason: 'no-agent' };
106
+
107
+ const body = {
108
+ items: (Array.isArray(items) ? items : []).map((it) => ({ portfolioId: it && it.portfolioId })),
109
+ };
110
+
111
+ let res;
112
+ try {
113
+ res = await postJsonWithTimeout(endpoint, body, timeoutMs, 'PATCH', null, hubHeaders({ hubAccessToken }));
114
+ } catch (e) {
115
+ return { ok: false, reason: (e && e.kind) || 'network-error' };
116
+ }
117
+
118
+ if (res.status < 200 || res.status >= 300) {
119
+ return { ok: false, reason: reasonForError(res.status, res.raw) };
120
+ }
121
+ const json = parseJson(res.raw);
122
+ if (!json || (json.success !== true && json.status !== 'OK')) return { ok: false, reason: 'bad-response' };
123
+ return { ok: true };
124
+ }
125
+
126
+ // `POST .../agents/draft-fields` -> { ok, whatItDoes, humanDecides } | { ok:false, reason }.
127
+ // No X-Hub-Token. The CALLER gates consent (ADR-052), so `consent:true` is always sent.
128
+ async function requestDraftAgentFields(
129
+ { name, tools, model, parent, definition, accessToken } = {},
130
+ { endpoint, timeoutMs = DEFAULT_TIMEOUT_MS } = {},
131
+ ) {
132
+ if (!endpoint) return { ok: false, reason: 'no-endpoint' };
133
+
134
+ const body = { consent: true, name };
135
+ if (Array.isArray(tools) && tools.length > 0) body.tools = tools.filter((t) => typeof t === 'string' && t);
136
+ if (typeof model === 'string' && model) body.model = model;
137
+ if (typeof parent === 'string' && parent) body.parent = parent;
138
+ if (typeof definition === 'string' && definition) body.definition = definition;
139
+
140
+ let res;
141
+ try {
142
+ res = await postJsonWithTimeout(endpoint, body, timeoutMs, 'POST', null, authHeaders({ accessToken }));
143
+ } catch (e) {
144
+ return { ok: false, reason: (e && e.kind) || 'network-error' };
145
+ }
146
+
147
+ if (res.status < 200 || res.status >= 300) {
148
+ return { ok: false, reason: reasonForError(res.status, res.raw) };
149
+ }
150
+
151
+ const json = parseJson(res.raw);
152
+ const data = json && json.status === 'OK' && json.data && typeof json.data === 'object' ? json.data : null;
153
+ const whatItDoes = data && typeof data.whatItDoes === 'string' ? data.whatItDoes : null;
154
+ const humanDecides = data && typeof data.humanDecides === 'string' ? data.humanDecides : null;
155
+ if (!whatItDoes || !humanDecides) return { ok: false, reason: 'bad-response' };
156
+ return { ok: true, whatItDoes, humanDecides };
157
+ }
158
+
159
+ module.exports = {
160
+ requestListAgents,
161
+ requestDeclareAgent,
162
+ requestRelateAgentPortfolios,
163
+ requestDraftAgentFields,
164
+ WORKS_CODE_TO_REASON,
165
+ };
@@ -0,0 +1,72 @@
1
+ 'use strict';
2
+
3
+ // Setup(tier) x Usage(interaction) -> 3x3 cell; mapping mirrors certs/hub (keep in
4
+ // sync). Usage level is judged server-side; this only maps already-judged bands.
5
+
6
+ const SETUP_LEVELS = ['ASSISTED', 'EXTENDED', 'ORCHESTRATED'];
7
+ const USAGE_LEVELS = ['REACTIVE', 'DELIBERATE', 'RIGOROUS'];
8
+
9
+ const SETUP_CODE = { ASSISTED: 'S1', EXTENDED: 'S2', ORCHESTRATED: 'S3' };
10
+
11
+ // hub CELL_NAMES, transposed to [setup][usage].
12
+ const CELL_NAMES = {
13
+ ASSISTED: { REACTIVE: 'Explorer', DELIBERATE: 'Practitioner', RIGOROUS: 'Craftsman' },
14
+ EXTENDED: { REACTIVE: 'Tooled-up', DELIBERATE: 'Builder', RIGOROUS: 'Architect' },
15
+ ORCHESTRATED: { REACTIVE: 'Automated', DELIBERATE: 'Scaler', RIGOROUS: 'AI Native' },
16
+ };
17
+
18
+ function deriveSetupLevelFromTier(tier) {
19
+ if (tier === null || tier === undefined || tier <= 0) return null;
20
+ if (tier <= 2) return 'ASSISTED';
21
+ if (tier <= 4) return 'EXTENDED';
22
+ return 'ORCHESTRATED';
23
+ }
24
+
25
+ function deriveUsageLevelFromInteraction(interactionLevel) {
26
+ switch (interactionLevel) {
27
+ case 'reactive':
28
+ return 'REACTIVE';
29
+ case 'directive':
30
+ return 'DELIBERATE';
31
+ case 'orchestrative':
32
+ return 'RIGOROUS';
33
+ default:
34
+ return null;
35
+ }
36
+ }
37
+
38
+ function deriveAiFluencyCell(setupLevel, usageLevel) {
39
+ if (!setupLevel || !usageLevel) return null;
40
+ const row = CELL_NAMES[setupLevel];
41
+ return (row && row[usageLevel]) || null;
42
+ }
43
+
44
+ function isAiNative(setupLevel, usageLevel) {
45
+ return setupLevel === 'ORCHESTRATED' && usageLevel === 'RIGOROUS';
46
+ }
47
+
48
+ // tier (0-7) + interactionLevel ('reactive'|'directive'|'orchestrative') -> matrix.
49
+ // Null bands when an input is missing (setup-only / usage-pending).
50
+ function composeAiFluencyMatrix({ tier = null, interactionLevel = null } = {}) {
51
+ const setupLevel = deriveSetupLevelFromTier(tier);
52
+ const usageLevel = deriveUsageLevelFromInteraction(interactionLevel);
53
+ return {
54
+ setupLevel,
55
+ setupCode: setupLevel ? SETUP_CODE[setupLevel] : null,
56
+ usageLevel,
57
+ cell: deriveAiFluencyCell(setupLevel, usageLevel),
58
+ isAiNative: isAiNative(setupLevel, usageLevel),
59
+ };
60
+ }
61
+
62
+ module.exports = {
63
+ SETUP_LEVELS,
64
+ USAGE_LEVELS,
65
+ SETUP_CODE,
66
+ CELL_NAMES,
67
+ deriveSetupLevelFromTier,
68
+ deriveUsageLevelFromInteraction,
69
+ deriveAiFluencyCell,
70
+ isAiNative,
71
+ composeAiFluencyMatrix,
72
+ };
@@ -0,0 +1,45 @@
1
+ 'use strict';
2
+
3
+ // GET hub `works/me/ai-profile` — the same read model the "My work with AI" front
4
+ // paints: { setup, usage, cell, isAiNative, traction, vision, howIWork }.
5
+
6
+ const { postJsonWithTimeout } = require('./backend-request');
7
+
8
+ const DEFAULT_TIMEOUT_MS = 20000;
9
+
10
+ function parseJson(raw) {
11
+ try {
12
+ return raw ? JSON.parse(raw) : null;
13
+ } catch {
14
+ return null;
15
+ }
16
+ }
17
+
18
+ function authHeaders(token) {
19
+ const headers = {};
20
+ if (typeof token === 'string' && token) headers.Authorization = `Bearer ${token}`;
21
+ return headers;
22
+ }
23
+
24
+ async function requestAiProfile({ accessToken, hubAccessToken } = {}, { endpoint, timeoutMs = DEFAULT_TIMEOUT_MS } = {}) {
25
+ if (!endpoint) return { ok: false, reason: 'no-endpoint' };
26
+ const token = hubAccessToken || accessToken;
27
+
28
+ let res;
29
+ try {
30
+ res = await postJsonWithTimeout(endpoint, null, timeoutMs, 'GET', null, authHeaders(token));
31
+ } catch (e) {
32
+ return { ok: false, reason: (e && e.kind) || 'network-error' };
33
+ }
34
+
35
+ // 404 = no profile row yet (projection hasn't landed) — a retriable "not ready".
36
+ if (res.status === 404) return { ok: false, reason: 'not-found' };
37
+ if (res.status < 200 || res.status >= 300) return { ok: false, reason: `http-${res.status}` };
38
+
39
+ const json = parseJson(res.raw);
40
+ const data = json && typeof json === 'object' ? json.data : null;
41
+ if (!data || typeof data !== 'object') return { ok: false, reason: 'bad-response' };
42
+ return { ok: true, profile: data };
43
+ }
44
+
45
+ module.exports = { requestAiProfile };
@@ -0,0 +1,154 @@
1
+ 'use strict';
2
+
3
+ // Polls the hub ai-profile (downstream of the report ingest, lands ~3-8s later) and normalizes it for the report/MCP.
4
+
5
+ const { SETUP_CODE } = require('./ai-fluency-cell');
6
+
7
+ const DEFAULT_TIMEOUT_MS = 90000;
8
+ const POLL_INTERVAL_MS = 4000;
9
+ const PROFILE_POLL_WINDOW_MS = DEFAULT_TIMEOUT_MS;
10
+ const PROFILE_POLL_INTERVAL_MS = POLL_INTERVAL_MS;
11
+ const MAX_TOKEN_REFRESHES = 2;
12
+ const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
13
+
14
+ const strOrNull = (v) => (typeof v === 'string' && v.trim() ? v.trim() : null);
15
+ const numOrNull = (v) => (typeof v === 'number' && Number.isFinite(v) ? v : null);
16
+
17
+ // Tolerant normalization of the hub TalentAiProfileDto into the render shape.
18
+ function normalizeProfile(profile) {
19
+ const p = profile && typeof profile === 'object' ? profile : {};
20
+ const setup = p.setup && typeof p.setup === 'object' ? p.setup : {};
21
+ const usage = p.usage && typeof p.usage === 'object' ? p.usage : {};
22
+ const setupLevel = strOrNull(setup.level);
23
+ const usageLevel = strOrNull(usage.level);
24
+ const t = p.traction && typeof p.traction === 'object' ? p.traction : null;
25
+ const tools = t && Array.isArray(t.tools)
26
+ ? t.tools.map((x) => ({
27
+ id: strOrNull(x && (x.id ?? x.name)),
28
+ name: strOrNull(x && (x.name ?? x.id)),
29
+ detected: x ? x.detected === true : false,
30
+ })).filter((x) => x.id || x.name)
31
+ : null;
32
+ const vision = p.vision && typeof p.vision === 'object' ? p.vision : null;
33
+ const howIWork = p.howIWork && typeof p.howIWork === 'object' ? p.howIWork : null;
34
+
35
+ return {
36
+ matrix: {
37
+ setupLevel,
38
+ setupCode: setupLevel ? (SETUP_CODE[setupLevel] || null) : null,
39
+ usageLevel,
40
+ cell: strOrNull(p.cell),
41
+ isAiNative: p.isAiNative === true,
42
+ },
43
+ setup: { tier: strOrNull(setup.tier), level: setupLevel, explanation: strOrNull(setup.explanation) },
44
+ usage: { level: usageLevel, explanation: strOrNull(usage.explanation) },
45
+ traction: t
46
+ ? {
47
+ sessions90d: numOrNull(t.sessions90d),
48
+ activeDaysPerWeek: numOrNull(t.activeDaysPerWeek),
49
+ agentsDetected: numOrNull(t.agentsDetected),
50
+ agentsOnProfile: numOrNull(t.agentsOnProfile),
51
+ tools,
52
+ }
53
+ : null,
54
+ vision: vision ? strOrNull(vision.text) : null,
55
+ howIWork: howIWork ? strOrNull(howIWork.body) : null,
56
+ };
57
+ }
58
+
59
+ // The matrix band is populated once the CLI footprint tier has been projected.
60
+ function matrixPresent(preview) {
61
+ const m = preview && preview.matrix;
62
+ return !!(m && (m.setupLevel || m.usageLevel || m.cell));
63
+ }
64
+
65
+ // Anything renderable: the matrix band OR the vision (E) OR how-I-work (F). The
66
+ // three land on independent projections, so the render must not gate on the matrix.
67
+ function hasRenderableProfile(preview) {
68
+ return !!preview && (matrixPresent(preview) || !!preview.vision || !!preview.howIWork);
69
+ }
70
+
71
+ // The full profile: matrix band AND both narratives. Once complete, stop polling.
72
+ function isProfileComplete(preview) {
73
+ return matrixPresent(preview) && !!preview.vision && !!preview.howIWork;
74
+ }
75
+
76
+ // How many of the three parts (matrix, E, F) landed — to keep the most complete partial.
77
+ function profileCompleteness(preview) {
78
+ if (!preview) return -1;
79
+ return (matrixPresent(preview) ? 1 : 0) + (preview.vision ? 1 : 0) + (preview.howIWork ? 1 : 0);
80
+ }
81
+
82
+ // Back-compat alias (matrix-band meaning preserved for external callers).
83
+ const isProfileReady = matrixPresent;
84
+
85
+ // deps (injectable for tests): requestAiProfile, now, wait, pollIntervalMs.
86
+ async function resolveAiProfilePreview(args = {}, deps = {}) {
87
+ const { session = null, endpoint = null, timeoutMs = DEFAULT_TIMEOUT_MS } = args;
88
+ const {
89
+ requestAiProfile = require('./ai-profile-client').requestAiProfile,
90
+ refreshToken = null,
91
+ now = () => Date.now(),
92
+ wait = sleep,
93
+ pollIntervalMs = POLL_INTERVAL_MS,
94
+ } = deps;
95
+
96
+ let accessToken = session && typeof session.accessToken === 'string' ? session.accessToken : null;
97
+ let hubAccessToken = session && typeof session.hubAccessToken === 'string' ? session.hubAccessToken : null;
98
+ if ((!accessToken && !hubAccessToken) || !endpoint) return { status: 'unavailable', preview: null };
99
+
100
+ const deadline = now() + Math.max(0, timeoutMs);
101
+ // The matrix band, the vision (E) and howIWork (F) land on INDEPENDENT projections and
102
+ // in any order (the matrix can even lag to the hourly reconcile cron). Keep the most
103
+ // complete partial seen and render it — never gate the whole preview on the matrix band.
104
+ let best = null;
105
+ let refreshes = 0;
106
+
107
+ while (true) {
108
+ const remaining = deadline - now();
109
+ if (remaining <= 0) break;
110
+
111
+ let res;
112
+ try {
113
+ res = await requestAiProfile({ accessToken, hubAccessToken }, { endpoint, timeoutMs: remaining });
114
+ } catch {
115
+ res = { ok: false, reason: 'network-error' };
116
+ }
117
+
118
+ if (res && res.ok) {
119
+ const preview = normalizeProfile(res.profile);
120
+ if (isProfileComplete(preview)) return { status: 'ready', preview };
121
+ if (profileCompleteness(preview) > profileCompleteness(best)) best = preview;
122
+ } else if (res && res.reason === 'http-401' && typeof refreshToken === 'function' && refreshes < MAX_TOKEN_REFRESHES) {
123
+ refreshes += 1;
124
+ let fresh = null;
125
+ try { fresh = await refreshToken(); } catch { fresh = null; }
126
+ if (fresh && (fresh.accessToken || fresh.hubAccessToken)) {
127
+ accessToken = typeof fresh.accessToken === 'string' ? fresh.accessToken : accessToken;
128
+ hubAccessToken = typeof fresh.hubAccessToken === 'string' ? fresh.hubAccessToken : hubAccessToken;
129
+ continue;
130
+ }
131
+ }
132
+
133
+ if (deadline - now() <= pollIntervalMs) break;
134
+ await wait(pollIntervalMs);
135
+ }
136
+
137
+ // Anything renderable (matrix OR E OR F) is surfaced as `ready`; the renderers degrade
138
+ // per-field. Only a genuinely empty profile stays `pending`.
139
+ if (hasRenderableProfile(best)) return { status: 'ready', preview: best };
140
+ return { status: 'pending', preview: null };
141
+ }
142
+
143
+ module.exports = {
144
+ resolveAiProfilePreview,
145
+ normalizeProfile,
146
+ isProfileReady,
147
+ matrixPresent,
148
+ hasRenderableProfile,
149
+ isProfileComplete,
150
+ profileCompleteness,
151
+ DEFAULT_TIMEOUT_MS,
152
+ PROFILE_POLL_WINDOW_MS,
153
+ PROFILE_POLL_INTERVAL_MS,
154
+ };
package/src/ai.js ADDED
@@ -0,0 +1,66 @@
1
+ 'use strict';
2
+
3
+ // The single named surface for every model-backed (LLM) call this CLI makes (talents-ai-score, arch-cleanup #4).
4
+
5
+ const agentSynthesis = require('./agent-synthesis');
6
+ const agentEvaluation = require('./agent-evaluation');
7
+ const roadmapPersonalization = require('./roadmap-personalization');
8
+ const onboardingClient = require('./onboarding-client');
9
+ const config = require('./config');
10
+
11
+ // The model calls, one entry each, paired with the request-builder, endpoint-getter and promptVersion pointer that define them. The skill-code certify/interview trio was removed when `certify` became a dimension LiveKit interview (its turns run over the onboarding HTTP/LiveKit surface, not a dedicated model call here).
12
+ const AI_CALLS = Object.freeze({
13
+ agentSynthesis: {
14
+ call: agentSynthesis.requestAgentSynthesis,
15
+ build: agentSynthesis.buildSynthesisRequest,
16
+ endpoint: config.getSynthesisEndpoint,
17
+ promptVersion: null,
18
+ },
19
+ agentEvaluation: {
20
+ call: agentEvaluation.requestAgentEvaluation,
21
+ build: agentEvaluation.buildAgentEvaluationRequest,
22
+ endpoint: config.getAgentEvaluationEndpoint,
23
+ promptVersion: agentEvaluation.PROMPT_VERSION,
24
+ },
25
+ roadmapPersonalization: {
26
+ call: roadmapPersonalization.requestRoadmapPersonalization,
27
+ build: roadmapPersonalization.buildRoadmapPersonalizationRequest,
28
+ endpoint: config.getRoadmapEndpoint,
29
+ promptVersion: null,
30
+ },
31
+ onboardingOpen: {
32
+ call: onboardingClient.requestStartTextSession,
33
+ build: null,
34
+ endpoint: config.getOnboardingInterviewsEndpoint,
35
+ promptVersion: null,
36
+ },
37
+ onboardingTurn: {
38
+ call: onboardingClient.requestOnboardingTurn,
39
+ build: null,
40
+ endpoint: config.getOnboardingInterviewsEndpoint,
41
+ promptVersion: null,
42
+ },
43
+ onboardingComplete: {
44
+ call: onboardingClient.requestCompleteTextSession,
45
+ build: null,
46
+ endpoint: config.getOnboardingInterviewsEndpoint,
47
+ promptVersion: null,
48
+ },
49
+ });
50
+
51
+ module.exports = {
52
+ AI_CALLS,
53
+
54
+ requestAgentSynthesis: agentSynthesis.requestAgentSynthesis,
55
+ buildSynthesisRequest: agentSynthesis.buildSynthesisRequest,
56
+
57
+ requestAgentEvaluation: agentEvaluation.requestAgentEvaluation,
58
+ buildAgentEvaluationRequest: agentEvaluation.buildAgentEvaluationRequest,
59
+
60
+ requestRoadmapPersonalization: roadmapPersonalization.requestRoadmapPersonalization,
61
+ buildRoadmapPersonalizationRequest: roadmapPersonalization.buildRoadmapPersonalizationRequest,
62
+
63
+ requestStartTextSession: onboardingClient.requestStartTextSession,
64
+ requestOnboardingTurn: onboardingClient.requestOnboardingTurn,
65
+ requestCompleteTextSession: onboardingClient.requestCompleteTextSession,
66
+ };
@@ -0,0 +1,123 @@
1
+ 'use strict';
2
+
3
+ // Alma client: POST `/alma/chat` (turn) and `/alma/decision` (approve/deny a
4
+ // pending action) with the talent's hub Bearer (Alma verifies it against the hub
5
+ // JWKS, ES256). Response is an NDJSON stream — one JSON event per line:
6
+ // {type:'turn_started'|'text_delta'|'confirmation_required'|'link'|...}. Parsed
7
+ // incrementally so the CLI can print tokens live. Zero-dep (http/https only).
8
+
9
+ const http = require('http');
10
+ const https = require('https');
11
+ const { URL } = require('url');
12
+ const { reasonForError } = require('./onboarding-client');
13
+
14
+ const DEFAULT_TIMEOUT_MS = 120000;
15
+
16
+ // Streams one NDJSON turn to `endpoint` with `body`. `onEvent(evt)` fires per line;
17
+ // resolves with the aggregated assistant text (the concatenated text_delta chunks).
18
+ function streamNdjson(endpoint, body, { hubAccessToken, onEvent, timeoutMs = DEFAULT_TIMEOUT_MS, requestImpl } = {}) {
19
+ return new Promise((resolve) => {
20
+ if (!endpoint) return resolve({ ok: false, reason: 'no-endpoint' });
21
+ if (!hubAccessToken) return resolve({ ok: false, reason: 'no-hub-token' });
22
+
23
+ let url;
24
+ try {
25
+ url = new URL(endpoint);
26
+ } catch {
27
+ return resolve({ ok: false, reason: 'bad-endpoint' });
28
+ }
29
+ const lib = requestImpl || (url.protocol === 'https:' ? https : http);
30
+ const payload = JSON.stringify(body);
31
+ const deltas = [];
32
+ let settled = false;
33
+ const done = (v) => { if (!settled) { settled = true; resolve(v); } };
34
+
35
+ const req = lib.request(
36
+ {
37
+ hostname: url.hostname,
38
+ port: url.port || (url.protocol === 'https:' ? 443 : 80),
39
+ path: `${url.pathname}${url.search}`,
40
+ method: 'POST',
41
+ headers: {
42
+ 'content-type': 'application/json',
43
+ 'content-length': Buffer.byteLength(payload),
44
+ accept: 'application/x-ndjson',
45
+ authorization: `Bearer ${hubAccessToken}`,
46
+ },
47
+ timeout: timeoutMs,
48
+ },
49
+ (res) => {
50
+ if (res.statusCode < 200 || res.statusCode >= 300) {
51
+ let err = '';
52
+ res.on('data', (c) => { err += c; });
53
+ res.on('end', () => done({ ok: false, reason: reasonForError(res.statusCode, err) }));
54
+ return;
55
+ }
56
+ res.setEncoding('utf8');
57
+ let buffer = '';
58
+ const handleLine = (line) => {
59
+ const s = line.trim();
60
+ if (!s) return;
61
+ let evt;
62
+ try { evt = JSON.parse(s); } catch { return; }
63
+ if (evt && evt.type === 'text_delta' && typeof evt.text === 'string') deltas.push(evt.text);
64
+ if (typeof onEvent === 'function') onEvent(evt);
65
+ };
66
+ res.on('data', (chunk) => {
67
+ buffer += chunk;
68
+ let idx;
69
+ while ((idx = buffer.indexOf('\n')) >= 0) {
70
+ handleLine(buffer.slice(0, idx));
71
+ buffer = buffer.slice(idx + 1);
72
+ }
73
+ });
74
+ // A stream error AFTER the headers (mid-stream reset) has no other
75
+ // handler — without this the promise would never settle (hang).
76
+ res.on('error', (e) => done({ ok: false, reason: (e && e.code) || 'stream-error' }));
77
+ res.on('end', () => { handleLine(buffer); done({ ok: true, text: deltas.join('') }); });
78
+ },
79
+ );
80
+ req.on('error', (e) => done({ ok: false, reason: (e && e.code) || 'network-error' }));
81
+ req.on('timeout', () => { req.destroy(); done({ ok: false, reason: 'timeout' }); });
82
+ req.write(payload);
83
+ req.end();
84
+ });
85
+ }
86
+
87
+ function streamAlmaChat({ hubAccessToken, message } = {}, opts = {}) {
88
+ if (typeof message !== 'string' || !message.trim()) return Promise.resolve({ ok: false, reason: 'no-message' });
89
+ return streamNdjson(opts.endpoint, { message, source: 'shakers-cli' }, { hubAccessToken, ...opts });
90
+ }
91
+
92
+ function streamAlmaDecision({ hubAccessToken, approved } = {}, opts = {}) {
93
+ if (typeof approved !== 'boolean') return Promise.resolve({ ok: false, reason: 'no-decision' });
94
+ return streamNdjson(opts.endpoint, { approved }, { hubAccessToken, ...opts });
95
+ }
96
+
97
+ // CLI+MCP entry: resolve the endpoint from config, then stream one turn.
98
+ async function askAlma(deps = {}, { hubAccessToken, message, onEvent, timeoutMs = DEFAULT_TIMEOUT_MS } = {}) {
99
+ const {
100
+ getAlmaChatEndpoint = require('./config').getAlmaChatEndpoint,
101
+ streamAlmaChat: stream = streamAlmaChat,
102
+ } = deps;
103
+ const endpoint = getAlmaChatEndpoint();
104
+ if (!endpoint) return { ok: false, reason: 'no-endpoint' };
105
+ return stream({ hubAccessToken, message }, { endpoint, onEvent, timeoutMs });
106
+ }
107
+
108
+ async function decideAlma(deps = {}, { hubAccessToken, approved, onEvent, timeoutMs = DEFAULT_TIMEOUT_MS } = {}) {
109
+ const {
110
+ getAlmaDecisionEndpoint = require('./config').getAlmaDecisionEndpoint,
111
+ streamAlmaDecision: stream = streamAlmaDecision,
112
+ } = deps;
113
+ const endpoint = getAlmaDecisionEndpoint();
114
+ if (!endpoint) return { ok: false, reason: 'no-endpoint' };
115
+ return stream({ hubAccessToken, approved }, { endpoint, onEvent, timeoutMs });
116
+ }
117
+
118
+ module.exports = {
119
+ streamAlmaChat,
120
+ streamAlmaDecision,
121
+ askAlma,
122
+ decideAlma,
123
+ };
@@ -0,0 +1,32 @@
1
+ 'use strict';
2
+
3
+ const EXIT_WORDS = new Set(['exit', 'quit', 'salir', 'q']);
4
+
5
+ function makeAlmaDeps(overrides = {}) {
6
+ const client = require('./alma-client');
7
+ return {
8
+ askAlma: (opts) => client.askAlma({}, opts),
9
+ decideAlma: (opts) => client.decideAlma({}, opts),
10
+ startSpinner: (label) => require('./terminal-progress').startSpinner(label),
11
+ collectContextBlock: (o) => require('./alma-repo-context').collectContextBlock(o),
12
+ withStatus: (label, fn) => require('./terminal-progress').withStaticStatus(label, fn),
13
+ ...overrides,
14
+ };
15
+ }
16
+
17
+ async function oneTurn({ io, hubAccessToken, message, catalog, deps, onStreamEvent = null, sendTurn = null }) {
18
+ const fp = catalog.ask;
19
+ const stop = deps.startSpinner ? deps.startSpinner(fp.thinking) : () => {};
20
+ const onEvent = (evt) => { if (typeof onStreamEvent === 'function') onStreamEvent(evt); };
21
+ const call = sendTurn || ((o) => deps.askAlma(o));
22
+ const res = await call({ hubAccessToken, message, onEvent });
23
+ stop();
24
+ if (!res.ok) io.error(fp.failed(res.reason));
25
+ return res;
26
+ }
27
+
28
+ module.exports = {
29
+ makeAlmaDeps,
30
+ oneTurn,
31
+ EXIT_WORDS,
32
+ };