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,226 @@
1
+ 'use strict';
2
+
3
+ // InterviewClient — the TEXT-over-LiveKit transport for a turn-based interview.
4
+
5
+ const CHAT_TOPIC = 'lk.chat';
6
+ // The agent's own protocol channel; it announces the end there with `interview-complete`.
7
+ const PROTOCOL_TOPIC = 'interview-protocol';
8
+ const DEFAULT_TURN_TIMEOUT_MS = 90000;
9
+ // A message is held this long before it is shown, so an `interview-complete`
10
+ // sent right after the goodbye marks it as the last one instead of prompting
11
+ // the Talent to answer a farewell.
12
+ const DEFAULT_SETTLE_MS = 400;
13
+
14
+ class LiveKitNotInstalledError extends Error {
15
+ constructor(cause) {
16
+ super('The optional @livekit/rtc-node package is not installed');
17
+ this.name = 'LiveKitNotInstalledError';
18
+ this.kind = 'livekit-not-installed';
19
+ this.cause = cause || null;
20
+ }
21
+ }
22
+
23
+ // Mute rtc-node's pino logger ("lk-rtc").
24
+ function silenceLiveKitLogger() {
25
+ try {
26
+ const path = require('path');
27
+ const logPath = path.join(path.dirname(require.resolve('@livekit/rtc-node')), 'log.cjs');
28
+ const logModule = require(logPath);
29
+ if (logModule && logModule.log) logModule.log.level = 'silent';
30
+ } catch {}
31
+ }
32
+
33
+ // The ONLY place the native package is required.
34
+ function loadLiveKitSdk() {
35
+ try {
36
+ // eslint-disable-next-line global-require
37
+ const sdk = require('@livekit/rtc-node');
38
+ silenceLiveKitLogger();
39
+ return sdk;
40
+ } catch (e) {
41
+ throw new LiveKitNotInstalledError(e);
42
+ }
43
+ }
44
+
45
+ class InterviewClient {
46
+ constructor({ topic = CHAT_TOPIC, turnTimeoutMs = DEFAULT_TURN_TIMEOUT_MS, sdk = null, isClosing = null, settleMs = DEFAULT_SETTLE_MS } = {}) {
47
+ this._topic = topic;
48
+ this._turnTimeoutMs = turnTimeoutMs;
49
+ this._sdk = sdk; // injectable; null => lazy-require the native SDK at connect()
50
+ this._isClosing = typeof isClosing === 'function' ? isClosing : null;
51
+ this._room = null;
52
+ this._queue = []; // completed agent messages waiting to be delivered
53
+ this._waiter = null; // { resolve, timer } for a pending receiveTurn()
54
+ this._disconnected = false;
55
+ this._connected = false;
56
+ this._settleMs = settleMs;
57
+ this._settling = 0; // messages still inside their settle window
58
+ this._ended = false; // the agent said the interview is over
59
+ }
60
+
61
+ get connected() {
62
+ return this._connected;
63
+ }
64
+
65
+ // Join the room.
66
+ async connect(livekitUrl, token, opts = {}) {
67
+ if (this._connected) throw new Error('already-connected');
68
+ if (!livekitUrl || !token) throw new Error('missing-credentials');
69
+ // A client reused after disconnect() starts clean, not already ended.
70
+ this._ended = false;
71
+ this._disconnected = false;
72
+ this._settling = 0;
73
+ this._queue = [];
74
+
75
+ const sdk = this._sdk || loadLiveKitSdk();
76
+ this._sdk = sdk;
77
+ const { Room, RoomEvent } = sdk;
78
+ const room = new Room();
79
+ this._room = room;
80
+
81
+ if (typeof room.registerTextStreamHandler !== 'function') {
82
+ // Text streams landed in @livekit/rtc-node ~0.13; older builds cannot carry lk.chat.
83
+ throw Object.assign(new Error('installed @livekit/rtc-node is too old for text streams'), { kind: 'livekit-no-text-streams' });
84
+ }
85
+
86
+ room.registerTextStreamHandler(this._topic, (reader) => {
87
+ // Counted as settling from the moment the stream opens, so an end signal that
88
+ // lands while a long goodbye is still being read waits for it.
89
+ this._settling += 1;
90
+ Promise.resolve()
91
+ .then(() => reader.readAll())
92
+ .then((text) => this._settle(String(text == null ? '' : text)))
93
+ // A failed read yields no turn; the loop still ends via the end signal, timeout or Disconnected.
94
+ .catch(() => this._settle(null));
95
+ });
96
+
97
+ if (RoomEvent && RoomEvent.Disconnected && typeof room.on === 'function') {
98
+ room.on(RoomEvent.Disconnected, () => this._markDisconnected());
99
+ }
100
+
101
+ // The end of the interview is a protocol message, not a sentence: matching the
102
+ // goodbye text never fired (certs sends no closingMessage), so the CLI kept
103
+ // asking for answers until its turn-timeout (new-works, 2026-09-25).
104
+ if (RoomEvent && RoomEvent.DataReceived && typeof room.on === 'function') {
105
+ room.on(RoomEvent.DataReceived, (payload, _participant, _kind, topic) => {
106
+ if (topic !== PROTOCOL_TOPIC) return;
107
+ let message = null;
108
+ try {
109
+ message = JSON.parse(Buffer.from(payload).toString('utf8'));
110
+ } catch {
111
+ return;
112
+ }
113
+ if (message && message.type === 'interview-complete') this._markEnded();
114
+ });
115
+ }
116
+
117
+ await room.connect(livekitUrl, token, opts);
118
+ this._connected = true;
119
+ return { ok: true };
120
+ }
121
+
122
+ // Resolve the NEXT agent message.
123
+ receiveTurn() {
124
+ return new Promise((resolve, reject) => {
125
+ if (this._queue.length) return resolve(this._deliver(this._queue.shift()));
126
+ if (this._disconnected || (this._ended && this._settling === 0)) return resolve({ text: '', ended: true });
127
+ const timer = setTimeout(() => {
128
+ this._waiter = null;
129
+ reject(Object.assign(new Error('turn-timeout'), { kind: 'turn-timeout' }));
130
+ }, this._turnTimeoutMs);
131
+ this._waiter = { resolve, timer };
132
+ });
133
+ }
134
+
135
+ // Send one candidate turn as lk.chat text, then wait for the agent's reply.
136
+ async sendTurn(text) {
137
+ if (!this._connected) throw new Error('not-connected');
138
+ const lp = this._room && this._room.localParticipant;
139
+ if (!lp || typeof lp.sendText !== 'function') throw new Error('no-local-participant');
140
+ await lp.sendText(String(text == null ? '' : text), { topic: this._topic });
141
+ return this.receiveTurn();
142
+ }
143
+
144
+ async disconnect() {
145
+ const room = this._room;
146
+ const sdk = this._sdk;
147
+ this._room = null;
148
+ this._connected = false;
149
+ if (this._waiter) {
150
+ const w = this._waiter;
151
+ this._waiter = null;
152
+ clearTimeout(w.timer);
153
+ w.resolve({ text: '', ended: true });
154
+ }
155
+ if (room) {
156
+ try { room.unregisterTextStreamHandler(this._topic); } catch {}
157
+ try { await room.disconnect(); } catch {}
158
+ }
159
+ // The FFI runtime keeps the event loop alive; dispose lets a one-shot CLI exit.
160
+ if (sdk && typeof sdk.dispose === 'function') {
161
+ try { await sdk.dispose(); } catch {}
162
+ }
163
+ }
164
+
165
+ _deliver(text) {
166
+ const ended = this._ended || (this._isClosing ? !!this._isClosing(text) : false);
167
+ return { text, ended };
168
+ }
169
+
170
+ _settle(text) {
171
+ const done = () => {
172
+ this._settling -= 1;
173
+ if (text !== null) this._release(text);
174
+ else if (this._ended) this._resolveEnded();
175
+ };
176
+ if (this._settleMs > 0) setTimeout(done, this._settleMs);
177
+ else done();
178
+ }
179
+
180
+ _resolveEnded() {
181
+ if (this._settling > 0 || !this._waiter || this._queue.length) return;
182
+ const w = this._waiter;
183
+ this._waiter = null;
184
+ clearTimeout(w.timer);
185
+ w.resolve({ text: '', ended: true });
186
+ }
187
+
188
+ _release(text) {
189
+ if (this._waiter) {
190
+ const w = this._waiter;
191
+ this._waiter = null;
192
+ clearTimeout(w.timer);
193
+ w.resolve(this._deliver(text));
194
+ } else {
195
+ this._queue.push(text);
196
+ }
197
+ }
198
+
199
+ _markEnded() {
200
+ this._ended = true;
201
+ // The goodbye and the end signal travel on different channels and can land in
202
+ // either order: give a goodbye still being read the same window to arrive, so
203
+ // it is shown as the last message instead of being cut off. A message still
204
+ // settling carries `ended` itself when it is released.
205
+ setTimeout(() => this._resolveEnded(), this._settleMs > 0 ? this._settleMs : 0);
206
+ }
207
+
208
+ _markDisconnected() {
209
+ this._disconnected = true;
210
+ if (this._waiter && !this._queue.length) {
211
+ const w = this._waiter;
212
+ this._waiter = null;
213
+ clearTimeout(w.timer);
214
+ w.resolve({ text: '', ended: true });
215
+ }
216
+ }
217
+ }
218
+
219
+ module.exports = {
220
+ InterviewClient,
221
+ LiveKitNotInstalledError,
222
+ loadLiveKitSdk,
223
+ CHAT_TOPIC,
224
+ PROTOCOL_TOPIC,
225
+ DEFAULT_TURN_TIMEOUT_MS,
226
+ };
@@ -0,0 +1,56 @@
1
+ 'use strict';
2
+
3
+ // TranscriptBuffer — records the onboarding interview Q&A as it happens over LiveKit text (lk.chat), in the exact shape certs' `PATCH /interviews/:id/ complete` persists.
4
+
5
+ // Upper case: certs validates the role against its AGENT/USER enum and 400s the
6
+ // whole PATCH otherwise, so every terminal onboarding lost its transcript
7
+ // ("no se pudo guardar la transcripcion (http-400)", new-works 2026-09-25).
8
+ const ROLE_AGENT = 'AGENT';
9
+ const ROLE_USER = 'USER';
10
+
11
+ class TranscriptBuffer {
12
+ constructor({ now = Date.now } = {}) {
13
+ this._now = typeof now === 'function' ? now : Date.now;
14
+ this._turns = [];
15
+ this._firstAt = null;
16
+ }
17
+
18
+ // Returns true if the turn was recorded, false if dropped (empty / bad role).
19
+ record(role, message) {
20
+ if (role !== ROLE_AGENT && role !== ROLE_USER) return false;
21
+ const msg = String(message == null ? '' : message).trim();
22
+ if (!msg) return false;
23
+ const at = this._now();
24
+ if (this._firstAt === null) this._firstAt = at;
25
+ this._turns.push({
26
+ role,
27
+ message: msg,
28
+ timestamp: new Date(at).toISOString(),
29
+ timeInCallSecs: Math.max(0, Math.floor((at - this._firstAt) / 1000)),
30
+ });
31
+ return true;
32
+ }
33
+
34
+ recordAgent(message) {
35
+ return this.record(ROLE_AGENT, message);
36
+ }
37
+
38
+ recordUser(message) {
39
+ return this.record(ROLE_USER, message);
40
+ }
41
+
42
+ get length() {
43
+ return this._turns.length;
44
+ }
45
+
46
+ // A defensive copy so callers cannot mutate the buffered turns.
47
+ toTranscripts() {
48
+ return this._turns.map((t) => ({ ...t }));
49
+ }
50
+ }
51
+
52
+ module.exports = {
53
+ TranscriptBuffer,
54
+ ROLE_AGENT,
55
+ ROLE_USER,
56
+ };
@@ -0,0 +1,90 @@
1
+ 'use strict';
2
+
3
+ const { postJsonWithTimeout } = require('./backend-request');
4
+
5
+ const DEFAULT_TIMEOUT_MS = 20000;
6
+
7
+ function parseJson(raw) {
8
+ try {
9
+ return raw ? JSON.parse(raw) : null;
10
+ } catch {
11
+ return null;
12
+ }
13
+ }
14
+
15
+ function authHeaders({ accessToken } = {}) {
16
+ const headers = {};
17
+ if (typeof accessToken === 'string' && accessToken) headers.Authorization = `Bearer ${accessToken}`;
18
+ return headers;
19
+ }
20
+
21
+ function normalizeSkill(entry) {
22
+ if (!entry || typeof entry !== 'object') return null;
23
+ const skillId = typeof entry.skillId === 'number' ? entry.skillId : null;
24
+ const skillName = typeof entry.skillName === 'string' ? entry.skillName : null;
25
+ if (skillId === null || !skillName) return null;
26
+ const technologies = Array.isArray(entry.technologies)
27
+ ? entry.technologies.filter((tech) => typeof tech === 'string' && tech)
28
+ : [];
29
+ return { skillId, skillName, technologies };
30
+ }
31
+
32
+ function normalizeAgent(entry) {
33
+ if (!entry || typeof entry !== 'object') return null;
34
+ const name = typeof entry.name === 'string' ? entry.name : null;
35
+ if (!name) return null;
36
+ const tools = Array.isArray(entry.tools)
37
+ ? entry.tools.filter((tool) => typeof tool === 'string' && tool)
38
+ : [];
39
+ const model = typeof entry.model === 'string' && entry.model ? entry.model : null;
40
+ const category = typeof entry.category === 'string' && entry.category ? entry.category : null;
41
+ const role = typeof entry.role === 'string' && entry.role ? entry.role : null;
42
+ const level = typeof entry.level === 'string' && entry.level ? entry.level : null;
43
+ const code = typeof entry.code === 'string' && entry.code ? entry.code : null;
44
+ const whatItDoes = typeof entry.whatItDoes === 'string' && entry.whatItDoes ? entry.whatItDoes : null;
45
+ const method = typeof entry.method === 'string' && entry.method ? entry.method : null;
46
+ return { name, tools, model, category, role, level, code, whatItDoes, method };
47
+ }
48
+
49
+ async function requestDiscoveredInventory({ accessToken } = {}, { endpoint, timeoutMs = DEFAULT_TIMEOUT_MS } = {}) {
50
+ if (!endpoint) return { ok: false, reason: 'no-endpoint' };
51
+
52
+ let res;
53
+ try {
54
+ res = await postJsonWithTimeout(endpoint, null, timeoutMs, 'GET', null, authHeaders({ accessToken }));
55
+ } catch (e) {
56
+ return { ok: false, reason: (e && e.kind) || 'network-error' };
57
+ }
58
+
59
+ if (res.status === 404) return { ok: false, reason: 'no-inventory' };
60
+ if (res.status < 200 || res.status >= 300) {
61
+ return { ok: false, reason: `http-${res.status}` };
62
+ }
63
+
64
+ const json = parseJson(res.raw);
65
+ const data = json && typeof json === 'object' ? json.data : null;
66
+ if (!data || typeof data !== 'object') return { ok: false, reason: 'bad-response' };
67
+
68
+ const skills = (Array.isArray(data.skills) ? data.skills : []).map(normalizeSkill).filter(Boolean);
69
+ const agents = (Array.isArray(data.agents) ? data.agents : []).map(normalizeAgent).filter(Boolean);
70
+ return { ok: true, skills, agents };
71
+ }
72
+
73
+ // Same read, but a `http-401` (expired hub JWT — certs validates it via hub JWKS)
74
+ // triggers one token refresh and a single retry, so an expired token never breaks the
75
+ // inventory read (add-agent / list_addable). `request` is injectable for tests.
76
+ async function requestDiscoveredInventoryWithRefresh(
77
+ { accessToken, refreshToken } = {},
78
+ { endpoint, timeoutMs, request = requestDiscoveredInventory } = {},
79
+ ) {
80
+ let res = await request({ accessToken }, { endpoint, timeoutMs });
81
+ if (res && res.reason === 'http-401' && typeof refreshToken === 'function') {
82
+ let fresh = null;
83
+ try { fresh = await refreshToken(); } catch { fresh = null; }
84
+ const nextToken = fresh && (fresh.accessToken || fresh.hubAccessToken);
85
+ if (nextToken) res = await request({ accessToken: nextToken }, { endpoint, timeoutMs });
86
+ }
87
+ return res;
88
+ }
89
+
90
+ module.exports = { requestDiscoveredInventory, requestDiscoveredInventoryWithRefresh };
@@ -0,0 +1,66 @@
1
+ 'use strict';
2
+
3
+ // Received-invitations client (hub `works/positions/received-invitations`,
4
+ // talent Bearer). Returns the talent's UNREAD position invitations.
5
+
6
+ const { postJsonWithTimeout } = require('./backend-request');
7
+ const { reasonForError } = require('./onboarding-client');
8
+
9
+ const DEFAULT_TIMEOUT_MS = 20000;
10
+
11
+ function parseJson(raw) {
12
+ try {
13
+ return raw ? JSON.parse(raw) : null;
14
+ } catch {
15
+ return null;
16
+ }
17
+ }
18
+
19
+ function bearerHeaders(accessToken) {
20
+ const headers = {};
21
+ if (typeof accessToken === 'string' && accessToken) headers.Authorization = `Bearer ${accessToken}`;
22
+ return headers;
23
+ }
24
+
25
+ function normalizeInvitation(raw) {
26
+ const it = raw && typeof raw === 'object' ? raw : {};
27
+ return {
28
+ projectId: typeof it.projectId === 'string' ? it.projectId : null,
29
+ name: typeof it.name === 'string' ? it.name : null,
30
+ invitationChatId: typeof it.invitationChatId === 'string' ? it.invitationChatId : null,
31
+ };
32
+ }
33
+
34
+ async function requestReceivedInvitations({ 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
+ let res;
38
+ try {
39
+ res = await postJsonWithTimeout(endpoint, null, timeoutMs, 'GET', null, bearerHeaders(hubAccessToken));
40
+ } catch (e) {
41
+ return { ok: false, reason: (e && e.kind) || 'network-error' };
42
+ }
43
+ if (res.status < 200 || res.status >= 300) return { ok: false, reason: reasonForError(res.status, res.raw) };
44
+
45
+ const env = parseJson(res.raw);
46
+ const data = env && env.status === 'OK' ? env.data : env;
47
+ const rows = Array.isArray(data) ? data : (Array.isArray(data && data.items) ? data.items : null);
48
+ if (!rows) return { ok: false, reason: 'bad-response' };
49
+ return { ok: true, items: rows.map(normalizeInvitation) };
50
+ }
51
+
52
+ async function fetchInvitations(deps = {}, { hubAccessToken, timeoutMs = DEFAULT_TIMEOUT_MS } = {}) {
53
+ const {
54
+ getReceivedInvitationsEndpoint = require('./config').getReceivedInvitationsEndpoint,
55
+ requestReceivedInvitations: request = requestReceivedInvitations,
56
+ } = deps;
57
+ const endpoint = getReceivedInvitationsEndpoint();
58
+ if (!endpoint) return { ok: false, reason: 'no-endpoint' };
59
+ return request({ hubAccessToken }, { endpoint, timeoutMs });
60
+ }
61
+
62
+ module.exports = {
63
+ normalizeInvitation,
64
+ requestReceivedInvitations,
65
+ fetchInvitations,
66
+ };
@@ -0,0 +1,50 @@
1
+ 'use strict';
2
+
3
+ // `invitations` flow: list the talent's unread position invitations. Read-only.
4
+
5
+ const { styleLabelPrefix } = require('./ansi');
6
+
7
+ function makeInvitationsDeps(overrides = {}) {
8
+ const client = require('./invitations-client');
9
+ return {
10
+ fetchInvitations: (opts) => client.fetchInvitations({}, opts),
11
+ ...overrides,
12
+ };
13
+ }
14
+
15
+ async function runInvitations({ io, rawOut, session, catalog, opts = {}, deps = makeInvitationsDeps() }) {
16
+ const fp = catalog.invitations;
17
+ const hubAccessToken = session ? session.hubAccessToken : null;
18
+
19
+ const doFetch = () => deps.fetchInvitations({ hubAccessToken });
20
+ const res = opts.json ? await doFetch() : await io.withProgress(fp.loading, doFetch);
21
+ if (!res.ok) {
22
+ io.error(fp.fetchFailed(res.reason));
23
+ return { ok: false, reason: res.reason };
24
+ }
25
+ const items = Array.isArray(res.items) ? res.items : [];
26
+
27
+ if (opts.json) {
28
+ (rawOut || ((s) => process.stdout.write(s)))(`${JSON.stringify({ ok: true, items }, null, 2)}\n`);
29
+ return { ok: true, items };
30
+ }
31
+
32
+ io.section(fp.title);
33
+ if (items.length === 0) {
34
+ io.notify(fp.none);
35
+ return { ok: true, items };
36
+ }
37
+ // Human render never prints the project id (it stays in --json only).
38
+ items.forEach((inv, i) => {
39
+ if (i > 0) io.notify('');
40
+ io.notify(`${i + 1}) ${inv.name || fp.untitled}${inv.invitationChatId ? ` · ${fp.hasChat}` : ''}`);
41
+ });
42
+ io.notify(styleLabelPrefix(fp.countNote(items.length)));
43
+ io.success(fp.done);
44
+ return { ok: true, items };
45
+ }
46
+
47
+ module.exports = {
48
+ makeInvitationsDeps,
49
+ runInvitations,
50
+ };
@@ -0,0 +1,147 @@
1
+ 'use strict';
2
+
3
+ // Languages client. VIEW = GET `works/talents/me/work-details/languages`; the
4
+ // catalog (id<->name) = GET `static-data/languages` (@Public); SET = PATCH the
5
+ // same work-details endpoint (REPLACE-ALL: send the full list of {id, level}).
6
+
7
+ const { postJsonWithTimeout } = require('./backend-request');
8
+ const { reasonForError } = require('./onboarding-client');
9
+
10
+ const DEFAULT_TIMEOUT_MS = 20000;
11
+ const LEVELS = ['NATIVE', 'ADVANCED', 'INTERMEDIATE', 'INTERMEDIATE_WRITTEN'];
12
+
13
+ function parseJson(raw) {
14
+ try {
15
+ return raw ? JSON.parse(raw) : null;
16
+ } catch {
17
+ return null;
18
+ }
19
+ }
20
+
21
+ function bearerHeaders(accessToken) {
22
+ const headers = {};
23
+ if (typeof accessToken === 'string' && accessToken) headers.Authorization = `Bearer ${accessToken}`;
24
+ return headers;
25
+ }
26
+
27
+ function num(v) {
28
+ return typeof v === 'number' && Number.isFinite(v) ? v : null;
29
+ }
30
+
31
+ function str(v) {
32
+ return typeof v === 'string' && v ? v : null;
33
+ }
34
+
35
+ // The hub returns `language.name` as an i18n KEY (staticDataLanguagesName_N),
36
+ // which the CLI cannot resolve — so we display by ISO code, mapping the common
37
+ // ones to a readable name and falling back to the bare code. NEVER the key.
38
+ const LANGUAGE_NAMES = {
39
+ ES: 'Spanish', EN: 'English', FR: 'French', DE: 'German', IT: 'Italian',
40
+ PT: 'Portuguese', CA: 'Catalan', EU: 'Basque', GL: 'Galician', NL: 'Dutch',
41
+ ZH: 'Chinese', JA: 'Japanese', KO: 'Korean', AR: 'Arabic', RU: 'Russian',
42
+ PL: 'Polish', TR: 'Turkish', SV: 'Swedish', NO: 'Norwegian', DA: 'Danish',
43
+ FI: 'Finnish', EL: 'Greek', CS: 'Czech', RO: 'Romanian', HU: 'Hungarian',
44
+ UK: 'Ukrainian', HE: 'Hebrew', HI: 'Hindi', ID: 'Indonesian', VI: 'Vietnamese',
45
+ };
46
+
47
+ function languageName(code) {
48
+ const c = str(code) ? String(code).toUpperCase() : null;
49
+ if (!c) return null;
50
+ return LANGUAGE_NAMES[c] || c;
51
+ }
52
+
53
+ function rowsOf(raw) {
54
+ const env = parseJson(raw);
55
+ const data = env && env.status === 'OK' ? env.data : env;
56
+ return Array.isArray(data) ? data : (Array.isArray(data && data.items) ? data.items : null);
57
+ }
58
+
59
+ // The talent's languages: [{ id, code, name, level }].
60
+ async function requestLanguages({ hubAccessToken } = {}, { endpoint, timeoutMs = DEFAULT_TIMEOUT_MS } = {}) {
61
+ if (!endpoint) return { ok: false, reason: 'no-endpoint' };
62
+ if (!hubAccessToken) return { ok: false, reason: 'no-hub-token' };
63
+ let res;
64
+ try {
65
+ res = await postJsonWithTimeout(endpoint, null, timeoutMs, 'GET', null, bearerHeaders(hubAccessToken));
66
+ } catch (e) {
67
+ return { ok: false, reason: (e && e.kind) || 'network-error' };
68
+ }
69
+ if (res.status < 200 || res.status >= 300) return { ok: false, reason: reasonForError(res.status, res.raw) };
70
+ const rows = rowsOf(res.raw);
71
+ if (!rows) return { ok: false, reason: 'bad-response' };
72
+ const languages = rows.map((r) => {
73
+ const lang = r && r.language && typeof r.language === 'object' ? r.language : {};
74
+ const code = str(lang.code) || str(r && r.code);
75
+ // `name` is derived from the code (readable or bare code), never the raw
76
+ // i18n-key the hub sends (staticDataLanguagesName_N), which we can't resolve.
77
+ return { id: num(lang.id) ?? num(r && r.id), code, name: languageName(code), level: str(r && r.level) };
78
+ }).filter((l) => l.id != null);
79
+ return { ok: true, languages };
80
+ }
81
+
82
+ // The full language catalog (id/name/code), for picking a language to add.
83
+ async function requestLanguageCatalog(_ = {}, { endpoint, timeoutMs = DEFAULT_TIMEOUT_MS } = {}) {
84
+ if (!endpoint) return { ok: false, reason: 'no-endpoint' };
85
+ let res;
86
+ try {
87
+ res = await postJsonWithTimeout(endpoint, null, timeoutMs, 'GET', null, {});
88
+ } catch (e) {
89
+ return { ok: false, reason: (e && e.kind) || 'network-error' };
90
+ }
91
+ if (res.status < 200 || res.status >= 300) return { ok: false, reason: reasonForError(res.status, res.raw) };
92
+ const rows = rowsOf(res.raw);
93
+ if (!rows) return { ok: false, reason: 'bad-response' };
94
+ // Same rule: display by code (readable or bare), never the i18n-key name.
95
+ const catalog = rows.map((r) => { const code = str(r && r.code); return { id: num(r && r.id), code, name: languageName(code) }; }).filter((l) => l.id != null && l.name);
96
+ return { ok: true, catalog };
97
+ }
98
+
99
+ // REPLACE-ALL: `languages` is the FULL desired set of { id, level }.
100
+ async function requestSetLanguages({ hubAccessToken, languages } = {}, { endpoint, timeoutMs = DEFAULT_TIMEOUT_MS } = {}) {
101
+ if (!endpoint) return { ok: false, reason: 'no-endpoint' };
102
+ if (!hubAccessToken) return { ok: false, reason: 'no-hub-token' };
103
+ if (!Array.isArray(languages)) return { ok: false, reason: 'no-languages' };
104
+ let res;
105
+ try {
106
+ res = await postJsonWithTimeout(endpoint, { languages }, timeoutMs, 'PATCH', null, bearerHeaders(hubAccessToken));
107
+ } catch (e) {
108
+ return { ok: false, reason: (e && e.kind) || 'network-error' };
109
+ }
110
+ if (res.status < 200 || res.status >= 300) return { ok: false, reason: reasonForError(res.status, res.raw) };
111
+ return { ok: true };
112
+ }
113
+
114
+ async function fetchLanguages(deps = {}, { hubAccessToken, timeoutMs = DEFAULT_TIMEOUT_MS } = {}) {
115
+ const getEndpoint = deps.getLanguagesEndpoint || require('./config').getLanguagesEndpoint;
116
+ const request = deps.requestLanguages || requestLanguages;
117
+ const endpoint = getEndpoint();
118
+ if (!endpoint) return { ok: false, reason: 'no-endpoint' };
119
+ return request({ hubAccessToken }, { endpoint, timeoutMs });
120
+ }
121
+
122
+ async function fetchLanguageCatalog(deps = {}, { timeoutMs = DEFAULT_TIMEOUT_MS } = {}) {
123
+ const getEndpoint = deps.getLanguagesCatalogEndpoint || require('./config').getLanguagesCatalogEndpoint;
124
+ const request = deps.requestLanguageCatalog || requestLanguageCatalog;
125
+ const endpoint = getEndpoint();
126
+ if (!endpoint) return { ok: false, reason: 'no-endpoint' };
127
+ return request({}, { endpoint, timeoutMs });
128
+ }
129
+
130
+ async function saveLanguages(deps = {}, { hubAccessToken, languages, timeoutMs = DEFAULT_TIMEOUT_MS } = {}) {
131
+ const getEndpoint = deps.getLanguagesEndpoint || require('./config').getLanguagesEndpoint;
132
+ const request = deps.requestSetLanguages || requestSetLanguages;
133
+ const endpoint = getEndpoint();
134
+ if (!endpoint) return { ok: false, reason: 'no-endpoint' };
135
+ return request({ hubAccessToken, languages }, { endpoint, timeoutMs });
136
+ }
137
+
138
+ module.exports = {
139
+ LEVELS,
140
+ languageName,
141
+ requestLanguages,
142
+ requestLanguageCatalog,
143
+ requestSetLanguages,
144
+ fetchLanguages,
145
+ fetchLanguageCatalog,
146
+ saveLanguages,
147
+ };
@@ -0,0 +1,14 @@
1
+ 'use strict';
2
+
3
+ const LANG_CODES = ['es', 'en', 'it', 'pt', 'fr'];
4
+
5
+ function toLowerLang(language) {
6
+ const lower = String(language || 'en').trim().toLowerCase();
7
+ return LANG_CODES.includes(lower) ? lower : 'en';
8
+ }
9
+
10
+ function toUpperLang(language) {
11
+ return toLowerLang(language).toUpperCase();
12
+ }
13
+
14
+ module.exports = { LANG_CODES, toLowerLang, toUpperLang };