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,333 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const { execFileSync } = require('child_process');
5
+ const { isRefusedWalkRoot } = require('./scan-exclusions');
6
+
7
+ function isExistingDir(p) {
8
+ try {
9
+ return fs.statSync(p).isDirectory();
10
+ } catch {
11
+ return false;
12
+ }
13
+ }
14
+
15
+ function defaultCwdIsRepo(cwd) {
16
+ try {
17
+ execFileSync('git', ['-C', cwd, 'rev-parse', '--show-toplevel'], {
18
+ stdio: ['ignore', 'ignore', 'ignore'],
19
+ timeout: 5000,
20
+ });
21
+ return true;
22
+ } catch {
23
+ return false;
24
+ }
25
+ }
26
+
27
+ const AI_USAGE_INPUT_SCHEMA = {
28
+ type: 'object',
29
+ properties: {
30
+ consent: {
31
+ type: 'object',
32
+ description:
33
+ 'Talent consent to persist the AI-usage report to Shakers. Ask the user in the chat before calling; do not assume it. When granted, an email is required.',
34
+ properties: {
35
+ granted: { type: 'boolean', description: 'Whether the talent granted consent to send the report.' },
36
+ email: { type: 'string', description: 'The talent email the report is attributed to. Required when granted is true.' },
37
+ },
38
+ required: ['granted'],
39
+ },
40
+ repoScope: {
41
+ type: 'object',
42
+ description: 'Which repositories feed the evaluation. Defaults to the current repository only.',
43
+ properties: {
44
+ mode: {
45
+ type: 'string',
46
+ enum: ['current', 'all', 'list'],
47
+ description: "'current' = only the repo at root; 'all' = machine-wide session signals; 'list' = the repos named in `repos`.",
48
+ },
49
+ repos: {
50
+ type: 'array',
51
+ items: { type: 'string' },
52
+ description: "Repo names or paths, used only when mode is 'list'.",
53
+ },
54
+ },
55
+ },
56
+ root: {
57
+ type: 'string',
58
+ description: 'Absolute path to the repository to evaluate. Defaults to the server working directory.',
59
+ },
60
+ lang: { type: 'string', enum: ['es', 'en'], description: 'Report language.' },
61
+ },
62
+ };
63
+
64
+ async function defaultCaptureStdout(fn) {
65
+ const chunks = [];
66
+ const original = process.stdout.write.bind(process.stdout);
67
+ process.stdout.write = (chunk, encoding, cb) => {
68
+ chunks.push(typeof chunk === 'string' ? chunk : chunk.toString(encoding || 'utf8'));
69
+ if (typeof encoding === 'function') encoding();
70
+ else if (typeof cb === 'function') cb();
71
+ return true;
72
+ };
73
+ try {
74
+ await fn();
75
+ } finally {
76
+ process.stdout.write = original;
77
+ }
78
+ return chunks.join('');
79
+ }
80
+
81
+ function parseReportJson(captured) {
82
+ const text = captured || '';
83
+ try {
84
+ return JSON.parse(text.trim());
85
+ } catch {
86
+ const start = text.indexOf('{');
87
+ if (start === -1) {
88
+ throw new Error('ai-usage --json produced no JSON document on stdout');
89
+ }
90
+ return JSON.parse(text.slice(start));
91
+ }
92
+ }
93
+
94
+ function buildArgv({ root, repoScope, lang }) {
95
+ const argv = ['--json', '--no-save', '--no-ai'];
96
+ if (root) argv.push('--root', root);
97
+ const mode = repoScope && repoScope.mode;
98
+ if (mode === 'all') {
99
+ argv.push('--all-repos');
100
+ } else if (mode === 'list' && Array.isArray(repoScope.repos) && repoScope.repos.length > 0) {
101
+ argv.push('--repos', repoScope.repos.join(','));
102
+ }
103
+ if (lang === 'es' || lang === 'en') argv.push('--lang', lang);
104
+ return argv;
105
+ }
106
+
107
+ function makeAiUsageTool(deps = {}) {
108
+ const {
109
+ runAiUsage = require('../bin/ai-usage').run,
110
+ autoShare = require('./share').autoShare,
111
+ recordConsent = require('./share').recordConsent,
112
+ isValidEmail = require('./share').isValidEmail,
113
+ persistFootprint = require('./report-store').persistFootprint,
114
+ captureStdout = defaultCaptureStdout,
115
+ cwdIsRepo = defaultCwdIsRepo,
116
+ loadAuthSession = require('./auth-session-store').loadAuthSession,
117
+ sessionStatus = require('./auth-session-store').sessionStatus,
118
+ getUsageDiscoveredInventoryEndpoint = require('./config').getUsageDiscoveredInventoryEndpoint,
119
+ requestDiscoveredInventory = require('./inventory-client').requestDiscoveredInventory,
120
+ pollClassifiedInventory = require('./mcp-ai-usage-result-tool').pollClassifiedInventory,
121
+ tierLadder = require('./tier-engine').tierLadder,
122
+ inlineWaitMs = 35000,
123
+ // "My work with AI" preview, from the same hub endpoint the front paints.
124
+ resolveAiProfilePreview = require('./ai-profile-preview').resolveAiProfilePreview,
125
+ ensureFreshSession = require('./session-refresh').ensureFreshSession,
126
+ getAiProfileEndpoint = require('./config').getAiProfileEndpoint,
127
+ // Plain-text render of the full report (terminal sections + matrix/vision/howIWork), shared with the CLI (no ANSI).
128
+ renderFullReportText = require('./render-report-text').renderFullReportText,
129
+ detectFlowLang = require('./i18n').detectFlowLang,
130
+ aiFluencyWaitMs = 30000,
131
+ } = deps;
132
+
133
+ async function handler(args = {}) {
134
+ const mode = (args.repoScope && args.repoScope.mode) || 'current';
135
+ const lang = args.lang === 'es' || args.lang === 'en' ? args.lang : detectFlowLang();
136
+
137
+ let root;
138
+ if (typeof args.root === 'string' && args.root) {
139
+ if (!isExistingDir(args.root)) {
140
+ return { ok: false, reason: 'invalid-root', message: `root is not an existing directory: ${args.root}` };
141
+ }
142
+ root = args.root;
143
+ } else {
144
+ const cwd = process.cwd();
145
+ if (!isRefusedWalkRoot(cwd) && cwdIsRepo(cwd)) {
146
+ root = cwd;
147
+ } else if (mode === 'all') {
148
+ root = cwd;
149
+ } else {
150
+ return {
151
+ ok: false,
152
+ reason: 'no-repo-root',
153
+ message: 'The Shakers MCP server is running from a non-repository directory, so there is no repository to evaluate. Pass an absolute path to the repository via `root`, or use repoScope.mode "all" for a machine-wide report.',
154
+ };
155
+ }
156
+ }
157
+
158
+ const consent = args.consent || {};
159
+ const granted = consent.granted === true;
160
+ const email = typeof consent.email === 'string' ? consent.email.trim() : '';
161
+
162
+ const verifySession = loadAuthSession();
163
+ const emailVerifiedByLogin =
164
+ sessionStatus(verifySession) === 'active'
165
+ && !!verifySession
166
+ && isValidEmail(verifySession.email);
167
+
168
+ if (granted) {
169
+ if (!isValidEmail(email)) {
170
+ throw new Error('consent.granted is true but consent.email is missing or invalid — ask the talent for their email.');
171
+ }
172
+ if (emailVerifiedByLogin) {
173
+ recordConsent('granted', verifySession.email, { verified: true });
174
+ } else {
175
+ recordConsent('granted', email, { verified: false });
176
+ }
177
+ } else {
178
+ recordConsent('denied');
179
+ }
180
+
181
+ const canSubmit = granted && emailVerifiedByLogin;
182
+ const submittedEmail = canSubmit ? verifySession.email : null;
183
+
184
+ const argv = buildArgv({ root, repoScope: args.repoScope, lang: args.lang });
185
+
186
+ let parsed;
187
+ const captured = await captureStdout(() => runAiUsage(argv));
188
+ parsed = parseReportJson(captured);
189
+ const { report, maturity } = parsed;
190
+
191
+ let savedLocally = false;
192
+ try {
193
+ persistFootprint({ root, report, maturity });
194
+ savedLocally = true;
195
+ } catch {
196
+ savedLocally = false;
197
+ }
198
+
199
+ const send = canSubmit
200
+ ? await autoShare(report, maturity, { root })
201
+ : { ok: false, skipped: true, reason: granted ? 'email-unverified' : 'consent-denied' };
202
+ const submitted = !!(send && send.ok);
203
+
204
+ const hasAgents = Array.isArray(report.agents) && report.agents.length > 0;
205
+
206
+ let enrichment = submitted && hasAgents ? 'pending' : 'none';
207
+ let enrichedAgents = null;
208
+ if (submitted && hasAgents) {
209
+ const session = loadAuthSession();
210
+ const endpoint = getUsageDiscoveredInventoryEndpoint();
211
+ if (sessionStatus(session) === 'active' && endpoint) {
212
+ const polled = await pollClassifiedInventory({
213
+ accessToken: session.accessToken,
214
+ endpoint,
215
+ requestDiscoveredInventory,
216
+ capMs: inlineWaitMs,
217
+ });
218
+ if (polled.status === 'ready') {
219
+ enrichment = 'ready';
220
+ enrichedAgents = polled.agents;
221
+ }
222
+ }
223
+ }
224
+
225
+ // "My work with AI" preview: only after a real submit this run.
226
+ let aiProfile = { status: 'unavailable', preview: null };
227
+ if (submitted) {
228
+ const usageSession = loadAuthSession();
229
+ if (sessionStatus(usageSession) === 'active') {
230
+ aiProfile = await resolveAiProfilePreview(
231
+ {
232
+ session: usageSession,
233
+ endpoint: getAiProfileEndpoint(),
234
+ timeoutMs: aiFluencyWaitMs,
235
+ },
236
+ {
237
+ refreshToken: async () => {
238
+ const fresh = await ensureFreshSession(process.env);
239
+ return fresh ? { accessToken: fresh.accessToken, hubAccessToken: fresh.hubAccessToken } : null;
240
+ },
241
+ },
242
+ );
243
+ }
244
+ }
245
+
246
+ const displayAgents = enrichedAgents
247
+ ? enrichedAgents.map((a) => ({ name: a.name, category: a.category, role: a.role }))
248
+ : (report.agents || []).map((a) => ({ name: a.name }));
249
+
250
+ // Same full report the CLI terminal prints, as plain markdown (no ANSI), so a chat client shows the talent the same thing.
251
+ const text = renderFullReportText(
252
+ report,
253
+ maturity,
254
+ { status: aiProfile.status, preview: aiProfile.preview },
255
+ { lang },
256
+ );
257
+
258
+ let message;
259
+ if (submitted) {
260
+ message =
261
+ enrichment === 'ready'
262
+ ? 'Classification complete — agent categories and descriptions are ready.'
263
+ : enrichment === 'pending'
264
+ ? 'Report submitted. Shakers is classifying the agents server-side (this is normal, not an error) — call ai_usage_result shortly to get their categories and descriptions.'
265
+ : 'Report submitted.';
266
+ } else if (send.reason === 'consent-denied') {
267
+ message = 'Not submitted — consent denied. The report was computed and saved locally only; nothing was sent to Shakers. Do not point the talent to "My work with AI".';
268
+ } else if (send.reason === 'email-unverified') {
269
+ message = "Not submitted — the talent's email is not verified. Ask them to log in / verify with the Shakers CLI (`shakers login`) first, then run ai_usage again; nothing was sent to Shakers.";
270
+ } else if (send.skipped) {
271
+ message = `Not submitted (${send.reason}). The report was computed and saved locally only; nothing was sent to Shakers.`;
272
+ } else {
273
+ message = `Submit failed (${send.reason}). The report was saved locally only; retry later.`;
274
+ }
275
+
276
+ return {
277
+ report,
278
+ maturity,
279
+ send,
280
+ savedLocally,
281
+ enrichment,
282
+ agents: enrichedAgents,
283
+ // Plain-text "My work with AI" report (no ANSI), same content/layout as the CLI.
284
+ text,
285
+ // "My work with AI" preview: status ready | pending | unavailable.
286
+ aiProfile,
287
+ next: submitted && enrichment === 'pending' ? 'ai_usage_result' : null,
288
+ message,
289
+ display: {
290
+ tier: {
291
+ key: maturity.tierKey,
292
+ name: maturity.tierName,
293
+ setupLevel: maturity.name,
294
+ score: maturity.score,
295
+ },
296
+ // Matrix (3x3) + vision (E) + howIWork (F) + traction; null until projected.
297
+ aiProfile: {
298
+ status: aiProfile.status,
299
+ setup: aiProfile.preview ? aiProfile.preview.matrix.setupCode : null,
300
+ usage: aiProfile.preview ? aiProfile.preview.matrix.usageLevel : null,
301
+ cell: aiProfile.preview ? aiProfile.preview.matrix.cell : null,
302
+ isAiNative: aiProfile.preview ? aiProfile.preview.matrix.isAiNative : false,
303
+ vision: aiProfile.preview ? aiProfile.preview.vision : null,
304
+ howIWork: aiProfile.preview ? aiProfile.preview.howIWork : null,
305
+ traction: aiProfile.preview ? aiProfile.preview.traction : null,
306
+ },
307
+ tierLegend: tierLadder(),
308
+ agents: displayAgents,
309
+ },
310
+ scope: {
311
+ mode: (args.repoScope && args.repoScope.mode) || 'current',
312
+ root,
313
+ },
314
+ consent: { granted, email: canSubmit ? submittedEmail : (granted ? email : null) },
315
+ };
316
+ }
317
+
318
+ return {
319
+ name: 'ai_usage',
320
+ description:
321
+ "Measure how a Shakers talent uses AI in their work: scans this machine and repository for AI-tool usage (subagents, MCP servers, sessions, git activity), scores their AI-maturity tier (T0-T7), and, with the talent's consent, submits the report to Shakers so their profile reflects it. Runs entirely on the talent's own machine. Ask the talent for consent and their email in the chat before calling. Submission also requires an active, email-verified Shakers session (via `shakers login` / registration OTP): with consent denied OR no verified session the report is computed and saved locally only and nothing is sent — check the `send` field for the outcome. Always fast and deterministic: it runs no LLM locally. Agent descriptions and catalog classification are produced by Shakers server-side. This waits briefly (~30s) and, if classification finishes, returns the enriched agents inline; otherwise it returns enrichment:'pending' (normal progress, not an error) and you should call `ai_usage_result` to get them. The `text` field is a ready-to-show plain-text 'My work with AI' report (Markdown, no ANSI) mirroring the CLI terminal — render it verbatim to the talent; the matrix grid is inside a fenced code block, keep it fenced so it stays aligned. Use the `display` block for structured access: show the tier (with the tierLegend explaining T0-T7) and the agent list by NAME and category/role. IDs and codes (agent `code`, skillId, catalogId) are internal handles for the add_* tools only — never show raw IDs or codes to the talent.",
322
+ inputSchema: AI_USAGE_INPUT_SCHEMA,
323
+ handler,
324
+ };
325
+ }
326
+
327
+ module.exports = {
328
+ makeAiUsageTool,
329
+ AI_USAGE_INPUT_SCHEMA,
330
+ buildArgv,
331
+ parseReportJson,
332
+ defaultCaptureStdout,
333
+ };
@@ -0,0 +1,91 @@
1
+ 'use strict';
2
+
3
+ const ALMA_CHAT_SCHEMA = {
4
+ type: 'object',
5
+ properties: {
6
+ message: { type: 'string', description: 'The message for Alma.' },
7
+ },
8
+ required: ['message'],
9
+ };
10
+
11
+ const ALMA_DECISION_SCHEMA = {
12
+ type: 'object',
13
+ properties: {
14
+ approved: { type: 'boolean', description: "The talent's decision on the pending confirmation: true = go ahead, false = cancel." },
15
+ },
16
+ required: ['approved'],
17
+ };
18
+
19
+ function makeAlmaTools(deps = {}) {
20
+ const {
21
+ askAlma = (opts) => require('./alma-client').askAlma({}, opts),
22
+ decideAlma = (opts) => require('./alma-client').decideAlma({}, opts),
23
+ loadAuthSession = require('./auth-session-store').loadAuthSession,
24
+ sessionStatus = require('./auth-session-store').sessionStatus,
25
+ } = deps;
26
+
27
+ const requireSession = () => {
28
+ const session = loadAuthSession();
29
+ if (sessionStatus(session) !== 'active') {
30
+ throw new Error('no active Shakers session — call the login tool (or run `shakers login`) first (alma is session-gated).');
31
+ }
32
+ return session;
33
+ };
34
+
35
+ const captureEvents = () => {
36
+ let pending = null;
37
+ const links = [];
38
+ const onEvent = (evt) => {
39
+ if (!evt) return;
40
+ if (evt.type === 'confirmation_required') {
41
+ pending = { text: evt.text || '', irreversible: !!(evt.detail && String(evt.detail).length) };
42
+ } else if (evt.type === 'link' && evt.data && evt.data.to) {
43
+ links.push(String(evt.data.to));
44
+ }
45
+ };
46
+ return { onEvent, snapshot: () => ({ pending, links }) };
47
+ };
48
+
49
+ const relay = (res, cap) => {
50
+ if (!res.ok) return { ok: false, reason: res.reason };
51
+ const { pending, links } = cap.snapshot();
52
+ const out = { ok: true, answer: res.text };
53
+ if (pending) out.pendingConfirmation = pending;
54
+ if (links.length) out.webLinks = links;
55
+ return out;
56
+ };
57
+
58
+ async function almaChat(args = {}) {
59
+ const session = requireSession();
60
+ const message = typeof args.message === 'string' ? args.message.trim() : '';
61
+ if (!message) return { ok: false, reason: 'no-message' };
62
+ const cap = captureEvents();
63
+ const res = await askAlma({ hubAccessToken: session.hubAccessToken, message, onEvent: cap.onEvent });
64
+ return relay(res, cap);
65
+ }
66
+
67
+ async function almaDecision(args = {}) {
68
+ const session = requireSession();
69
+ if (typeof args.approved !== 'boolean') return { ok: false, reason: 'no-decision' };
70
+ const cap = captureEvents();
71
+ const res = await decideAlma({ hubAccessToken: session.hubAccessToken, approved: args.approved, onEvent: cap.onEvent });
72
+ return relay(res, cap);
73
+ }
74
+
75
+ return [
76
+ {
77
+ name: 'alma_chat',
78
+ description: "Send a message to Alma, the Shakers AI assistant, and get her answer (about the talent's profile, rate, availability, skills, positions, applications). Continuity is automatic across turns (same session). When Alma proposes a profile change she returns `pendingConfirmation { text, irreversible }` — DO NOT auto-approve: show the talent `pendingConfirmation.text` (and warn when irreversible), ask them yes/no, then call `alma_decision` with their answer. `webLinks` are web-only navigation targets. Relay the answer as-is; never show internal Shakers ids/codes.",
79
+ inputSchema: ALMA_CHAT_SCHEMA,
80
+ handler: almaChat,
81
+ },
82
+ {
83
+ name: 'alma_decision',
84
+ description: "Relay the talent's decision on the pending Alma action from alma_chat: approved=true makes Alma perform the change, approved=false cancels it. Call this ONLY after the talent has explicitly answered yes/no to the pendingConfirmation — never decide on their behalf.",
85
+ inputSchema: ALMA_DECISION_SCHEMA,
86
+ handler: almaDecision,
87
+ },
88
+ ];
89
+ }
90
+
91
+ module.exports = { makeAlmaTools, ALMA_CHAT_SCHEMA, ALMA_DECISION_SCHEMA };
@@ -0,0 +1,42 @@
1
+ 'use strict';
2
+
3
+ // MCP: the positions the talent applied to + status.
4
+
5
+ const LIST_APPLICATIONS_SCHEMA = { type: 'object', properties: {} };
6
+
7
+ function makeApplicationsTools(deps = {}) {
8
+ const {
9
+ fetchApplications = (opts) => require('./applications-client').fetchApplications({}, opts),
10
+ loadAuthSession = require('./auth-session-store').loadAuthSession,
11
+ sessionStatus = require('./auth-session-store').sessionStatus,
12
+ } = deps;
13
+
14
+ const requireLoginSession = () => {
15
+ const session = loadAuthSession();
16
+ if (sessionStatus(session) !== 'active') {
17
+ throw new Error('no active Shakers session — call the login tool (or run `shakers login`) first (list_applications is session-gated).');
18
+ }
19
+ return session;
20
+ };
21
+
22
+ async function listApplications() {
23
+ const session = requireLoginSession();
24
+ const res = await fetchApplications({ hubAccessToken: session.hubAccessToken });
25
+ if (!res.ok) return { ok: false, reason: res.reason };
26
+ return { ok: true, applications: res.items || [] };
27
+ }
28
+
29
+ return [
30
+ {
31
+ name: 'list_applications',
32
+ description: "List the positions the talent has applied to, with each candidature's status. Read-only. Requires an active session. Any positionId/projectId in the result are INTERNAL handles for calling tools — never show or mention them to the talent; refer to a project by its title/company.",
33
+ inputSchema: LIST_APPLICATIONS_SCHEMA,
34
+ handler: listApplications,
35
+ },
36
+ ];
37
+ }
38
+
39
+ module.exports = {
40
+ makeApplicationsTools,
41
+ LIST_APPLICATIONS_SCHEMA,
42
+ };
@@ -0,0 +1,50 @@
1
+ 'use strict';
2
+
3
+ // MCP: ask Alma. Returns the AGGREGATED answer (MCP does not stream).
4
+
5
+ const ASK_SCHEMA = {
6
+ type: 'object',
7
+ properties: {
8
+ message: { type: 'string', description: 'The question / message for Alma.' },
9
+ },
10
+ required: ['message'],
11
+ };
12
+
13
+ function makeAskTools(deps = {}) {
14
+ const {
15
+ askAlma = (opts) => require('./alma-client').askAlma({}, opts),
16
+ loadAuthSession = require('./auth-session-store').loadAuthSession,
17
+ sessionStatus = require('./auth-session-store').sessionStatus,
18
+ } = deps;
19
+
20
+ const requireLoginSession = () => {
21
+ const session = loadAuthSession();
22
+ if (sessionStatus(session) !== 'active') {
23
+ throw new Error('no active Shakers session — call the login tool (or run `shakers login`) first (ask is session-gated).');
24
+ }
25
+ return session;
26
+ };
27
+
28
+ async function ask(args = {}) {
29
+ const session = requireLoginSession();
30
+ const message = typeof args.message === 'string' ? args.message.trim() : '';
31
+ if (!message) return { ok: false, reason: 'no-message' };
32
+ const res = await askAlma({ hubAccessToken: session.hubAccessToken, message });
33
+ if (!res.ok) return { ok: false, reason: res.reason };
34
+ return { ok: true, answer: res.text };
35
+ }
36
+
37
+ return [
38
+ {
39
+ name: 'ask',
40
+ description: 'Ask Alma, the Shakers AI assistant, a question (about your profile, rates, availability, skills, open positions, applications). Returns Alma\'s full answer — relay it as-is; do not surface any internal Shakers ids/codes to the talent.',
41
+ inputSchema: ASK_SCHEMA,
42
+ handler: ask,
43
+ },
44
+ ];
45
+ }
46
+
47
+ module.exports = {
48
+ makeAskTools,
49
+ ASK_SCHEMA,
50
+ };