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,351 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ // `superadmin` — NON-PROD, password-authenticated superadmin SESSION (ADR-027, supersedes the ADR-021/022/023 provision/teardown/authorized-authoring flow).
5
+
6
+ const http = require('http');
7
+ const https = require('https');
8
+ const { detectReportLang, getCatalog } = require('../src/i18n');
9
+ const {
10
+ getSuperadminSessionEndpoint,
11
+ getInspectCertificationsEndpoint,
12
+ saveSuperadminSession,
13
+ clearSuperadminSession,
14
+ loadSuperadminSession,
15
+ getProfile,
16
+ } = require('../src/config');
17
+ const { isValidEmail, normalizeEmail } = require('../src/share');
18
+ const { createStdinAsk } = require('../src/stdin-ask');
19
+ const { runInteractiveMultiSelect, wrapDesc } = require('../src/interactive-select');
20
+ const { palette } = require('../src/ansi');
21
+ const { switchProfile } = require('../src/profile-switch');
22
+
23
+ const REQUEST_TIMEOUT_MS = 20000;
24
+
25
+ // Only `dim`/`reset` — the numbered fallback's per-option descriptions are quiet chrome, same treatment every other picker in this CLI gives them.
26
+ const ANSI = palette({ reset: '\x1b[0m', dim: '\x1b[2m' });
27
+
28
+ // Minimal flag parse: --email, --lang, --inspect, --logout (all optional; the interactive prompt / a stdin pipe fills the gaps).
29
+ function parseArgs(argv) {
30
+ const opts = {
31
+ email: null,
32
+ lang: null,
33
+ inspect: false,
34
+ logout: false,
35
+ };
36
+ for (let i = 0; i < argv.length; i++) {
37
+ const a = argv[i];
38
+ if (a === '--email') opts.email = argv[++i];
39
+ else if (a.startsWith('--email=')) opts.email = a.slice('--email='.length);
40
+ else if (a === '--inspect' || a === '--audit') opts.inspect = true;
41
+ else if (a === '--logout' || a === '--forget') opts.logout = true;
42
+ else if (a === '--lang' && (argv[i + 1] === 'es' || argv[i + 1] === 'en')) opts.lang = argv[++i];
43
+ else if (a === '--lang=es') opts.lang = 'es';
44
+ else if (a === '--lang=en') opts.lang = 'en';
45
+ }
46
+ return opts;
47
+ }
48
+
49
+ function postJson(url, body) {
50
+ return new Promise((resolve, reject) => {
51
+ let u;
52
+ try {
53
+ u = new URL(url);
54
+ } catch (e) {
55
+ return reject(e);
56
+ }
57
+ const lib = u.protocol === 'https:' ? https : http;
58
+ const data = Buffer.from(JSON.stringify(body));
59
+ const req = lib.request(
60
+ u,
61
+ {
62
+ method: 'POST',
63
+ headers: {
64
+ Accept: 'application/json',
65
+ 'Content-Type': 'application/json',
66
+ 'Content-Length': data.length,
67
+ },
68
+ timeout: REQUEST_TIMEOUT_MS,
69
+ },
70
+ (res) => {
71
+ let raw = '';
72
+ res.on('data', (c) => (raw += c));
73
+ res.on('end', () => {
74
+ let json = null;
75
+ try {
76
+ json = raw ? JSON.parse(raw) : null;
77
+ } catch {
78
+ json = null;
79
+ }
80
+ resolve({ status: res.statusCode, json });
81
+ });
82
+ },
83
+ );
84
+ req.on('timeout', () => req.destroy(Object.assign(new Error('timeout'), { kind: 'timeout' })));
85
+ req.on('error', reject);
86
+ req.write(data);
87
+ req.end();
88
+ });
89
+ }
90
+
91
+ // Reads a line via the shared reader when available.
92
+ async function promptLine(ask, canPrompt, label) {
93
+ if (!canPrompt) return '';
94
+ return String(await ask(label)).trim();
95
+ }
96
+
97
+ // The SECRET prompt (issue 102).
98
+ async function promptSecret(ask, canPrompt, label) {
99
+ if (!canPrompt) return '';
100
+ const read = typeof ask.secret === 'function' ? ask.secret.bind(ask) : ask;
101
+ return String(await read(label)).trim();
102
+ }
103
+
104
+ function profileMenuItems(c) {
105
+ const current = getProfile();
106
+ return [
107
+ {
108
+ key: 'talent',
109
+ label: current === 'talent' ? c.profileMenuTalentCurrent : c.profileMenuTalent,
110
+ desc: c.profileMenuTalentDesc,
111
+ },
112
+ {
113
+ key: 'external',
114
+ label: current === 'external' ? c.profileMenuExternalCurrent : c.profileMenuExternal,
115
+ desc: c.profileMenuExternalDesc,
116
+ },
117
+ ];
118
+ }
119
+
120
+ async function askNumberedChoice(ask, promptText, count) {
121
+ const raw = (await ask(promptText)).trim();
122
+ if (!raw) return null;
123
+ const n = Number.parseInt(raw, 10);
124
+ if (!Number.isInteger(n) || n < 1 || n > count) return null;
125
+ return n - 1;
126
+ }
127
+
128
+ // The picker itself.
129
+ async function chooseProfileAction({ ask, c, input, output, stdinIsTTY }) {
130
+ const items = profileMenuItems(c);
131
+ if (stdinIsTTY && typeof ask.suspend === 'function') {
132
+ ask.suspend();
133
+ const picked = await runInteractiveMultiSelect({
134
+ items,
135
+ labelFor: (i) => i.label,
136
+ header: c.profileMenuHeading,
137
+ hint: c.profileMenuHint,
138
+ single: true,
139
+ descriptionFor: (i) => i.desc || null,
140
+ ...(input ? { input } : {}),
141
+ ...(output ? { output } : {}),
142
+ });
143
+ ask.resume();
144
+ return picked && picked.length ? picked[0].key : null;
145
+ }
146
+
147
+ process.stdout.write(`\n ${c.profileMenuHeading}\n`);
148
+ items.forEach((item, i) => {
149
+ process.stdout.write(` ${i + 1}) ${item.label}\n`);
150
+ if (item.desc) for (const line of wrapDesc(item.desc)) process.stdout.write(` ${ANSI.dim}${line}${ANSI.reset}\n`);
151
+ });
152
+ const idx = await askNumberedChoice(ask, ` ${c.profileMenuPrompt(items.length)}`, items.length);
153
+ return idx === null ? null : items[idx].key;
154
+ }
155
+
156
+ // Runs the picker and, on a real choice, the switch itself.
157
+ async function runProfileMenu({ ask, c, input, output, stdinIsTTY }) {
158
+ const picked = await chooseProfileAction({ ask, c, input, output, stdinIsTTY });
159
+ if (!picked) {
160
+ process.stdout.write(`\n ${c.profileMenuCancelled}\n\n`);
161
+ return;
162
+ }
163
+ const current = getProfile();
164
+ if (picked === current) {
165
+ process.stdout.write(`\n ${c.profileAlreadyOn(picked)}\n\n`);
166
+ return;
167
+ }
168
+ const result = switchProfile(picked);
169
+ process.stdout.write(`\n ${c.profileSwitched(result.to)}\n\n`);
170
+ }
171
+
172
+ async function run(argv = process.argv.slice(2), { ask: injectedAsk = null, input = undefined, output = undefined } = {}) {
173
+ const opts = parseArgs(argv);
174
+ const lang = opts.lang || detectReportLang();
175
+ const c = getCatalog(lang).superadmin;
176
+
177
+ // --logout is local-only (no endpoint, no password): forget the stored token.
178
+ if (opts.logout) {
179
+ clearSuperadminSession();
180
+ process.stdout.write(`\n ${c.loggedOut}\n\n`);
181
+ return;
182
+ }
183
+
184
+ if (opts.inspect) {
185
+ const endpoint = getInspectCertificationsEndpoint();
186
+ if (!endpoint) {
187
+ process.stderr.write(`\n ${c.errorNoEndpoint}\n\n`);
188
+ process.exitCode = 1;
189
+ return;
190
+ }
191
+ process.stdout.write(`\n ${c.inspectIntro}\n`);
192
+ process.stdout.write(` ${c.queryingPrimary(endpoint)}\n`);
193
+ const canPrompt = true;
194
+ const ask = injectedAsk || createStdinAsk();
195
+ try {
196
+ const password = await promptSecret(ask, canPrompt, c.passwordPrompt);
197
+ await runInspect({ opts, c, endpoint, password, ask, canPrompt });
198
+ } finally {
199
+ if (!injectedAsk) ask.close();
200
+ }
201
+ return;
202
+ }
203
+
204
+ // Default ("session") mode.
205
+ const existing = loadSuperadminSession();
206
+ const ask = injectedAsk || createStdinAsk();
207
+ try {
208
+ let sessionReady = !!existing;
209
+ if (!existing) {
210
+ const endpoint = getSuperadminSessionEndpoint();
211
+ if (!endpoint) {
212
+ process.stderr.write(`\n ${c.errorNoEndpoint}\n\n`);
213
+ process.exitCode = 1;
214
+ return;
215
+ }
216
+ process.stdout.write(`\n ${c.sessionIntro}\n`);
217
+ // talents-ai-score, ADR-020/021 (coordinator ruling): superadmin is DELIBERATELY single-hop, PRIMARY (certifications service) ONLY — never hub.
218
+ process.stdout.write(` ${c.queryingPrimary(endpoint)}\n`);
219
+ // Always true (three-way review): the password has NO argv flag anymore, so it must always be collectible — either from a real TTY, the REPL's injected reader, or a piped stdin.
220
+ const password = await promptSecret(ask, true, c.passwordPrompt);
221
+ sessionReady = await runOpenSession({ opts, c, endpoint, password, ask, canPrompt: true });
222
+ }
223
+ if (sessionReady) {
224
+ const stdinIsTTY =
225
+ input !== undefined ? !!input.isTTY : !!process.stdin.isTTY;
226
+ await runProfileMenu({ ask, c, input, output, stdinIsTTY });
227
+ }
228
+ } finally {
229
+ if (!injectedAsk) ask.close();
230
+ }
231
+ }
232
+
233
+ // ADR-025 read-only attribution receipt for stored certifications.
234
+ async function runInspect({ opts, c, endpoint, password, ask, canPrompt }) {
235
+ const email = await resolveEmailArg(opts, c, ask, canPrompt, c.inspectEmailPrompt);
236
+ if (!password || !email || !isValidEmail(email)) {
237
+ process.stderr.write(`\n ${c.needInput}\n\n`);
238
+ process.exitCode = 1;
239
+ return;
240
+ }
241
+
242
+ let res;
243
+ try {
244
+ res = await postJson(endpoint, { password, email: normalizeEmail(email) });
245
+ } catch {
246
+ process.stderr.write(`\n ${c.inspectErrorGeneric}\n\n`);
247
+ process.exitCode = 1;
248
+ return;
249
+ }
250
+
251
+ if (res.status >= 200 && res.status < 300) {
252
+ const certs = (res.json && Array.isArray(res.json.certifications) && res.json.certifications) || [];
253
+ printInspectReceipts(certs, normalizeEmail(email), c);
254
+ return;
255
+ }
256
+ if (res.status === 403) process.stderr.write(`\n ${c.errorWrongPassword}\n\n`);
257
+ else if (res.status === 404) process.stderr.write(`\n ${c.errorDisabled}\n\n`);
258
+ else process.stderr.write(`\n ${c.inspectErrorGeneric}\n\n`);
259
+ process.exitCode = 1;
260
+ }
261
+
262
+ // Prints the stored authorship + rubric evidence per certification. Attribution
263
+ // trail (git authorship), NOT cryptographic proof — stated in the note.
264
+ function printInspectReceipts(certs, email, c) {
265
+ if (certs.length === 0) {
266
+ process.stdout.write(`\n ${c.inspectNone(email)}\n\n`);
267
+ return;
268
+ }
269
+ const L = c.inspectLabels;
270
+ const out = [`\n ${c.inspectHeader(certs.length, email)}\n`];
271
+ for (const cert of certs) {
272
+ const dims =
273
+ cert.dimensionScores && typeof cert.dimensionScores === 'object'
274
+ ? Object.entries(cert.dimensionScores)
275
+ .map(([k, v]) => `${k} ${v == null ? 'N/A' : `${v}/4`}`)
276
+ .join(', ')
277
+ : '—';
278
+ const confirmed = Array.isArray(cert.authorEmails)
279
+ ? cert.authorEmails.filter((a) => a && a.matched).map((a) => a.email)
280
+ : [];
281
+ const considered = Array.isArray(cert.authorEmails) ? cert.authorEmails.map((a) => a.email) : [];
282
+ const files = Array.isArray(cert.sampledFiles) ? cert.sampledFiles : [];
283
+ out.push(` ── ${cert.skillName}${cert.technology ? ` (${cert.technology})` : ''}`);
284
+ out.push(` ${L.score}: ${cert.score == null ? 'n/a' : `${cert.score}/100`}`);
285
+ out.push(` ${L.dimensions}: ${dims}`);
286
+ if (cert.repository) out.push(` ${L.repo}: ${cert.repository}`);
287
+ if (cert.commitRange) out.push(` ${L.commitRange}: ${cert.commitRange}`);
288
+ if (files.length) out.push(` ${L.sampledFiles}: ${files.join(', ')}`);
289
+ if (confirmed.length) out.push(` ${L.authorsConfirmed}: ${confirmed.join(', ')}`);
290
+ else if (considered.length) out.push(` ${L.authorsConsidered}: ${considered.join(', ')}`);
291
+ out.push(` ${L.model}: ${cert.model || '—'}${cert.promptVersion ? ` · ${cert.promptVersion}` : ''}`);
292
+ out.push(` ${L.when}: ${cert.createdAt}`);
293
+ if (cert.testOrigin) out.push(` ${L.testOrigin}: ✓`);
294
+ out.push('');
295
+ }
296
+ out.push(` ${c.inspectNote}\n`);
297
+ process.stdout.write(out.join('\n'));
298
+ }
299
+
300
+ async function resolveEmailArg(opts, c, ask, canPrompt, promptLabel) {
301
+ if (opts.email) return opts.email;
302
+ if (!canPrompt) return null;
303
+ for (let attempt = 0; attempt < 5; attempt++) {
304
+ const raw = String(await ask(promptLabel)).trim();
305
+ if (isValidEmail(raw)) return raw;
306
+ process.stdout.write(` ${c.emailInvalid}\n`);
307
+ }
308
+ return null;
309
+ }
310
+
311
+ // ADR-027: open a superadmin SESSION — validate the password server-side, get a token, persist it locally.
312
+ async function runOpenSession({ opts, c, endpoint, password, ask, canPrompt }) {
313
+ const email = await resolveEmailArg(opts, c, ask, canPrompt, c.emailPrompt);
314
+ if (!password || !email || !isValidEmail(email)) {
315
+ process.stderr.write(`\n ${c.needInput}\n\n`);
316
+ process.exitCode = 1;
317
+ return false;
318
+ }
319
+
320
+ let res;
321
+ try {
322
+ res = await postJson(endpoint, { password, email: normalizeEmail(email) });
323
+ } catch {
324
+ process.stderr.write(`\n ${c.errorGeneric}\n\n`);
325
+ process.exitCode = 1;
326
+ return false;
327
+ }
328
+
329
+ if (res.status >= 200 && res.status < 300 && res.json && res.json.token) {
330
+ saveSuperadminSession({
331
+ email: res.json.email || normalizeEmail(email),
332
+ token: res.json.token,
333
+ expiresAt: res.json.expiresAt || null,
334
+ });
335
+ process.stdout.write(`\n ${c.sessionReady(res.json.email || normalizeEmail(email))}\n`);
336
+ if (res.json.expiresAt) process.stdout.write(` ${c.sessionExpires(res.json.expiresAt)}\n`);
337
+ process.stdout.write(` ${c.sessionHint}\n\n`);
338
+ return true;
339
+ }
340
+ if (res.status === 403) process.stderr.write(`\n ${c.errorWrongPassword}\n\n`);
341
+ else if (res.status === 404) process.stderr.write(`\n ${c.errorDisabled}\n\n`);
342
+ else process.stderr.write(`\n ${c.errorGeneric}\n\n`);
343
+ process.exitCode = 1;
344
+ return false;
345
+ }
346
+
347
+ module.exports = { run };
348
+
349
+ if (require.main === module) {
350
+ run();
351
+ }
package/package.json ADDED
@@ -0,0 +1,40 @@
1
+ {
2
+ "name": "shakers-cli-beta",
3
+ "version": "0.8.0-beta.0",
4
+ "description": "Shakers AI usage & skills evaluator: a local-first CLI (`shakers` binary) plus a local MCP server. Commands: ai-usage (scan + classify your AI-tool setup, T0-T7), certify (certify Skills from your code), share (branded LinkedIn card).",
5
+ "type": "commonjs",
6
+ "bin": {
7
+ "shakers": "bin/shakers.js"
8
+ },
9
+ "files": [
10
+ "bin/",
11
+ "src/",
12
+ "README.md",
13
+ "LICENSE"
14
+ ],
15
+ "scripts": {
16
+ "start": "node bin/shakers.js",
17
+ "shakers": "node bin/shakers.js",
18
+ "test": "node --test test/*.test.js"
19
+ },
20
+ "engines": {
21
+ "node": ">=18"
22
+ },
23
+ "dependencies": {
24
+ "@livekit/rtc-node": "^0.13.0"
25
+ },
26
+ "keywords": [
27
+ "shakers",
28
+ "cli",
29
+ "ai-usage",
30
+ "ai",
31
+ "developer-tools",
32
+ "skills",
33
+ "certification"
34
+ ],
35
+ "license": "SEE LICENSE IN LICENSE",
36
+ "publishConfig": {
37
+ "access": "public",
38
+ "registry": "https://registry.npmjs.org/"
39
+ }
40
+ }
@@ -0,0 +1,13 @@
1
+ 'use strict';
2
+
3
+ // THE AGENT-CATEGORY VOCABULARY, AND WHICH OF IT IS A SPECIALITY (issue 120).
4
+
5
+ /** The floor category key, as the service's catalog defines it (`other-1`'s category). */
6
+ const FLOOR_CATEGORY = 'other';
7
+
8
+ // True for the floor category only.
9
+ function isFloorCategory(category) {
10
+ return category === FLOOR_CATEGORY;
11
+ }
12
+
13
+ module.exports = { FLOOR_CATEGORY, isFloorCategory };
@@ -0,0 +1,177 @@
1
+ 'use strict';
2
+
3
+ const { scrubSecrets } = require('./agent-synthesis');
4
+ const { requestBackend } = require('./backend-request');
5
+ const { reporterFor, REASON } = require('./model-call-record');
6
+
7
+ // Agent definition-quality evaluation client (ADR-016, agent evaluation).
8
+
9
+ // HOW LONG TO WAIT FOR THE AGENT EVALUATION, and why it keeps growing.
10
+ const DEFAULT_TIMEOUT_MS = 150000;
11
+ const PROMPT_VERSION = 'agent-eval-v1';
12
+
13
+ // Cap each agent definition to a prefix before sending.
14
+ const MAX_DEFINITION_CHARS = 2000;
15
+
16
+ // Builds the frozen-contract body from the deterministic org chart (structure) + the per-agent definition text, scrubbing each definition.
17
+ function buildAgentEvaluationRequest(structuralAgents, definitionsByName, locale = null, consentGranted = false) {
18
+ const defMap = new Map(
19
+ (definitionsByName || []).map((d) => [d.name, d.definition != null ? d.definition : d.description]),
20
+ );
21
+ return {
22
+ agents: (structuralAgents || []).map((a) => ({
23
+ name: a.name,
24
+ // Scrub first, THEN cap to the prefix — so a secret straddling the cut is
25
+ // already redacted and can't leak a half-matched fragment.
26
+ definition: scrubSecrets(defMap.get(a.name) || '').slice(0, MAX_DEFINITION_CHARS),
27
+ tools: Array.isArray(a.tools) ? a.tools : [],
28
+ model: a.model || null,
29
+ parent: a.parent || null,
30
+ })),
31
+ promptVersion: PROMPT_VERSION,
32
+ // ADR-026: detected report language for the rationale + description prose.
33
+ ...(locale === 'es' || locale === 'en' ? { locale } : {}),
34
+ ...(consentGranted === true ? { traceContentConsent: true } : {}),
35
+ };
36
+ }
37
+
38
+ function isValidEvaluationResponse(parsed) {
39
+ return !!parsed && typeof parsed === 'object' && Array.isArray(parsed.evaluations);
40
+ }
41
+
42
+ // The three classification methods the server may report; anything else is
43
+ // coerced to 'unclassified' (never trust an unexpected value).
44
+ const CLASSIFICATION_METHODS = new Set(['deterministic', 'llm', 'unclassified']);
45
+ const UNCLASSIFIED = Object.freeze({
46
+ catalogId: null,
47
+ category: null,
48
+ role: null,
49
+ level: null,
50
+ method: 'unclassified',
51
+ });
52
+
53
+ function normalizeClassification(c) {
54
+ if (!c || typeof c !== 'object') return { ...UNCLASSIFIED };
55
+ const catalogId = typeof c.catalogId === 'string' && c.catalogId ? c.catalogId : null;
56
+ if (!catalogId) return { ...UNCLASSIFIED };
57
+ const method = CLASSIFICATION_METHODS.has(c.method) ? c.method : 'unclassified';
58
+ return {
59
+ catalogId,
60
+ category: typeof c.category === 'string' && c.category ? c.category : null,
61
+ role: typeof c.role === 'string' && c.role ? c.role : null,
62
+ level: typeof c.level === 'string' && c.level ? c.level : null,
63
+ // A real id but method 'unclassified' would be contradictory — treat an id
64
+ // that arrived without a usable method as LLM-inferred (the conservative read).
65
+ method: method === 'unclassified' ? 'llm' : method,
66
+ };
67
+ }
68
+
69
+ // Bounds the improvements to a clean string[] (max 3, trimmed, non-empty).
70
+ function normalizeImprovements(list) {
71
+ return (Array.isArray(list) ? list : [])
72
+ .filter((s) => typeof s === 'string' && s.trim())
73
+ .map((s) => s.trim())
74
+ .slice(0, 3);
75
+ }
76
+
77
+ // WHICH AGENTS THE CALL LEFT OUT (`omittedAgentNames`, server contract 2026-08-03).
78
+ function normalizeOmittedAgentNames(parsed, requestBody, evaluations) {
79
+ const requested = new Set(
80
+ (requestBody && Array.isArray(requestBody.agents) ? requestBody.agents : [])
81
+ .map((a) => (a && typeof a.name === 'string' ? a.name : null))
82
+ .filter(Boolean),
83
+ );
84
+ const answered = new Set((evaluations || []).map((e) => e.name));
85
+
86
+ const declared = (parsed && Array.isArray(parsed.omittedAgentNames) ? parsed.omittedAgentNames : [])
87
+ .filter((n) => typeof n === 'string' && n.trim())
88
+ .map((n) => n.trim())
89
+ .filter((n) => requested.has(n));
90
+
91
+ const derived = [...requested].filter((n) => !answered.has(n));
92
+
93
+ // Deduped, and in the order the agents were REQUESTED so the list reads the same
94
+ // way the report lists them (never in the order a server happened to emit).
95
+ const union = new Set([...declared, ...derived]);
96
+ return [...requested].filter((n) => union.has(n));
97
+ }
98
+
99
+ // Normalizes each evaluation.
100
+ function normalizeEvaluations(list) {
101
+ return (Array.isArray(list) ? list : [])
102
+ .filter((e) => e && typeof e.name === 'string')
103
+ .map((e) => ({
104
+ name: e.name,
105
+ rationale: typeof e.rationale === 'string' ? e.rationale : '',
106
+ // ADR-026: target-language one-line description; null when the server
107
+ // omitted it — the caller falls back to the verbatim phrase.
108
+ description: typeof e.description === 'string' && e.description ? e.description : null,
109
+ // v4 (agent classification): closest catalog agent + how it was matched.
110
+ classification: normalizeClassification(e.classification),
111
+ // v4: 2-3 concrete improvement tips (target language).
112
+ improvements: normalizeImprovements(e.improvements),
113
+ }));
114
+ }
115
+
116
+ // Requests the agent-evaluation endpoint.
117
+ async function requestAgentEvaluation(requestBody, { endpoint, timeoutMs = DEFAULT_TIMEOUT_MS, onOutcome = null } = {}) {
118
+ const outcome = reporterFor(onOutcome, { timeoutMs });
119
+ if (!endpoint) {
120
+ outcome.skip(REASON.NO_ENDPOINT);
121
+ return null;
122
+ }
123
+
124
+ const promptVersion = (requestBody && requestBody.promptVersion) || PROMPT_VERSION;
125
+ const locale = requestBody && (requestBody.locale === 'es' || requestBody.locale === 'en')
126
+ ? requestBody.locale
127
+ : null;
128
+ const traceContentConsent = requestBody && requestBody.traceContentConsent === true;
129
+ const safeBody = {
130
+ promptVersion,
131
+ agents: Array.isArray(requestBody && requestBody.agents)
132
+ ? requestBody.agents.map((a) => ({ ...a, definition: scrubSecrets(a.definition) }))
133
+ : [],
134
+ ...(locale ? { locale } : {}),
135
+ ...(traceContentConsent ? { traceContentConsent: true } : {}),
136
+ };
137
+
138
+ let res;
139
+ try {
140
+ res = await requestBackend({ endpoint, body: safeBody, timeoutMs, trace: outcome.hops });
141
+ } catch (e) {
142
+ outcome.failFromError(e);
143
+ return null; // network error or timeout on every hop: never breaks the local report
144
+ }
145
+
146
+ if (res.status < 200 || res.status >= 300) {
147
+ outcome.fail(REASON.HTTP, { status: res.status, backend: res.backend });
148
+ return null;
149
+ }
150
+
151
+ let parsed;
152
+ try {
153
+ parsed = JSON.parse(res.raw);
154
+ } catch {
155
+ outcome.fail(REASON.MALFORMED_JSON, { status: res.status, backend: res.backend });
156
+ return null; // malformed (non-JSON) body
157
+ }
158
+
159
+ if (!isValidEvaluationResponse(parsed)) {
160
+ outcome.fail(REASON.UNEXPECTED_SHAPE, { status: res.status, backend: res.backend });
161
+ return null;
162
+ }
163
+
164
+ const evaluations = normalizeEvaluations(parsed.evaluations);
165
+ const omittedAgentNames = normalizeOmittedAgentNames(parsed, safeBody, evaluations);
166
+ // A PARTIAL ANSWER IS NOT A FAILURE, and it is not a plain success either.
167
+ outcome.succeed({ status: res.status, backend: res.backend, omitted: omittedAgentNames });
168
+ return { evaluations, promptVersion, omittedAgentNames };
169
+ }
170
+
171
+ module.exports = {
172
+ normalizeOmittedAgentNames,
173
+ PROMPT_VERSION,
174
+ MAX_DEFINITION_CHARS,
175
+ buildAgentEvaluationRequest,
176
+ requestAgentEvaluation,
177
+ };