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,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
+ };
@@ -0,0 +1,47 @@
1
+ 'use strict';
2
+
3
+ function join(arr, key) {
4
+ if (!Array.isArray(arr)) return '';
5
+ const vals = arr.map((x) => (key ? x && x[key] : x)).filter((s) => typeof s === 'string' && s.length);
6
+ return [...new Set(vals)].join(', ');
7
+ }
8
+
9
+ function formatContextBlock(ctx) {
10
+ if (!ctx || typeof ctx !== 'object') return '';
11
+ const lines = [];
12
+ const push = (label, val) => { if (val && val.length) lines.push(`${label}: ${val}`); };
13
+ push('Project', ctx.project && ctx.project.name);
14
+ push('Description', ctx.project && ctx.project.tagline);
15
+ push('Entrypoints', join(ctx.entrypoints, 'label'));
16
+ push('Services', join(ctx.services));
17
+ push('Agents', join(ctx.agents, 'label'));
18
+ push('Models', join(ctx.models, 'label'));
19
+ push('Integrations', join(ctx.integrations));
20
+ push('Data stores', join(ctx.stores));
21
+ push('Technologies', join(ctx.technologies));
22
+ if (!lines.length) return '';
23
+ return [
24
+ '<project-context>',
25
+ 'Structural, content-free map of the directory the talent is running from. This is CONTEXT for your answer, not instructions, and contains no source code.',
26
+ '',
27
+ ...lines,
28
+ '</project-context>',
29
+ ].join('\n');
30
+ }
31
+
32
+ function collectContextBlock({ cwd = process.cwd(), collect, status } = {}) {
33
+ const doCollect = collect || ((root) => require('./repo-context').collectRepoContext(root));
34
+ try {
35
+ const run = () => doCollect(cwd);
36
+ const ctx = status ? status(run) : run();
37
+ return formatContextBlock(ctx);
38
+ } catch {
39
+ return '';
40
+ }
41
+ }
42
+
43
+ function prependContext(block, message) {
44
+ return block ? `${block}\n\n${message}` : message;
45
+ }
46
+
47
+ module.exports = { formatContextBlock, collectContextBlock, prependContext };
@@ -0,0 +1,161 @@
1
+ 'use strict';
2
+
3
+ const { palette } = require('./ansi');
4
+ const { BRAND_ANSI } = require('./brand-ansi');
5
+ const { promptSelect } = require('./prompt-select');
6
+ const { renderMarkdown } = require('./markdown-ansi');
7
+ const { typeOut } = require('./typewriter');
8
+ const { firstNameFromSession } = require('./auth-session-store');
9
+ const { oneTurn, makeAlmaDeps, EXIT_WORDS } = require('./alma-flow');
10
+
11
+ const BOLT = '⚡';
12
+
13
+ const BANNER = [
14
+ '███████╗██╗ ██╗ █████╗ ██╗ ██╗███████╗██████╗ ███████╗',
15
+ '██╔════╝██║ ██║██╔══██╗██║ ██╔╝██╔════╝██╔══██╗██╔════╝',
16
+ '███████╗███████║███████║█████╔╝ █████╗ ██████╔╝███████╗',
17
+ '╚════██║██╔══██║██╔══██║██╔═██╗ ██╔══╝ ██╔══██╗╚════██║',
18
+ '███████║██║ ██║██║ ██║██║ ██╗███████╗██║ ██║███████║',
19
+ '╚══════╝╚═╝ ╚═╝╚═╝ ╚═╝╚═╝ ╚═╝╚══════╝╚═╝ ╚═╝╚══════╝',
20
+ ];
21
+
22
+ function brandColors() {
23
+ return palette({
24
+ primary: BRAND_ANSI.primary,
25
+ accent: BRAND_ANSI.accent,
26
+ muted: BRAND_ANSI.muted,
27
+ bold: '\x1b[1m',
28
+ dim: '\x1b[2m',
29
+ reset: '\x1b[0m',
30
+ });
31
+ }
32
+
33
+ function brandHeader(catalog, out) {
34
+ const C = brandColors();
35
+ const a = catalog.alma;
36
+ out('\n');
37
+ BANNER.forEach((line, idx) => {
38
+ if (idx === 0) out(` ${C.accent}${C.bold}${BOLT}${C.reset} ${C.primary}${C.bold}${line}${C.reset}\n`);
39
+ else out(` ${C.primary}${C.bold}${line}${C.reset}\n`);
40
+ });
41
+ out(` ${C.accent}${C.bold}${a.title}${C.reset} ${C.dim}· ${a.tagline}${C.reset}\n`);
42
+ out(` ${C.dim}${a.replHint}${C.reset}\n`);
43
+ }
44
+
45
+ function userPrompt(catalog, session) {
46
+ const C = palette({ primary: BRAND_ANSI.primary, reset: '\x1b[0m' });
47
+ const first = firstNameFromSession(session);
48
+ const label = first ? `${first}:` : catalog.alma.prompt;
49
+ return `${C.primary}${BOLT}${C.reset} ${label}`;
50
+ }
51
+
52
+ function displayWidth(s) {
53
+ let w = 0;
54
+ for (const ch of s) w += ch === BOLT ? 2 : 1;
55
+ return w;
56
+ }
57
+
58
+ async function printAlmaReply(res, catalog, write, { streamIsTTY = false, env = null } = {}) {
59
+ if (!res || !res.ok) return;
60
+ const body = String(res.text || '').trim();
61
+ if (!body) return;
62
+ const C = brandColors();
63
+ const label = `${C.accent}${C.bold}${BOLT} ${catalog.alma.title}:${C.reset}`;
64
+ const pad = ' '.repeat(displayWidth(` ${BOLT} ${catalog.alma.title}: `));
65
+ const lines = renderMarkdown(body).split('\n');
66
+ let block = `\n ${label} ${lines[0]}\n`;
67
+ for (let i = 1; i < lines.length; i++) block += lines[i] ? `${pad}${lines[i]}\n` : '\n';
68
+ const animate = !!streamIsTTY && !(env && env.NO_ANIMATION);
69
+ const outStream = { write: (s) => { write(s); return true; }, isTTY: animate };
70
+ await typeOut(block, { stream: outStream });
71
+ }
72
+
73
+ async function almaTurn({ io, ask, stdinIsTTY, streamIsTTY, env, write, hubAccessToken, catalog, deps, message }) {
74
+ const a = catalog.alma;
75
+ const C = brandColors();
76
+ const dimNote = (text) => write(` ${C.dim}${text}${C.reset}\n`);
77
+
78
+ let pending = null;
79
+ const links = [];
80
+ const capture = (evt) => {
81
+ if (!evt) return;
82
+ if (evt.type === 'confirmation_required') pending = { text: evt.text || '', detail: evt.detail || '', data: evt.data || {} };
83
+ else if (evt.type === 'link' && evt.data && evt.data.to) links.push(String(evt.data.to));
84
+ };
85
+
86
+ const res = await oneTurn({ io, hubAccessToken, message, catalog: { ask: a }, deps, onStreamEvent: capture });
87
+ await printAlmaReply(res, catalog, write, { streamIsTTY, env });
88
+ for (const to of links) dimNote(a.webLink(to));
89
+ if (!res.ok || !pending) return res;
90
+
91
+ if (pending.text) write(`\n ${renderMarkdown(pending.text)}\n`);
92
+ if (pending.detail) io.warn(a.irreversible);
93
+ if (pending.data && pending.data.to) dimNote(a.webLink(pending.data.to));
94
+
95
+ const choice = await promptSelect({
96
+ ask,
97
+ stdinIsTTY,
98
+ out: (line) => write(` ${line}\n`),
99
+ header: a.confirmPrompt,
100
+ items: [{ id: 'yes', label: a.confirmYes }, { id: 'no', label: a.confirmNo }],
101
+ labelFor: (it) => it.label,
102
+ });
103
+ const approved = !!(choice && choice.id === 'yes');
104
+
105
+ const dres = await oneTurn({
106
+ io,
107
+ hubAccessToken,
108
+ message: null,
109
+ catalog: { ask: a },
110
+ deps,
111
+ sendTurn: ({ onEvent }) => deps.decideAlma({ hubAccessToken, approved, onEvent }),
112
+ });
113
+ await printAlmaReply(dres, catalog, write, { streamIsTTY, env });
114
+ return res;
115
+ }
116
+
117
+ async function runAlmaShell({ io, ask, stdinIsTTY, streamIsTTY, env = process.env, rawOut, session, catalog, deps = makeAlmaDeps() }) {
118
+ const a = catalog.alma;
119
+ const write = rawOut || ((s) => process.stdout.write(s));
120
+ const hubAccessToken = session ? session.hubAccessToken : null;
121
+
122
+ brandHeader(catalog, write);
123
+
124
+ // Collect the (code-free) project map ONCE, then ask consent before ever sending
125
+ // it — privacy gate. Declining (or no TTY to ask) sends every turn without context.
126
+ const ctxBlock = deps.collectContextBlock
127
+ ? deps.collectContextBlock({ status: (fn) => (deps.withStatus ? deps.withStatus(a.contextLabel, fn) : fn()) })
128
+ : '';
129
+ const readLine = typeof ask === 'function' ? ask : (p) => io.ask(p);
130
+
131
+ let ctxConsent = false;
132
+ if (ctxBlock && stdinIsTTY) {
133
+ const choice = await promptSelect({
134
+ ask: readLine,
135
+ stdinIsTTY,
136
+ out: (line) => write(` ${line}\n`),
137
+ header: a.contextConsentHeader,
138
+ hint: a.contextConsentHint,
139
+ items: [{ id: 'yes', label: a.contextConsentYes }, { id: 'no', label: a.contextConsentNo }],
140
+ labelFor: (it) => it.label,
141
+ });
142
+ ctxConsent = !!(choice && choice.id === 'yes');
143
+ if (!ctxConsent) io.notify(a.contextConsentDeclined);
144
+ }
145
+ const withCtx = (msg) => (ctxConsent && ctxBlock ? `${ctxBlock}\n\n${msg}` : msg);
146
+ const prompt = userPrompt(catalog, session);
147
+ let first = true;
148
+ for (;;) {
149
+ const line = (await readLine(prompt)).trim();
150
+ if (!line || EXIT_WORDS.has(line.toLowerCase())) {
151
+ io.notify(a.bye);
152
+ break;
153
+ }
154
+ const message = first ? withCtx(line) : line;
155
+ first = false;
156
+ await almaTurn({ io, ask, stdinIsTTY, streamIsTTY, env, write, hubAccessToken, catalog, deps, message });
157
+ }
158
+ return { ok: true };
159
+ }
160
+
161
+ module.exports = { runAlmaShell, almaTurn, printAlmaReply, brandHeader, userPrompt };
package/src/ansi.js ADDED
@@ -0,0 +1,62 @@
1
+ 'use strict';
2
+
3
+ const { isInteractive } = require('./terminal-progress');
4
+ const { BRAND_ANSI } = require('./brand-ansi');
5
+
6
+ // THE ONE GATE for terminal colour (talents-ai-score).
7
+
8
+ // `NO_COLOR` per the de-facto standard: present and non-empty disables colour, whatever the value.
9
+ function noColorRequested(env = process.env) {
10
+ const value = env ? env.NO_COLOR : undefined;
11
+ return typeof value === 'string' && value.length > 0;
12
+ }
13
+
14
+ // The gate. `stream` defaults to stdout because every palette in this repo
15
+ // decorates text that ends up there.
16
+ function colorEnabled({ stream = process.stdout, env = process.env } = {}) {
17
+ if (noColorRequested(env)) return false;
18
+ return isInteractive(stream);
19
+ }
20
+
21
+ // Wraps a map of escape codes so each key yields its code when colour is on and `''` when it is off.
22
+ function palette(codes, opts = {}) {
23
+ const out = {};
24
+ for (const key of Object.keys(codes)) {
25
+ const code = codes[key];
26
+ Object.defineProperty(out, key, {
27
+ enumerable: true,
28
+ get: () => (colorEnabled(opts) ? code : ''),
29
+ });
30
+ }
31
+ return out;
32
+ }
33
+
34
+ function styleQuestion(text, opts = {}) {
35
+ const ANSI = palette({ bold: '\x1b[1m', primary: BRAND_ANSI.primary, reset: '\x1b[0m' }, opts);
36
+ return `${ANSI.bold}${ANSI.primary}${text}${ANSI.reset}`;
37
+ }
38
+
39
+ const SUCCESS_GREEN = '\x1b[38;5;114m';
40
+
41
+ function styleSuccess(text, opts = {}) {
42
+ const ANSI = palette({ success: SUCCESS_GREEN, reset: '\x1b[0m' }, opts);
43
+ return `${ANSI.success}${text}${ANSI.reset}`;
44
+ }
45
+
46
+ // Bold a field LABEL (no colour). Gate-aware via `palette`: empty codes when
47
+ // colour is off (non-TTY / NO_COLOR), so piped/redirected output stays clean.
48
+ function styleLabel(text, opts = {}) {
49
+ const ANSI = palette({ bold: '\x1b[1m', reset: '\x1b[0m' }, opts);
50
+ return `${ANSI.bold}${text}${ANSI.reset}`;
51
+ }
52
+
53
+ // Bold only the "Label:" prefix of a "Label: value" line, leaving the value
54
+ // plain. No `: ` -> returned unchanged (value-only lines never get bolded).
55
+ function styleLabelPrefix(line, opts = {}) {
56
+ const s = typeof line === 'string' ? line : '';
57
+ const idx = s.indexOf(': ');
58
+ if (idx === -1) return s;
59
+ return `${styleLabel(s.slice(0, idx + 1), opts)}${s.slice(idx + 1)}`;
60
+ }
61
+
62
+ module.exports = { palette, colorEnabled, noColorRequested, styleQuestion, styleSuccess, styleLabel, styleLabelPrefix, SUCCESS_GREEN };
@@ -0,0 +1,79 @@
1
+ 'use strict';
2
+
3
+ // My-applications client. Lists the
4
+ // positions the talent applied to, with the candidature status.
5
+
6
+ const { postJsonWithTimeout } = require('./backend-request');
7
+ const { reasonForError } = require('./onboarding-client');
8
+
9
+ const DEFAULT_TIMEOUT_MS = 20000;
10
+ const DEFAULT_LIMIT = 30;
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 str(v) {
27
+ return typeof v === 'string' && v ? v : null;
28
+ }
29
+
30
+ function normalizeApplication(raw) {
31
+ const it = raw && typeof raw === 'object' ? raw : {};
32
+ const project = it.project && typeof it.project === 'object' ? it.project : {};
33
+ const position = it.position && typeof it.position === 'object' ? it.position : {};
34
+ const org = it.organization && typeof it.organization === 'object' ? it.organization : {};
35
+ return {
36
+ positionId: str(position.id),
37
+ title: str(position.name) || str(project.name),
38
+ projectName: str(project.name),
39
+ company: str(org.name),
40
+ status: str(it.status),
41
+ createdAt: str(it.createdAt),
42
+ };
43
+ }
44
+
45
+ async function requestMyCandidatures({ hubAccessToken } = {}, { endpoint, limit = DEFAULT_LIMIT, timeoutMs = DEFAULT_TIMEOUT_MS } = {}) {
46
+ if (!endpoint) return { ok: false, reason: 'no-endpoint' };
47
+ if (!hubAccessToken) return { ok: false, reason: 'no-hub-token' };
48
+ const criteria = { limit, offset: 0 };
49
+ const url = `${endpoint}${endpoint.includes('?') ? '&' : '?'}criteria=${encodeURIComponent(JSON.stringify(criteria))}`;
50
+ let res;
51
+ try {
52
+ res = await postJsonWithTimeout(url, null, timeoutMs, 'GET', null, bearerHeaders(hubAccessToken));
53
+ } catch (e) {
54
+ return { ok: false, reason: (e && e.kind) || 'network-error' };
55
+ }
56
+ if (res.status < 200 || res.status >= 300) return { ok: false, reason: reasonForError(res.status, res.raw) };
57
+
58
+ const env = parseJson(res.raw);
59
+ const data = env && env.status === 'OK' ? env.data : env;
60
+ const rows = Array.isArray(data) ? data : (Array.isArray(data && data.items) ? data.items : null);
61
+ if (!rows) return { ok: false, reason: 'bad-response' };
62
+ return { ok: true, items: rows.map(normalizeApplication), meta: (env && typeof env.meta === 'object' ? env.meta : null) };
63
+ }
64
+
65
+ async function fetchApplications(deps = {}, { hubAccessToken, limit = DEFAULT_LIMIT, timeoutMs = DEFAULT_TIMEOUT_MS } = {}) {
66
+ const {
67
+ getMyCandidaturesEndpoint = require('./config').getMyCandidaturesEndpoint,
68
+ requestMyCandidatures: request = requestMyCandidatures,
69
+ } = deps;
70
+ const endpoint = getMyCandidaturesEndpoint();
71
+ if (!endpoint) return { ok: false, reason: 'no-endpoint' };
72
+ return request({ hubAccessToken }, { endpoint, limit, timeoutMs });
73
+ }
74
+
75
+ module.exports = {
76
+ normalizeApplication,
77
+ requestMyCandidatures,
78
+ fetchApplications,
79
+ };
@@ -0,0 +1,58 @@
1
+ 'use strict';
2
+
3
+ // `applications` flow: list the positions the talent applied to + status.
4
+ // Read-only. Writes the SAME ordinal->positionId cache as find-projects, so
5
+ // `show-project N` / `save-project N` work after `applications` too.
6
+
7
+ const { styleLabelPrefix } = require('./ansi');
8
+
9
+ function makeApplicationsDeps(overrides = {}) {
10
+ const client = require('./applications-client');
11
+ return {
12
+ fetchApplications: (opts) => client.fetchApplications({}, opts),
13
+ writePositionCache: (ids) => require('./find-projects-cache').writePositionCache(ids),
14
+ ...overrides,
15
+ };
16
+ }
17
+
18
+ async function runApplications({ io, rawOut, session, catalog, opts = {}, deps = makeApplicationsDeps() }) {
19
+ const fp = catalog.applications;
20
+ const hubAccessToken = session ? session.hubAccessToken : null;
21
+
22
+ const doFetch = () => deps.fetchApplications({ hubAccessToken });
23
+ const res = opts.json ? await doFetch() : await io.withProgress(fp.loading, doFetch);
24
+ if (!res.ok) {
25
+ io.error(fp.fetchFailed(res.reason));
26
+ return { ok: false, reason: res.reason };
27
+ }
28
+ const items = Array.isArray(res.items) ? res.items : [];
29
+
30
+ if (opts.json) {
31
+ (rawOut || ((s) => process.stdout.write(s)))(`${JSON.stringify({ ok: true, items }, null, 2)}\n`);
32
+ return { ok: true, items };
33
+ }
34
+
35
+ // Human path: refresh the number->positionId cache (last listing wins).
36
+ try { deps.writePositionCache(items.map((it) => it.positionId)); } catch { /* best-effort */ }
37
+
38
+ io.section(fp.title);
39
+ if (items.length === 0) {
40
+ io.notify(fp.none);
41
+ return { ok: true, items };
42
+ }
43
+ // No ids in the human render (they ride the cache + --json).
44
+ items.forEach((a, i) => {
45
+ if (i > 0) io.notify('');
46
+ const headline = a.title || a.projectName || fp.untitled;
47
+ io.notify(`${i + 1}) ${headline}${a.company ? ` — ${a.company}` : ''}`);
48
+ io.notify(styleLabelPrefix(fp.statusLine(a.status || fp.statusUnknown)));
49
+ });
50
+ io.notify(styleLabelPrefix(fp.countNote(items.length)));
51
+ io.success(fp.done);
52
+ return { ok: true, items };
53
+ }
54
+
55
+ module.exports = {
56
+ makeApplicationsDeps,
57
+ runApplications,
58
+ };