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,173 @@
1
+ 'use strict';
2
+
3
+ // MCP: two provider-agnostic tools (start returns URL+user_code to relay VERBATIM; poll finishes). device_code stays in the local store, never in a result.
4
+
5
+ const START_SCHEMA = {
6
+ type: 'object',
7
+ properties: {
8
+ provider: { type: 'string', enum: ['google', 'linkedin'], description: "Optional social provider. Defaults to 'google'. Same device flow either way; it selects which social button the sign-in page opens." },
9
+ mode: { type: 'string', enum: ['login', 'register'], description: "Optional. 'login' signs an existing talent in; 'register' creates the account if new (the profile steps come from the register/onboarding tools afterwards). Defaults to 'login'." },
10
+ freelanceType: { type: 'string', description: 'Optional, register only: FREELANCE | EMPLOYEE | AGENCY | POTENTIAL_FREELANCE. Used to complete a new account.' },
11
+ preferredLanguage: { type: 'string', description: 'Optional, register only: es | en | it | pt | fr.' },
12
+ },
13
+ };
14
+
15
+ const POLL_SCHEMA = { type: 'object', properties: {} };
16
+
17
+ function makeDeviceLoginTools(deps = {}) {
18
+ const {
19
+ getDeviceAuthorizeEndpoint = require('./config').getDeviceAuthorizeEndpoint,
20
+ getDeviceTokenEndpoint = require('./config').getDeviceTokenEndpoint,
21
+ getAuthTokenEndpoint = require('./config').getAuthTokenEndpoint,
22
+ getCompleteRegistrationEndpoint = require('./config').getCompleteRegistrationEndpoint,
23
+ startDeviceLogin = require('./device-login').startDeviceLogin,
24
+ pollDeviceToken = require('./device-login').pollDeviceToken,
25
+ finalizeDeviceSession = require('./device-login').finalizeDeviceSession,
26
+ appendProviderParam = require('./device-login').appendProviderParam,
27
+ savePendingDevice = require('./device-auth-store').savePendingDevice,
28
+ loadPendingDevice = require('./device-auth-store').loadPendingDevice,
29
+ clearPendingDevice = require('./device-auth-store').clearPendingDevice,
30
+ saveAuthSession = require('./auth-session-store').saveAuthSession,
31
+ buildRegistrationContext = require('./signup-client').buildRegistrationContext,
32
+ } = deps;
33
+
34
+ async function start(args = {}) {
35
+ const mode = args.mode === 'register' ? 'register' : 'login';
36
+ const provider = args.provider === 'linkedin' ? 'linkedin' : 'google';
37
+ const authorizeEndpoint = getDeviceAuthorizeEndpoint();
38
+ const tokenEndpoint = getDeviceTokenEndpoint();
39
+ const authTokenEndpoint = getAuthTokenEndpoint();
40
+ if (!authorizeEndpoint || !tokenEndpoint || !authTokenEndpoint) {
41
+ return { ok: false, reason: 'no-endpoint', message: 'No sign-in endpoint configured — set SHAKERS_CLI_HUB_BASE.' };
42
+ }
43
+
44
+ const authz = await startDeviceLogin({ authorizeEndpoint });
45
+ if (!authz.ok) return { ok: false, reason: authz.reason || 'authorize-failed' };
46
+
47
+ const verificationUrl = appendProviderParam(authz.verificationUriComplete, provider);
48
+ const expiresAt = new Date(Date.now() + authz.expiresIn * 1000).toISOString();
49
+ try {
50
+ savePendingDevice({
51
+ deviceCode: authz.deviceCode,
52
+ verificationUrl,
53
+ userCode: authz.userCode,
54
+ tokenEndpoint,
55
+ authTokenEndpoint,
56
+ interval: authz.interval,
57
+ expiresAt,
58
+ mode,
59
+ provider,
60
+ registerContext: mode === 'register'
61
+ ? { freelanceType: args.freelanceType || null, preferredLanguage: args.preferredLanguage || null }
62
+ : null,
63
+ });
64
+ } catch {
65
+ return { ok: false, reason: 'pending-persist-failed' };
66
+ }
67
+
68
+ return {
69
+ ok: true,
70
+ status: 'pending',
71
+ provider,
72
+ verificationUrl,
73
+ verificationUri: authz.verificationUri,
74
+ userCode: authz.userCode,
75
+ expiresInSeconds: authz.expiresIn,
76
+ pollIntervalSeconds: authz.interval,
77
+ message: `Show the talent this link and code so they can authorize in their own browser — relay both verbatim, do not invent or complete the code:\n\n${verificationUrl}\n\nCode: ${authz.userCode}\n\nThen call social_signin_poll; if it returns status "pending", wait pollIntervalSeconds and call it again.`,
78
+ };
79
+ }
80
+
81
+ async function poll() {
82
+ const pending = loadPendingDevice();
83
+ if (!pending || !pending.deviceCode) {
84
+ return { ok: false, status: 'no-pending', message: 'No social sign-in is in progress — call social_signin_start first.' };
85
+ }
86
+
87
+ const res = await pollDeviceToken({ deviceCode: pending.deviceCode, tokenEndpoint: pending.tokenEndpoint });
88
+
89
+ const linkBlock = pending.verificationUrl
90
+ ? `\n\n${pending.verificationUrl}\n\nCode: ${pending.userCode || ''}`
91
+ : '';
92
+
93
+ if (res.status === 'pending') {
94
+ return {
95
+ ok: true,
96
+ status: 'pending',
97
+ pollIntervalSeconds: pending.interval,
98
+ verificationUrl: pending.verificationUrl,
99
+ userCode: pending.userCode,
100
+ message: `Not authorized yet. Keep showing the talent this link and code so they can finish in their browser:${linkBlock}\n\nThen wait pollIntervalSeconds and call social_signin_poll again.`,
101
+ };
102
+ }
103
+ if (res.status === 'slow_down') {
104
+ const interval = pending.interval + 5;
105
+ try { savePendingDevice({ ...pending, interval }); } catch { /* keep the old interval */ }
106
+ return {
107
+ ok: true,
108
+ status: 'pending',
109
+ pollIntervalSeconds: interval,
110
+ verificationUrl: pending.verificationUrl,
111
+ userCode: pending.userCode,
112
+ message: `Polling too fast — wait pollIntervalSeconds and call social_signin_poll again. Keep the talent's link and code visible:${linkBlock}`,
113
+ };
114
+ }
115
+ if (res.status === 'denied') {
116
+ clearPendingDevice();
117
+ return { ok: false, status: 'denied', message: 'The talent cancelled or did not grant access. No session was started.' };
118
+ }
119
+ if (res.status === 'expired') {
120
+ clearPendingDevice();
121
+ return { ok: false, status: 'expired', message: 'The sign-in request expired. Call social_signin_start to try again.' };
122
+ }
123
+ if (res.status !== 'authorized') {
124
+ return { ok: false, status: 'error', reason: res.reason || 'poll-failed' };
125
+ }
126
+
127
+ // finalize enforces the order: NEW user -> complete-registration before the JWT exchange. freelanceType is a transient default.
128
+ const rc = pending.registerContext || {};
129
+ const registrationContext = buildRegistrationContext({
130
+ preferredLanguage: rc.preferredLanguage || undefined,
131
+ newsletterConsent: false,
132
+ freelanceType: rc.freelanceType || 'POTENTIAL_FREELANCE',
133
+ });
134
+ const fin = await finalizeDeviceSession({
135
+ sessionToken: res.accessToken,
136
+ isNewUser: res.isNewUser,
137
+ authTokenEndpoint: pending.authTokenEndpoint,
138
+ completeRegistrationEndpoint: getCompleteRegistrationEndpoint(),
139
+ registrationContext,
140
+ });
141
+ if (!fin.ok) {
142
+ // The authorization was consumed; a re-poll can't recover — never treat this as logged in.
143
+ clearPendingDevice();
144
+ return { ok: false, status: 'error', reason: fin.reason };
145
+ }
146
+ try {
147
+ saveAuthSession({ accessToken: fin.token, hubAccessToken: fin.token, email: res.email || pending.email || null });
148
+ } catch {
149
+ clearPendingDevice();
150
+ return { ok: false, status: 'error', reason: 'session-persist-failed' };
151
+ }
152
+
153
+ clearPendingDevice();
154
+ return { ok: true, status: 'authorized', method: pending.provider || 'google', email: res.email || null, isNewUser: !!res.isNewUser };
155
+ }
156
+
157
+ return [
158
+ {
159
+ name: 'social_signin_start',
160
+ description: "Begin social sign-in (or registration) via the device flow — provider 'google' (default) or 'linkedin'. Returns a verificationUrl and a userCode: show BOTH to the talent verbatim — they open the URL and enter the code in their own browser (they never type anything in the chat, and you never invent or fill the code). Pass mode 'register' to create the account if it is new. After showing them, call social_signin_poll. Returns { ok, status, provider, verificationUrl, userCode, pollIntervalSeconds, ... }.",
161
+ inputSchema: START_SCHEMA,
162
+ handler: start,
163
+ },
164
+ {
165
+ name: 'social_signin_poll',
166
+ description: "Check whether the talent has finished the social sign-in started by social_signin_start, and persist the session when they have. Call it after showing the code; if it returns status 'pending', wait pollIntervalSeconds and call it again — the 'pending' response repeats the verificationUrl and userCode so you can keep the clickable link in front of the talent. Returns status 'authorized' (session saved, with isNewUser + method), 'pending', 'denied', or 'expired'.",
167
+ inputSchema: POLL_SCHEMA,
168
+ handler: poll,
169
+ },
170
+ ];
171
+ }
172
+
173
+ module.exports = { makeDeviceLoginTools, START_SCHEMA, POLL_SCHEMA };
@@ -0,0 +1,67 @@
1
+ 'use strict';
2
+
3
+ // MCP: list the Positions/Projects available to the talent (Find Positions),
4
+ // paginated + filtered server-side. Same params as the CLI so an agent can page.
5
+
6
+ const { TABS, ATTENDANCE, DEFAULT_LIMIT } = require('./find-projects-client');
7
+ const { splitRecommended } = require('./find-projects-flow');
8
+
9
+ const FIND_PROJECTS_SCHEMA = {
10
+ type: 'object',
11
+ properties: {
12
+ tab: { type: 'string', enum: TABS, description: 'all (default order, match desc) or saved (bookmarked).' },
13
+ page: { type: 'integer', minimum: 1, description: '1-based page. offset = (page-1)*limit.' },
14
+ limit: { type: 'integer', minimum: 1, maximum: 50, description: `Items per page (default ${DEFAULT_LIMIT}, max 50).` },
15
+ attendance: { type: 'string', enum: Object.keys(ATTENDANCE), description: 'Work mode filter (server-side).' },
16
+ country: { type: 'string', description: 'ISO 3166-1 alpha-2 country filter (server-side).' },
17
+ recommended: { type: 'boolean', description: 'Client-side: keep only items with a match on this page. If none, falls back to the available items (never empty). `recommended` in the result is "recommended" | "fallback" | null.' },
18
+ },
19
+ };
20
+
21
+ function makeFindProjectsTools(deps = {}) {
22
+ const {
23
+ fetchFindProjects = (opts) => require('./find-projects-client').fetchFindProjects({}, opts),
24
+ loadAuthSession = require('./auth-session-store').loadAuthSession,
25
+ sessionStatus = require('./auth-session-store').sessionStatus,
26
+ } = deps;
27
+
28
+ const requireLoginSession = () => {
29
+ const session = loadAuthSession();
30
+ if (sessionStatus(session) !== 'active') {
31
+ throw new Error('no active Shakers session — call the login tool (or run `shakers login`) first (find_projects is session-gated).');
32
+ }
33
+ return session;
34
+ };
35
+
36
+ async function findProjects(args = {}) {
37
+ const session = requireLoginSession();
38
+ const tab = TABS.includes(args.tab) ? args.tab : 'all';
39
+ const limit = Number.isInteger(args.limit) && args.limit > 0 ? Math.min(args.limit, 50) : DEFAULT_LIMIT;
40
+ const page = Number.isInteger(args.page) && args.page > 0 ? args.page : 1;
41
+ const offset = (page - 1) * limit;
42
+ const attRaw = typeof args.attendance === 'string' ? args.attendance.trim().toLowerCase() : null;
43
+ const attendance = attRaw && ATTENDANCE[attRaw] ? ATTENDANCE[attRaw] : null;
44
+ const countryRaw = typeof args.country === 'string' ? args.country.trim().toUpperCase() : null;
45
+ const country = countryRaw && /^[A-Z]{2}$/.test(countryRaw) ? countryRaw : null;
46
+
47
+ const res = await fetchFindProjects({ hubAccessToken: session.hubAccessToken, tab, limit, offset, attendance, country });
48
+ if (!res.ok) return { ok: false, reason: res.reason };
49
+ // Same client-side recommended filter (+ fallback to available) as the CLI.
50
+ const { shown, mode } = splitRecommended(res.items || [], !!args.recommended);
51
+ return { ok: true, tab, page, limit, filters: { attendance, country, recommended: !!args.recommended }, recommended: mode, meta: res.meta || null, items: shown };
52
+ }
53
+
54
+ return [
55
+ {
56
+ name: 'find_projects',
57
+ description: "List the Positions/Projects available to the talent right now (Shakers Find Positions), paginated and ordered by match desc. Each item carries its parent Project id/name, title, company, budget, work mode and match. Supports server-side pagination (page/limit) and filters (tab all|saved, attendance, country ISO2). `meta` carries {page,pageSize,total,totalPages}. Requires an active session. The positionId/projectId in each item are INTERNAL handles for calling other tools (e.g. show_project, save_project) — NEVER show or mention them to the talent; refer to a project by its title/company only.",
58
+ inputSchema: FIND_PROJECTS_SCHEMA,
59
+ handler: findProjects,
60
+ },
61
+ ];
62
+ }
63
+
64
+ module.exports = {
65
+ makeFindProjectsTools,
66
+ FIND_PROJECTS_SCHEMA,
67
+ };
@@ -0,0 +1,245 @@
1
+ 'use strict';
2
+
3
+ // Run by install.sh as its last step: points every MCP client found on this machine at this CLI's
4
+ // `mcp` server. Absolute paths, because Claude Desktop launched from the Dock has no
5
+ // shell PATH and a bare `"command": "shakers"` never starts.
6
+
7
+ const fs = require('fs');
8
+ const os = require('os');
9
+ const path = require('path');
10
+ const { spawnSync } = require('child_process');
11
+
12
+ const SERVER_NAME = 'shakers';
13
+ // The name the first published setup guide used.
14
+ const LEGACY_SERVER_NAME = 'shakers-ai-usage';
15
+ const FORWARDED_ENV = [
16
+ 'SHAKERS_PROFILE',
17
+ 'SHAKERS_CLI_CERTS_BASE',
18
+ 'SHAKERS_CLI_HUB_BASE',
19
+ 'SHAKERS_CLI_PROFILE_URL',
20
+ 'SHAKERS_CLI_INGEST_ENDPOINT',
21
+ ];
22
+
23
+ // The `node` the shell finds on PATH, unresolved: Homebrew's /opt/homebrew/bin/node survives
24
+ // `brew upgrade node`, the versioned Cellar path process.execPath points at does not.
25
+ function stableNodePath(env, execPath) {
26
+ const exe = process.platform === 'win32' ? 'node.exe' : 'node';
27
+ for (const dir of (env.PATH || '').split(path.delimiter)) {
28
+ if (!dir) continue;
29
+ const candidate = path.join(dir, exe);
30
+ try {
31
+ fs.accessSync(candidate, fs.constants.X_OK);
32
+ if (fs.realpathSync(candidate) === fs.realpathSync(execPath)) return candidate;
33
+ } catch {
34
+ // not this one
35
+ }
36
+ }
37
+ return execPath;
38
+ }
39
+
40
+ function serverEntry({ nodePath, cliPath, env }) {
41
+ const entryEnv = {};
42
+ if (env.PATH) entryEnv.PATH = env.PATH;
43
+ for (const key of FORWARDED_ENV) if (env[key]) entryEnv[key] = env[key];
44
+ return { command: nodePath, args: [cliPath, 'mcp'], env: entryEnv };
45
+ }
46
+
47
+ function claudeDesktopConfigPath({ platform, homedir, env }) {
48
+ if (platform === 'darwin') return path.join(homedir, 'Library', 'Application Support', 'Claude', 'claude_desktop_config.json');
49
+ if (platform === 'win32') return path.join(env.APPDATA || path.join(homedir, 'AppData', 'Roaming'), 'Claude', 'claude_desktop_config.json');
50
+ return path.join(homedir, '.config', 'Claude', 'claude_desktop_config.json');
51
+ }
52
+
53
+ function writeJsonAtomic(file, value, mode) {
54
+ const tmp = path.join(path.dirname(file), `.${path.basename(file)}.${process.pid}.${Date.now()}.tmp`);
55
+ const fd = fs.openSync(tmp, 'wx', mode);
56
+ try {
57
+ fs.writeSync(fd, `${JSON.stringify(value, null, 2)}\n`);
58
+ fs.fsyncSync(fd);
59
+ } catch (e) {
60
+ fs.closeSync(fd);
61
+ fs.rmSync(tmp, { force: true });
62
+ throw e;
63
+ }
64
+ fs.closeSync(fd);
65
+ fs.renameSync(tmp, file);
66
+ }
67
+
68
+ const sameEntry = (a, b) => JSON.stringify(a) === JSON.stringify(b);
69
+
70
+ // Merges our entry into a client's JSON config, leaving every other key as it was.
71
+ function installIntoJsonConfig(file, entry, key = 'mcpServers') {
72
+ if (!fs.existsSync(path.dirname(file))) return { status: 'not-found' };
73
+ let config = {};
74
+ let mode = 0o600;
75
+ if (fs.existsSync(file)) {
76
+ const raw = fs.readFileSync(file, 'utf8');
77
+ try {
78
+ config = raw.trim() ? JSON.parse(raw) : {};
79
+ } catch {
80
+ return { status: 'invalid-json' };
81
+ }
82
+ if (!config || typeof config !== 'object' || Array.isArray(config)) return { status: 'invalid-json' };
83
+ mode = fs.statSync(file).mode & 0o777;
84
+ }
85
+ const current = config[key];
86
+ if (current !== undefined && (!current || typeof current !== 'object' || Array.isArray(current))) {
87
+ return { status: 'invalid-json' };
88
+ }
89
+ const servers = current || {};
90
+ const legacy = Object.prototype.hasOwnProperty.call(servers, LEGACY_SERVER_NAME);
91
+ if (!legacy && sameEntry(servers[SERVER_NAME], entry)) return { status: 'unchanged' };
92
+
93
+ const next = { ...config, [key]: { ...servers, [SERVER_NAME]: entry } };
94
+ delete next[key][LEGACY_SERVER_NAME];
95
+ if (fs.existsSync(file)) fs.copyFileSync(file, `${file}.bak`);
96
+ writeJsonAtomic(file, next, mode);
97
+ return { status: 'configured', removedLegacy: legacy };
98
+ }
99
+
100
+ function installIntoClaudeCode(entry, { spawn }) {
101
+ spawn('claude', ['mcp', 'remove', '--scope', 'user', SERVER_NAME], { encoding: 'utf8' });
102
+ const envFlags = Object.entries(entry.env).flatMap(([k, v]) => ['-e', `${k}=${v}`]);
103
+ const added = spawn(
104
+ 'claude',
105
+ // Name first: `-e` takes every value up to `--`, so a name after it is read as one more variable.
106
+ ['mcp', 'add', SERVER_NAME, '--scope', 'user', ...envFlags, '--', entry.command, ...entry.args],
107
+ { encoding: 'utf8' },
108
+ );
109
+ if (added.error || added.status !== 0) return { status: 'failed' };
110
+ return { status: 'configured' };
111
+ }
112
+
113
+ function installIntoCodex(entry, { spawn }) {
114
+ spawn('codex', ['mcp', 'remove', SERVER_NAME], { encoding: 'utf8' });
115
+ const envFlags = Object.entries(entry.env).flatMap(([k, v]) => ['--env', `${k}=${v}`]);
116
+ const added = spawn('codex', ['mcp', 'add', SERVER_NAME, ...envFlags, '--', entry.command, ...entry.args], { encoding: 'utf8' });
117
+ if (added.error || added.status !== 0) return { status: 'failed' };
118
+ return { status: 'configured' };
119
+ }
120
+
121
+ function vscodeUserDir({ platform, homedir, env }) {
122
+ if (platform === 'darwin') return path.join(homedir, 'Library', 'Application Support', 'Code', 'User');
123
+ if (platform === 'win32') return path.join(env.APPDATA || path.join(homedir, 'AppData', 'Roaming'), 'Code', 'User');
124
+ return path.join(homedir, '.config', 'Code', 'User');
125
+ }
126
+
127
+ function clientTable({ env, platform, homedir, spawn }) {
128
+ const hasCli = (bin) => {
129
+ const probe = spawn(bin, ['--version'], { encoding: 'utf8' });
130
+ return !probe.error && probe.status === 0;
131
+ };
132
+ const jsonClient = (client, file, key, shape = (entry) => entry) => ({
133
+ client,
134
+ found: () => fs.existsSync(path.dirname(file)),
135
+ install: (entry) => installIntoJsonConfig(file, shape(entry), key),
136
+ });
137
+ return [
138
+ jsonClient('claudeDesktop', claudeDesktopConfigPath({ platform, homedir, env })),
139
+ { client: 'claudeCode', found: () => hasCli('claude'), install: (entry) => installIntoClaudeCode(entry, { spawn }) },
140
+ jsonClient('cursor', path.join(homedir, '.cursor', 'mcp.json')),
141
+ jsonClient('geminiCli', path.join(homedir, '.gemini', 'settings.json')),
142
+ { client: 'codex', found: () => hasCli('codex'), install: (entry) => installIntoCodex(entry, { spawn }) },
143
+ // VS Code keys servers under "servers" and wants the transport spelled out.
144
+ jsonClient('vscode', path.join(vscodeUserDir({ platform, homedir, env }), 'mcp.json'), 'servers', (entry) => ({ type: 'stdio', ...entry })),
145
+ jsonClient('windsurf', path.join(homedir, '.codeium', 'windsurf', 'mcp_config.json')),
146
+ ];
147
+ }
148
+
149
+ const guard = (task) => {
150
+ try {
151
+ return task();
152
+ } catch {
153
+ return { status: 'failed' };
154
+ }
155
+ };
156
+
157
+ /** The AI apps on this machine that can take the MCP. */
158
+ function detectMcpClients({ env = process.env, platform = process.platform, homedir = os.homedir(), spawn = spawnSync } = {}) {
159
+ return clientTable({ env, platform, homedir, spawn })
160
+ .filter((c) => guard(() => c.found()) === true)
161
+ .map((c) => c.client);
162
+ }
163
+
164
+ function installMcpClients({
165
+ env = process.env,
166
+ platform = process.platform,
167
+ homedir = os.homedir(),
168
+ nodePath = stableNodePath(env, process.execPath),
169
+ cliPath = fs.realpathSync(path.join(__dirname, '..', 'bin', 'shakers.js')),
170
+ spawn = spawnSync,
171
+ } = {}) {
172
+ const entry = serverEntry({ nodePath, cliPath, env });
173
+ return {
174
+ entry,
175
+ results: clientTable({ env, platform, homedir, spawn }).map((c) => ({
176
+ client: c.client,
177
+ ...(guard(() => c.found()) === true ? guard(() => c.install(entry)) : { status: 'not-found' }),
178
+ })),
179
+ };
180
+ }
181
+
182
+ // The Talent's answer, read from the terminal: under `curl | bash` stdin is the script.
183
+ function askTerminal(question) {
184
+ let fd;
185
+ try {
186
+ fd = fs.openSync('/dev/tty', 'r+');
187
+ } catch {
188
+ return Promise.resolve(null);
189
+ }
190
+ const tty = require('tty');
191
+ const readline = require('readline');
192
+ const input = new tty.ReadStream(fd);
193
+ const output = new tty.WriteStream(fd);
194
+ const rl = readline.createInterface({ input, output });
195
+ return new Promise((resolve) => {
196
+ rl.question(question, (answer) => {
197
+ rl.close();
198
+ input.destroy();
199
+ output.destroy();
200
+ resolve(answer);
201
+ });
202
+ });
203
+ }
204
+
205
+ /**
206
+ * Asks before touching the Talent's AI apps. SHAKERS_MCP_INSTALL=1 answers yes
207
+ * without asking; with no terminal to ask on, nothing is touched.
208
+ */
209
+ async function runMcpInstall({ out = (s) => process.stdout.write(s), lang = null, ask = askTerminal, ...deps } = {}) {
210
+ const { getCatalog, detectFlowLang } = require('./i18n');
211
+ const c = getCatalog(lang || detectFlowLang()).mcpInstall;
212
+ const env = deps.env || process.env;
213
+ const found = detectMcpClients(deps);
214
+ out(`\n ${c.title}\n\n`);
215
+ if (found.length === 0) {
216
+ out(` ${c.noneFound}\n\n`);
217
+ return { ok: true, results: [] };
218
+ }
219
+ out(` ${c.found(found.map((client) => c.clients[client]).join(', '))}\n`);
220
+ let yes = env.SHAKERS_MCP_INSTALL === '1';
221
+ if (!yes) {
222
+ const answer = await ask(` ${c.confirm} `);
223
+ if (answer === null) {
224
+ out(`\n ${c.noTerminal}\n\n`);
225
+ return { ok: true, results: [], declined: true };
226
+ }
227
+ yes = !/^\s*(n|no)\s*$/i.test(answer);
228
+ }
229
+ if (!yes) {
230
+ out(`\n ${c.declined}\n\n`);
231
+ return { ok: true, results: [], declined: true };
232
+ }
233
+
234
+ const { results } = installMcpClients(deps);
235
+ out('\n');
236
+ for (const r of results.filter((x) => x.status !== 'not-found')) {
237
+ out(` ${c.clients[r.client]}: ${c.status[r.status]}\n`);
238
+ if (r.removedLegacy) out(` ${c.removedLegacy}\n`);
239
+ }
240
+ out(`\n ${c.restart}\n\n`);
241
+ if (results.some((r) => r.status === 'failed' || r.status === 'invalid-json')) process.exitCode = 1;
242
+ return { ok: true, results };
243
+ }
244
+
245
+ module.exports = { detectMcpClients, installMcpClients, stableNodePath, runMcpInstall, serverEntry, claudeDesktopConfigPath, SERVER_NAME, LEGACY_SERVER_NAME };
@@ -0,0 +1,42 @@
1
+ 'use strict';
2
+
3
+ // MCP: the talent's unread position invitations.
4
+
5
+ const LIST_INVITATIONS_SCHEMA = { type: 'object', properties: {} };
6
+
7
+ function makeInvitationsTools(deps = {}) {
8
+ const {
9
+ fetchInvitations = (opts) => require('./invitations-client').fetchInvitations({}, 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_invitations is session-gated).');
18
+ }
19
+ return session;
20
+ };
21
+
22
+ async function listInvitations() {
23
+ const session = requireLoginSession();
24
+ const res = await fetchInvitations({ hubAccessToken: session.hubAccessToken });
25
+ if (!res.ok) return { ok: false, reason: res.reason };
26
+ return { ok: true, invitations: res.items || [] };
27
+ }
28
+
29
+ return [
30
+ {
31
+ name: 'list_invitations',
32
+ description: "List the talent's unread position invitations (projects that invited them). Requires an active session. Any projectId/chatId in the result are INTERNAL handles for calling tools — never show or mention them to the talent; refer to a project by its name.",
33
+ inputSchema: LIST_INVITATIONS_SCHEMA,
34
+ handler: listInvitations,
35
+ },
36
+ ];
37
+ }
38
+
39
+ module.exports = {
40
+ makeInvitationsTools,
41
+ LIST_INVITATIONS_SCHEMA,
42
+ };
@@ -0,0 +1,39 @@
1
+ 'use strict';
2
+
3
+ // MCP: view the talent's languages. Read-only (editing is CLI-only).
4
+
5
+ const GET_LANGUAGES_SCHEMA = { type: 'object', properties: {} };
6
+
7
+ function makeLangTools(deps = {}) {
8
+ const {
9
+ fetchLanguages = (opts) => require('./lang-client').fetchLanguages({}, 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 (get_languages is session-gated).');
18
+ }
19
+ return session;
20
+ };
21
+
22
+ async function getLanguages() {
23
+ const session = requireLoginSession();
24
+ const res = await fetchLanguages({ hubAccessToken: session.hubAccessToken });
25
+ if (!res.ok) return { ok: false, reason: res.reason };
26
+ return { ok: true, languages: res.languages };
27
+ }
28
+
29
+ return [
30
+ {
31
+ name: 'get_languages',
32
+ description: "View the talent's languages and proficiency levels (NATIVE/ADVANCED/INTERMEDIATE/INTERMEDIATE_WRITTEN). Read-only. Requires an active session. Any language ids in the result are internal handles for tools only — never show or mention them to the talent; refer to a language by its name.",
33
+ inputSchema: GET_LANGUAGES_SCHEMA,
34
+ handler: getLanguages,
35
+ },
36
+ ];
37
+ }
38
+
39
+ module.exports = { makeLangTools, GET_LANGUAGES_SCHEMA };