shakers-cli 0.0.0-stage → 0.14.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (237) hide show
  1. package/LICENSE +59 -0
  2. package/README.md +1233 -2
  3. package/bin/add-agent.js +88 -0
  4. package/bin/add-project.js +88 -0
  5. package/bin/add-role.js +61 -0
  6. package/bin/add-skill.js +88 -0
  7. package/bin/ai-usage.js +99 -0
  8. package/bin/alma.js +99 -0
  9. package/bin/applications.js +69 -0
  10. package/bin/availability.js +70 -0
  11. package/bin/certifications.js +69 -0
  12. package/bin/certify.js +73 -0
  13. package/bin/change-role.js +61 -0
  14. package/bin/config.js +124 -0
  15. package/bin/experiences.js +69 -0
  16. package/bin/find-projects.js +89 -0
  17. package/bin/invitations.js +69 -0
  18. package/bin/lang.js +70 -0
  19. package/bin/login.js +216 -0
  20. package/bin/mcp.js +124 -0
  21. package/bin/mcpb.js +5 -0
  22. package/bin/me.js +69 -0
  23. package/bin/onboarding.js +134 -0
  24. package/bin/portfolios.js +69 -0
  25. package/bin/rate.js +70 -0
  26. package/bin/register.js +337 -0
  27. package/bin/report-html.js +134 -0
  28. package/bin/save.js +77 -0
  29. package/bin/shakers.js +363 -0
  30. package/bin/share.js +72 -0
  31. package/bin/show-project.js +72 -0
  32. package/bin/socials.js +70 -0
  33. package/bin/superadmin.js +351 -0
  34. package/bin/update-check-worker.js +70 -0
  35. package/bin/update.js +111 -0
  36. package/package.json +40 -4
  37. package/src/agent-category.js +13 -0
  38. package/src/agent-evaluation.js +177 -0
  39. package/src/agent-org-chart.js +355 -0
  40. package/src/agent-synthesis.js +132 -0
  41. package/src/agent-usage.js +118 -0
  42. package/src/agents-client.js +165 -0
  43. package/src/ai-fluency-cell.js +72 -0
  44. package/src/ai-profile-client.js +45 -0
  45. package/src/ai-profile-preview.js +154 -0
  46. package/src/ai.js +66 -0
  47. package/src/alma-client.js +123 -0
  48. package/src/alma-flow.js +32 -0
  49. package/src/alma-repo-context.js +47 -0
  50. package/src/alma-shell.js +161 -0
  51. package/src/ansi.js +62 -0
  52. package/src/applications-client.js +79 -0
  53. package/src/applications-flow.js +58 -0
  54. package/src/auth-client.js +144 -0
  55. package/src/auth-device-client.js +118 -0
  56. package/src/auth-method-picker.js +44 -0
  57. package/src/auth-methods.js +35 -0
  58. package/src/auth-session-store.js +215 -0
  59. package/src/authorship-mismatch.js +62 -0
  60. package/src/authorship.js +204 -0
  61. package/src/automations-detector.js +170 -0
  62. package/src/availability-client.js +93 -0
  63. package/src/availability-flow.js +124 -0
  64. package/src/backend-request.js +191 -0
  65. package/src/blind-approval-extract.js +48 -0
  66. package/src/brand-ansi.js +14 -0
  67. package/src/brand-svg.js +9 -0
  68. package/src/browser-tools-detector.js +21 -0
  69. package/src/build-next-level.js +56 -0
  70. package/src/certifications-client.js +89 -0
  71. package/src/certifications-flow.js +75 -0
  72. package/src/certify-dimension-client.js +163 -0
  73. package/src/certify-dimension-flow.js +135 -0
  74. package/src/certify-disclaimer.js +51 -0
  75. package/src/certify-remediation-prompt.js +23 -0
  76. package/src/cli-args.js +83 -0
  77. package/src/command-audit.js +105 -0
  78. package/src/config-dir.js +120 -0
  79. package/src/config.js +877 -0
  80. package/src/consent-flow.js +115 -0
  81. package/src/consent-skip.js +27 -0
  82. package/src/cv-file.js +62 -0
  83. package/src/cv-text.js +92 -0
  84. package/src/decisions-extract.js +43 -0
  85. package/src/detect-repos.js +145 -0
  86. package/src/detected-tools.js +15 -0
  87. package/src/detection-evidence.js +105 -0
  88. package/src/detection-noise.js +132 -0
  89. package/src/detectors.js +221 -0
  90. package/src/device-auth-store.js +100 -0
  91. package/src/device-login.js +120 -0
  92. package/src/email-verification.js +152 -0
  93. package/src/env-paths.js +30 -0
  94. package/src/find-projects-cache.js +89 -0
  95. package/src/find-projects-client.js +148 -0
  96. package/src/find-projects-flow.js +164 -0
  97. package/src/git-activity.js +124 -0
  98. package/src/git-log-prefetch.js +53 -0
  99. package/src/graph-certs.js +58 -0
  100. package/src/graph-generator.js +230 -0
  101. package/src/graph-scan.js +239 -0
  102. package/src/i18n.js +3341 -0
  103. package/src/inter-fonts.js +6 -0
  104. package/src/interactive-select.js +183 -0
  105. package/src/interview-client.js +226 -0
  106. package/src/interview-transcript.js +56 -0
  107. package/src/inventory-client.js +90 -0
  108. package/src/invitations-client.js +66 -0
  109. package/src/invitations-flow.js +50 -0
  110. package/src/lang-client.js +147 -0
  111. package/src/lang-codes.js +14 -0
  112. package/src/lang-flow.js +85 -0
  113. package/src/legal-notice.js +53 -0
  114. package/src/locale.js +72 -0
  115. package/src/markdown-ansi.js +50 -0
  116. package/src/maturity.js +111 -0
  117. package/src/mcp-ai-usage-result-tool.js +111 -0
  118. package/src/mcp-ai-usage-tool.js +401 -0
  119. package/src/mcp-alma-tools.js +95 -0
  120. package/src/mcp-applications-tools.js +44 -0
  121. package/src/mcp-auth-form.js +480 -0
  122. package/src/mcp-availability-tools.js +45 -0
  123. package/src/mcp-certifications-tools.js +44 -0
  124. package/src/mcp-certify-dimension-tools.js +66 -0
  125. package/src/mcp-choice.js +95 -0
  126. package/src/mcp-detector.js +223 -0
  127. package/src/mcp-device-login-tools.js +177 -0
  128. package/src/mcp-find-projects-tools.js +69 -0
  129. package/src/mcp-install.js +275 -0
  130. package/src/mcp-instructions.js +27 -0
  131. package/src/mcp-invitations-tools.js +44 -0
  132. package/src/mcp-lang-tools.js +41 -0
  133. package/src/mcp-onboarding-tools.js +379 -0
  134. package/src/mcp-portfolios-tools.js +50 -0
  135. package/src/mcp-profile-tools.js +48 -0
  136. package/src/mcp-prompts.js +73 -0
  137. package/src/mcp-rate-tools.js +44 -0
  138. package/src/mcp-register-tools.js +441 -0
  139. package/src/mcp-report-tools.js +105 -0
  140. package/src/mcp-role-tools.js +372 -0
  141. package/src/mcp-saved-positions-tools.js +59 -0
  142. package/src/mcp-server.js +290 -0
  143. package/src/mcp-show-project-tools.js +52 -0
  144. package/src/mcp-signup-client.js +118 -0
  145. package/src/mcp-signup-tools.js +1171 -0
  146. package/src/mcp-socials-tools.js +41 -0
  147. package/src/memory-structure-detector.js +108 -0
  148. package/src/model-call-record.js +198 -0
  149. package/src/onboarding-availability.js +26 -0
  150. package/src/onboarding-client.js +374 -0
  151. package/src/onboarding-flow.js +1162 -0
  152. package/src/onboarding-funnel.js +272 -0
  153. package/src/onboarding-livekit-client.js +102 -0
  154. package/src/onboarding-status-client.js +57 -0
  155. package/src/open-file.js +32 -0
  156. package/src/osc-link.js +16 -0
  157. package/src/portfolio-client.js +164 -0
  158. package/src/portfolio-git-info.js +61 -0
  159. package/src/portfolios-client.js +85 -0
  160. package/src/portfolios-flow.js +79 -0
  161. package/src/profile-client.js +133 -0
  162. package/src/profile-flow.js +115 -0
  163. package/src/profile-switch.js +46 -0
  164. package/src/prompt-select.js +66 -0
  165. package/src/rate-client.js +81 -0
  166. package/src/rate-flow.js +137 -0
  167. package/src/register-context.js +145 -0
  168. package/src/render-ai-fluency.js +266 -0
  169. package/src/render-certification.js +177 -0
  170. package/src/render-html.js +140 -0
  171. package/src/render-report-text.js +27 -0
  172. package/src/render-sheet.js +261 -0
  173. package/src/render-terminal.js +109 -0
  174. package/src/render-usage-report-text.js +155 -0
  175. package/src/repo-context.js +215 -0
  176. package/src/report-gating.js +49 -0
  177. package/src/report-store.js +226 -0
  178. package/src/report-theme.js +177 -0
  179. package/src/report-view-client.js +50 -0
  180. package/src/reveal-output.js +29 -0
  181. package/src/roadmap-content.js +453 -0
  182. package/src/roadmap-personalization.js +132 -0
  183. package/src/roadmap-prompt.js +154 -0
  184. package/src/roles-client.js +112 -0
  185. package/src/roles-flow.js +192 -0
  186. package/src/sanitize-network-text.js +36 -0
  187. package/src/saved-positions-client.js +71 -0
  188. package/src/saved-positions-flow.js +50 -0
  189. package/src/scan-exclusions.js +50 -0
  190. package/src/scanner.js +624 -0
  191. package/src/scope-signals.js +148 -0
  192. package/src/secret-prompt.js +72 -0
  193. package/src/session-gate.js +30 -0
  194. package/src/session-normalizers.js +174 -0
  195. package/src/session-refresh.js +51 -0
  196. package/src/session-scan.js +475 -0
  197. package/src/session-signals.js +36 -0
  198. package/src/shakers-wordmark.js +17 -0
  199. package/src/share-card.js +362 -0
  200. package/src/share-certification.js +111 -0
  201. package/src/share-consent-state.js +157 -0
  202. package/src/share-payload.js +300 -0
  203. package/src/share.js +185 -0
  204. package/src/show-project-client.js +58 -0
  205. package/src/show-project-flow.js +119 -0
  206. package/src/signup-client.js +103 -0
  207. package/src/signup-copy.js +450 -0
  208. package/src/signup-language.js +21 -0
  209. package/src/skills-client.js +102 -0
  210. package/src/socials-client.js +91 -0
  211. package/src/socials-flow.js +83 -0
  212. package/src/specificity-extract.js +41 -0
  213. package/src/start-add-agents.js +306 -0
  214. package/src/start-add-portfolio.js +388 -0
  215. package/src/start-add-skills.js +260 -0
  216. package/src/stdin-ask.js +129 -0
  217. package/src/steering-extract.js +43 -0
  218. package/src/strip-internal-comments.js +45 -0
  219. package/src/tech-detector.js +295 -0
  220. package/src/templates/report-sheet.html +572 -0
  221. package/src/terminal-agents.js +207 -0
  222. package/src/terminal-format.js +85 -0
  223. package/src/terminal-progress.js +106 -0
  224. package/src/terminal-sections.js +219 -0
  225. package/src/tier-analysis.js +126 -0
  226. package/src/tier-engine.js +114 -0
  227. package/src/tier-projection.js +117 -0
  228. package/src/transcript-turns.js +49 -0
  229. package/src/translations-client.js +84 -0
  230. package/src/typewriter.js +154 -0
  231. package/src/update-flow.js +223 -0
  232. package/src/update-notifier.js +113 -0
  233. package/src/usage-model-calls.js +119 -0
  234. package/src/usage-oneshot-commands.js +180 -0
  235. package/src/usage-repo-scope.js +162 -0
  236. package/src/usage-run-steps.js +354 -0
  237. package/src/work-streams.js +79 -0
@@ -0,0 +1,379 @@
1
+ 'use strict';
2
+
3
+ const path = require('path');
4
+ const { askConsent } = require('./mcp-choice');
5
+ const { tildify } = require('./env-paths');
6
+
7
+ const LOGOUT_SCHEMA = { type: 'object', properties: {} };
8
+
9
+ const LIST_ADDABLE_SCHEMA = { type: 'object', properties: {} };
10
+
11
+ const ADD_SKILL_SCHEMA = {
12
+ type: 'object',
13
+ properties: {
14
+ skillId: { type: 'integer', description: 'The skillId to declare, from list_addable.skills[].skillId.' },
15
+ relatedPortfolioIds: {
16
+ type: 'array',
17
+ items: { type: 'string' },
18
+ description: "Optional portfolio/experience IDs to relate this skill to (from a prior response's `portfolios[].id`, or from add_project). Omit to declare without a relation — the response always returns the talent's current `portfolios` so you know the IDs for the NEXT add_skill call.",
19
+ },
20
+ },
21
+ required: ['skillId'],
22
+ };
23
+
24
+ const ADD_AGENT_SCHEMA = {
25
+ type: 'object',
26
+ properties: {
27
+ agentName: { type: 'string', description: 'The agent name to declare, from list_addable.agents[].name.' },
28
+ whatItDoes: { type: 'string' },
29
+ humanDecides: { type: 'string' },
30
+ relatedPortfolios: {
31
+ type: 'array',
32
+ items: {
33
+ type: 'object',
34
+ properties: {
35
+ portfolioId: { type: 'string', description: "A portfolio/experience id (from a prior response's `portfolios[].id`, or from add_project)." },
36
+ },
37
+ required: ['portfolioId'],
38
+ },
39
+ description: "Optional experiences to relate this agent to (matches the web's 'Relate to an experience' step). Replace-set. Omit to declare without a relation — the response always returns the talent's current `portfolios` so you know the IDs for a follow-up call.",
40
+ },
41
+ },
42
+ required: ['agentName'],
43
+ };
44
+
45
+ const ADD_PROJECT_SCHEMA = {
46
+ type: 'object',
47
+ properties: {
48
+ root: { type: 'string', description: 'Repository root. Defaults to the server working directory. Its name and remote URL seed the portfolio entry.' },
49
+ name: { type: 'string', description: 'Portfolio entry name. Defaults to the repository directory name.' },
50
+ type: { type: 'string', enum: ['PORTFOLIO', 'EXPERIENCE'], description: 'Defaults to PORTFOLIO.' },
51
+ description: { type: 'string' },
52
+ url: { type: 'string', description: 'Defaults to the git remote origin URL when available.' },
53
+ clientName: { type: 'string', description: 'Client name (optional, e.g. for freelance work).' },
54
+ clientDomain: { type: 'string', description: "Client website or domain (optional, e.g. 'acme.com' or a full URL — normalized to a bare domain). Hub generates the client logo from it." },
55
+ skillIds: { type: 'array', items: { type: 'integer' } },
56
+ answer: { type: 'string', description: 'The option the talent picked when you printed the question this tool returned. Never set it yourself.' },
57
+ },
58
+ };
59
+
60
+ function makeOnboardingTools(deps = {}) {
61
+ const {
62
+ getLoginEndpoint = require('./config').getLoginEndpoint,
63
+ clearAuthSession = require('./auth-session-store').clearAuthSession,
64
+ getUsageDiscoveredInventoryEndpoint = require('./config').getUsageDiscoveredInventoryEndpoint,
65
+ getSkillsDeclareEndpoint = require('./config').getSkillsDeclareEndpoint,
66
+ getAgentsDeclareEndpoint = require('./config').getAgentsDeclareEndpoint,
67
+ getAgentsListEndpoint = require('./config').getAgentsListEndpoint,
68
+ getPortfoliosDeclareEndpoint = require('./config').getPortfoliosDeclareEndpoint,
69
+ getPortfoliosListEndpoint = require('./config').getPortfoliosListEndpoint,
70
+ getPortfolioUpdateEndpoint = require('./config').getPortfolioUpdateEndpoint,
71
+ getAgentPortfoliosEndpoint = require('./config').getAgentPortfoliosEndpoint,
72
+ requestLogin = require('./auth-client').requestLogin,
73
+ saveAuthSession = require('./auth-session-store').saveAuthSession,
74
+ loadAuthSession = require('./auth-session-store').loadAuthSession,
75
+ sessionStatus = require('./auth-session-store').sessionStatus,
76
+ requestDiscoveredInventory = require('./inventory-client').requestDiscoveredInventory,
77
+ requestDiscoveredInventoryWithRefresh = require('./inventory-client').requestDiscoveredInventoryWithRefresh,
78
+ ensureFreshSession = require('./session-refresh').ensureFreshSession,
79
+ requestDeclareSkill = require('./skills-client').requestDeclareSkill,
80
+ requestDeclareAgent = require('./agents-client').requestDeclareAgent,
81
+ requestListAgents = require('./agents-client').requestListAgents,
82
+ requestRelateAgentPortfolios = require('./agents-client').requestRelateAgentPortfolios,
83
+ requestDeclarePortfolio = require('./portfolio-client').requestDeclarePortfolio,
84
+ requestListPortfolios = require('./portfolio-client').requestListPortfolios,
85
+ requestUpdatePortfolioSkills = require('./portfolio-client').requestUpdatePortfolioSkills,
86
+ getRemoteUrl = require('./portfolio-git-info').getRemoteUrl,
87
+ getCommitDateRange = require('./portfolio-git-info').getCommitDateRange,
88
+ consentCopy = () => require('./signup-copy').signupCopy(require('./signup-language').signupLanguage(require('./i18n').detectFlowLang())).consent,
89
+ normalizeDomain = require('./start-add-portfolio').normalizeDomain,
90
+ isValidEmail = require('./share').isValidEmail,
91
+ normalizeEmail = require('./share').normalizeEmail,
92
+ } = deps;
93
+
94
+ const requireSession = () => {
95
+ const session = loadAuthSession();
96
+ if (sessionStatus(session) !== 'active') {
97
+ throw new Error('no active Shakers session — sign in with social_signin_start (or run `shakers login`) first.');
98
+ }
99
+ return session;
100
+ };
101
+
102
+ // Mint a fresh hub JWT (email/better-auth sessions) so an expired token doesn't
103
+ // fail the inventory read — mirrors the CLI add-agent and usage-poll retry.
104
+ const refreshTokenFor = async () => {
105
+ const fresh = await ensureFreshSession(process.env);
106
+ return fresh ? { accessToken: fresh.accessToken, hubAccessToken: fresh.hubAccessToken } : null;
107
+ };
108
+
109
+ const requireEndpoint = (getter, name) => {
110
+ const endpoint = getter();
111
+ if (!endpoint) {
112
+ throw new Error(`no ${name} endpoint configured — set SHAKERS_CLI_INGEST_ENDPOINT.`);
113
+ }
114
+ return endpoint;
115
+ };
116
+
117
+ async function logout() {
118
+ const wasLoggedIn = loadAuthSession() !== null;
119
+ try { clearAuthSession(); } catch { /* idempotent — nothing to clear */ }
120
+ return { ok: true, wasLoggedIn };
121
+ }
122
+
123
+ async function listAddable() {
124
+ const session = requireSession();
125
+ const endpoint = requireEndpoint(getUsageDiscoveredInventoryEndpoint, 'discovered-inventory');
126
+ const inv = await requestDiscoveredInventoryWithRefresh(
127
+ { accessToken: session.accessToken, refreshToken: refreshTokenFor },
128
+ { endpoint, request: requestDiscoveredInventory },
129
+ );
130
+ if (!inv.ok) {
131
+ return { ok: false, reason: inv.reason || 'inventory-failed' };
132
+ }
133
+ return { ok: true, skills: inv.skills, agents: inv.agents };
134
+ }
135
+
136
+ async function addSkill(args = {}) {
137
+ const session = requireSession();
138
+ const endpoint = requireEndpoint(getSkillsDeclareEndpoint, 'skills-declare');
139
+ const result = await requestDeclareSkill(
140
+ { skillId: args.skillId, accessToken: session.accessToken, hubAccessToken: session.hubAccessToken },
141
+ { endpoint },
142
+ );
143
+ const skillExisted = !result.ok && result.reason === 'skill-exists';
144
+ if (!result.ok && !skillExisted) return { ok: false, reason: result.reason || 'declare-failed' };
145
+
146
+ const listEndpoint = getPortfoliosListEndpoint();
147
+ const updateEndpoint = getPortfolioUpdateEndpoint();
148
+ if (!listEndpoint || !updateEndpoint) return skillExisted ? { ok: true, skillId: args.skillId, alreadyExisted: true } : { ok: true, skillId: args.skillId };
149
+
150
+ const listed = await requestListPortfolios(
151
+ { accessToken: session.accessToken, hubAccessToken: session.hubAccessToken },
152
+ { endpoint: listEndpoint },
153
+ );
154
+ if (!listed.ok) return { ok: true, skillId: args.skillId };
155
+ const portfolios = listed.portfolios.map((p) => ({ id: p.id, name: p.name, type: p.type }));
156
+
157
+ const relatedIds = Array.isArray(args.relatedPortfolioIds) ? args.relatedPortfolioIds : [];
158
+ if (relatedIds.length === 0) return { ok: true, skillId: args.skillId, portfolios, ...(skillExisted ? { alreadyExisted: true } : {}) };
159
+
160
+ const related = [];
161
+ const relateFailures = [];
162
+ for (const portfolioId of relatedIds) {
163
+ const portfolio = listed.portfolios.find((p) => p.id === portfolioId);
164
+ if (!portfolio) {
165
+ relateFailures.push({ portfolioId, reason: 'unknown-portfolio' });
166
+ continue;
167
+ }
168
+ const skillIds = Array.from(new Set([...(portfolio.skillIds || []), args.skillId]));
169
+ const r = await requestUpdatePortfolioSkills(
170
+ { portfolioId, skillIds, hubAccessToken: session.hubAccessToken },
171
+ { endpoint: updateEndpoint },
172
+ );
173
+ if (r.ok) related.push(portfolioId);
174
+ else relateFailures.push({ portfolioId, reason: r.reason });
175
+ }
176
+ return { ok: true, skillId: args.skillId, portfolios, related, relateFailures, ...(skillExisted ? { alreadyExisted: true } : {}) };
177
+ }
178
+
179
+ async function inventoryWhatItDoes(accessToken, agentName) {
180
+ try {
181
+ const endpoint = getUsageDiscoveredInventoryEndpoint();
182
+ if (!endpoint) return undefined;
183
+ const inv = await requestDiscoveredInventoryWithRefresh(
184
+ { accessToken, refreshToken: refreshTokenFor },
185
+ { endpoint, request: requestDiscoveredInventory },
186
+ );
187
+ if (!inv.ok || !Array.isArray(inv.agents)) return undefined;
188
+ const match = inv.agents.find((a) => a && a.name === agentName);
189
+ return match && typeof match.whatItDoes === 'string' && match.whatItDoes
190
+ ? match.whatItDoes
191
+ : undefined;
192
+ } catch {
193
+ return undefined;
194
+ }
195
+ }
196
+
197
+ async function resolveExistingAgentId(session, agentName) {
198
+ const listEndpoint = getAgentsListEndpoint();
199
+ if (!listEndpoint) return null;
200
+ const listed = await requestListAgents(
201
+ { accessToken: session.accessToken, hubAccessToken: session.hubAccessToken },
202
+ { endpoint: listEndpoint },
203
+ );
204
+ if (!listed.ok) return null;
205
+ const wanted = String(agentName || '').toLowerCase();
206
+ const match = listed.agents.find((a) => a.name && a.name.toLowerCase() === wanted);
207
+ return match && match.id ? match.id : null;
208
+ }
209
+
210
+ async function addAgent(args = {}) {
211
+ const session = requireSession();
212
+ const endpoint = requireEndpoint(getAgentsDeclareEndpoint, 'agents-declare');
213
+ if (!args.agentName) throw new Error('agentName is required (from list_addable.agents[].name).');
214
+ const whatItDoes =
215
+ typeof args.whatItDoes === 'string'
216
+ ? args.whatItDoes
217
+ : await inventoryWhatItDoes(session.accessToken, args.agentName);
218
+ const result = await requestDeclareAgent(
219
+ {
220
+ name: args.agentName,
221
+ whatItDoes,
222
+ humanDecides: typeof args.humanDecides === 'string' ? args.humanDecides : undefined,
223
+ accessToken: session.accessToken,
224
+ hubAccessToken: session.hubAccessToken,
225
+ },
226
+ { endpoint },
227
+ );
228
+
229
+ let agentId = result.ok ? (result.agentId || null) : null;
230
+ let alreadyExisted = false;
231
+ if (!result.ok) {
232
+ const existingId = await resolveExistingAgentId(session, args.agentName);
233
+ if (!existingId) return { ok: false, reason: result.reason || 'declare-failed' };
234
+ agentId = existingId;
235
+ alreadyExisted = true;
236
+ }
237
+ const base = { ok: true, agentName: args.agentName, agentId };
238
+ if (alreadyExisted) base.alreadyExisted = true;
239
+
240
+ // Echo the talent's current experiences (id/name/type) so a follow-up call
241
+ // can relate by id — same shape/purpose as add_skill's `portfolios` echo.
242
+ const listEndpoint = getPortfoliosListEndpoint();
243
+ if (!listEndpoint) return base;
244
+ const listed = await requestListPortfolios(
245
+ { accessToken: session.accessToken, hubAccessToken: session.hubAccessToken },
246
+ { endpoint: listEndpoint },
247
+ );
248
+ if (!listed.ok) return base;
249
+ const portfolios = listed.portfolios.map((p) => ({ id: p.id, name: p.name, type: p.type }));
250
+
251
+ const relatedInput = Array.isArray(args.relatedPortfolios) ? args.relatedPortfolios : [];
252
+ if (relatedInput.length === 0 || !agentId) return { ...base, portfolios };
253
+
254
+ // ADR-041 replace-set: validate every portfolioId against the talent's own
255
+ // list, then PATCH the whole set in ONE call.
256
+ const items = [];
257
+ const relateFailures = [];
258
+ for (const entry of relatedInput) {
259
+ const portfolioId = entry && entry.portfolioId;
260
+ const portfolio = listed.portfolios.find((p) => p.id === portfolioId);
261
+ if (!portfolio) {
262
+ relateFailures.push({ portfolioId, reason: 'unknown-portfolio' });
263
+ continue;
264
+ }
265
+ items.push({ portfolioId });
266
+ }
267
+ let related = [];
268
+ if (items.length > 0) {
269
+ const relateEndpoint = getAgentPortfoliosEndpoint(agentId);
270
+ const r = await requestRelateAgentPortfolios(
271
+ { agentId, items, hubAccessToken: session.hubAccessToken },
272
+ { endpoint: relateEndpoint },
273
+ );
274
+ if (r.ok) related = items.map((it) => it.portfolioId);
275
+ else for (const it of items) relateFailures.push({ portfolioId: it.portfolioId, reason: r.reason });
276
+ }
277
+ return { ...base, portfolios, related, relateFailures };
278
+ }
279
+
280
+ // Sends the project only after the talent says yes to exactly what goes: name, web address, dates, client.
281
+ async function addProject(args = {}, ctx = {}) {
282
+ const session = requireSession();
283
+ const endpoint = requireEndpoint(getPortfoliosDeclareEndpoint, 'portfolios-declare');
284
+ const root = path.resolve(typeof args.root === 'string' && args.root ? args.root : process.cwd());
285
+ const name = typeof args.name === 'string' && args.name ? args.name : path.basename(root);
286
+ const type = args.type === 'EXPERIENCE' ? 'EXPERIENCE' : 'PORTFOLIO';
287
+ let url = typeof args.url === 'string' && args.url ? args.url : null;
288
+ if (!url) {
289
+ try { url = getRemoteUrl(root) || null; } catch { url = null; }
290
+ }
291
+ let startDate;
292
+ let endDate;
293
+ try { ({ startDate, endDate } = getCommitDateRange(root)); } catch { startDate = null; endDate = null; }
294
+ const clientName = typeof args.clientName === 'string' && args.clientName ? args.clientName : null;
295
+ const clientDomain = normalizeDomain(args.clientDomain) || null;
296
+ const description = typeof args.description === 'string' && args.description ? args.description : null;
297
+ const sent = { name, type, url, startDate, endDate, clientName, clientDomain, description };
298
+ const c = consentCopy();
299
+ const asked = await askConsent(ctx, {
300
+ tool: 'add_project',
301
+ key: `add_project:${root}:${JSON.stringify(sent)}`,
302
+ question: c.project({ folder: tildify(root), name, url, startDate, endDate, client: [clientName, clientDomain].filter(Boolean).join(', '), description }),
303
+ yes: c.yes,
304
+ no: c.no,
305
+ answer: args.answer,
306
+ });
307
+ if (asked.reply) return asked.reply;
308
+ if (!asked.granted) return { ok: false, reason: 'consent-declined', message: 'The talent said no: nothing was sent. Do not add the project.' };
309
+ const result = await requestDeclarePortfolio(
310
+ {
311
+ name,
312
+ type,
313
+ description: description || undefined,
314
+ url: url || undefined,
315
+ clientName: clientName || undefined,
316
+ clientDomain: clientDomain || undefined,
317
+ startDate: startDate || undefined,
318
+ endDate: endDate || undefined,
319
+ skillIds: Array.isArray(args.skillIds) ? args.skillIds : undefined,
320
+ accessToken: session.accessToken,
321
+ hubAccessToken: session.hubAccessToken,
322
+ },
323
+ { endpoint },
324
+ );
325
+ return result.ok ? { ok: true, name, type } : { ok: false, reason: result.reason || 'declare-failed' };
326
+ }
327
+
328
+ return [
329
+ {
330
+ name: 'logout',
331
+ title: 'Sign out of Shakers',
332
+ description: 'Signs you out by deleting the Shakers sign-in saved on this computer.',
333
+ guidance: "Sign the talent out of Shakers by clearing the persisted session. Idempotent — returns { ok: true, wasLoggedIn } whether or not a session existed.",
334
+ inputSchema: LOGOUT_SCHEMA,
335
+ handler: logout,
336
+ },
337
+ {
338
+ name: 'list_addable',
339
+ title: 'Skills and agents you can add',
340
+ description: 'Shows the skills and AI agents found in the AI analysis you sent to Shakers, so you can pick which ones go on your profile.',
341
+ guidance: "List the skills and AI agents that Shakers discovered from the talent's submitted ai_usage report and that can be added to their Shakers profile. Use it to offer choices before add_skill / add_agent. Present skills and agents to the talent by NAME only; skillId, agent code and catalogId are internal handles for the add_* tools — never show raw IDs or codes. Requires an active session.",
342
+ inputSchema: LIST_ADDABLE_SCHEMA,
343
+ handler: listAddable,
344
+ },
345
+ {
346
+ name: 'add_skill',
347
+ title: 'Add a skill to your profile',
348
+ description: 'Adds the skill you pick to your Shakers profile.',
349
+ guidance: "Add a discovered skill to the talent's Shakers profile by skillId (taken from list_addable). skillId is an internal handle — refer to the skill by its name when talking to the talent, never show the id. Optionally pass relatedPortfolioIds to relate it to one or more of the talent's portfolio experiences (matches the web's 'Relate to an experience' step) — the response always includes `portfolios` (id/name/type) so you can ask the talent which one(s) on a later call. If the talent has no portfolios yet, the skill is still added, unrelated. Requires an active session.",
350
+ inputSchema: ADD_SKILL_SCHEMA,
351
+ handler: addSkill,
352
+ },
353
+ {
354
+ name: 'add_agent',
355
+ title: 'Add an AI agent to your profile',
356
+ description: 'Adds the AI agent you pick, with the description you agree on, to your Shakers profile.',
357
+ guidance: "Add a discovered AI agent to the talent's Shakers profile by name (from list_addable). Optionally pass whatItDoes and humanDecides (what the talent does around the agent — supervision/validation). Optionally pass relatedPortfolios ({ portfolioId }) to relate it to the talent's experiences (matches the web's 'Relate to an experience' step) — the response always includes `portfolios` (id/name/type) so you can ask which one(s) on a later call. If the talent has no experiences yet, the agent is still added, unrelated. Requires an active session.",
358
+ inputSchema: ADD_AGENT_SCHEMA,
359
+ handler: addAgent,
360
+ },
361
+ {
362
+ name: 'add_project',
363
+ title: 'Add this project to your portfolio',
364
+ description: 'Reads the name of the project folder open here, the web address of its code and the dates of its first and last change. It asks you first, naming all of them, and only then sends them to Shakers as a new portfolio project.',
365
+ guidance: "Add the current repository to the talent's Shakers portfolio as a project. Name and URL default to the repo directory and its git remote, and the start/end dates are derived from the repo's git history. Optionally pass clientName and clientDomain (the client's website/domain, e.g. 'acme.com' — Shakers fetches the client logo from it). The tool asks the talent first, naming the folder, web address, dates and client it will send (a dialog when the client has one, otherwise it returns `say` to print word for word as your whole reply); nothing is sent until they answer yes. After a `say`, call it again with the same arguments and answer set to the option they pick. Never answer for them. Requires an active session. Any ids/codes in the result are internal handles for tools only — never show or mention them to the talent.",
366
+ inputSchema: ADD_PROJECT_SCHEMA,
367
+ handler: addProject,
368
+ },
369
+ ];
370
+ }
371
+
372
+ module.exports = {
373
+ makeOnboardingTools,
374
+ LOGOUT_SCHEMA,
375
+ LIST_ADDABLE_SCHEMA,
376
+ ADD_SKILL_SCHEMA,
377
+ ADD_AGENT_SCHEMA,
378
+ ADD_PROJECT_SCHEMA,
379
+ };
@@ -0,0 +1,50 @@
1
+ 'use strict';
2
+
3
+ // MCP: list the talent's experiences (type=EXPERIENCE) and portfolio pieces
4
+ // (type=PORTFOLIO). Both read-only.
5
+
6
+ const SCHEMA = { type: 'object', properties: {} };
7
+
8
+ function makePortfolioTools(deps = {}) {
9
+ const {
10
+ fetchPortfolios = (opts) => require('./portfolios-client').fetchPortfolios({}, opts),
11
+ loadAuthSession = require('./auth-session-store').loadAuthSession,
12
+ sessionStatus = require('./auth-session-store').sessionStatus,
13
+ } = deps;
14
+
15
+ const requireLoginSession = (tool) => {
16
+ const session = loadAuthSession();
17
+ if (sessionStatus(session) !== 'active') {
18
+ throw new Error(`no active Shakers session — sign in with social_signin_start (or run \`shakers login\`) first (${tool} is session-gated).`);
19
+ }
20
+ return session;
21
+ };
22
+
23
+ const lister = (kind, tool) => async () => {
24
+ const session = requireLoginSession(tool);
25
+ const res = await fetchPortfolios({ hubAccessToken: session.hubAccessToken, kind });
26
+ if (!res.ok) return { ok: false, reason: res.reason };
27
+ return { ok: true, items: res.items };
28
+ };
29
+
30
+ return [
31
+ {
32
+ name: 'list_experiences',
33
+ title: 'Your work experience',
34
+ description: 'Shows the work experience on your Shakers profile.',
35
+ guidance: "List the talent's work experiences (company, role/title, dates, location, skills). Read-only. Requires an active session. Any ids in the result are internal handles for tools only — never show or mention them to the talent.",
36
+ inputSchema: SCHEMA,
37
+ handler: lister('experiences', 'list_experiences'),
38
+ },
39
+ {
40
+ name: 'list_portfolios',
41
+ title: 'Your portfolio',
42
+ description: 'Shows the portfolio projects on your Shakers profile.',
43
+ guidance: "List the talent's portfolio showcase pieces (name, dates, location, skills, link). Read-only. Requires an active session. Any ids in the result are internal handles for tools only — never show or mention them to the talent.",
44
+ inputSchema: SCHEMA,
45
+ handler: lister('portfolios', 'list_portfolios'),
46
+ },
47
+ ];
48
+ }
49
+
50
+ module.exports = { makePortfolioTools, SCHEMA };
@@ -0,0 +1,48 @@
1
+ 'use strict';
2
+
3
+ // MCP: the talent's summary (role + rate + "My work with AI").
4
+
5
+ const GET_PROFILE_SCHEMA = { type: 'object', properties: {} };
6
+
7
+ function makeProfileTools(deps = {}) {
8
+ const {
9
+ runProfile = require('./profile-flow').runProfile,
10
+ makeProfileDeps = require('./profile-flow').makeProfileDeps,
11
+ loadAuthSession = require('./auth-session-store').loadAuthSession,
12
+ sessionStatus = require('./auth-session-store').sessionStatus,
13
+ getCatalog = require('./i18n').getCatalog,
14
+ detectFlowLang = require('./i18n').detectFlowLang,
15
+ } = deps;
16
+
17
+ const requireLoginSession = () => {
18
+ const session = loadAuthSession();
19
+ if (sessionStatus(session) !== 'active') {
20
+ throw new Error('no active Shakers session — sign in with social_signin_start (or run `shakers login`) first (get_profile is session-gated).');
21
+ }
22
+ return session;
23
+ };
24
+
25
+ async function getProfile() {
26
+ const session = requireLoginSession();
27
+ // A silent io: the tool returns structured data, not printed lines.
28
+ const io = { section() {}, notify() {}, error() {}, warn() {}, success() {}, withProgress: (_l, task) => task() };
29
+ const catalog = getCatalog(detectFlowLang());
30
+ return runProfile({ io, rawOut: () => {}, session, catalog, opts: {}, deps: makeProfileDeps() });
31
+ }
32
+
33
+ return [
34
+ {
35
+ name: 'get_profile',
36
+ title: 'Your Shakers profile',
37
+ description: 'Shows your main role, your rate and your "My work with AI" summary on Shakers.',
38
+ guidance: "Get the talent's summary: main role, per-project rate, and the 'My work with AI' read model (setup/usage tier+level, 3x3 cell, vision, how-I-work). Requires an active session. Present it in human terms; any internal ids/codes in the result are handles for tools only — never show or mention them to the talent.",
39
+ inputSchema: GET_PROFILE_SCHEMA,
40
+ handler: getProfile,
41
+ },
42
+ ];
43
+ }
44
+
45
+ module.exports = {
46
+ makeProfileTools,
47
+ GET_PROFILE_SCHEMA,
48
+ };
@@ -0,0 +1,73 @@
1
+ 'use strict';
2
+
3
+ // MCP prompts = in-client slash-commands. Each is a thin wrapper that injects a user turn
4
+ // telling the assistant which Shakers tool(s) to use — the logic stays in the tools, so
5
+ // there is no duplication. `buildMessages(args)` returns the prompts/get message list.
6
+
7
+ function userText(text) {
8
+ return [{ role: 'user', content: { type: 'text', text } }];
9
+ }
10
+
11
+ // Only reference tools that exist in the FULL server; `available(toolNames)` hides a prompt
12
+ // when its tool isn't registered (e.g. the --local scan-only server).
13
+ const PROMPTS = [
14
+ {
15
+ name: 'ai_usage',
16
+ description: 'Evaluate and showcase how you work with AI (scans this machine, builds your "My work with AI").',
17
+ tools: ['ai_usage'],
18
+ buildMessages: () => userText(
19
+ 'Evaluate how I work with AI for my Shakers profile. First ask me for consent and the email to attribute it to, then call the `ai_usage` tool and show me the resulting report verbatim. Never show internal ids/codes.',
20
+ ),
21
+ },
22
+ {
23
+ name: 'onboarding',
24
+ description: 'Sign up on Shakers from this chat: your CV and LinkedIn, a password or Google, and your profile filled in for you.',
25
+ tools: ['signup_start'],
26
+ buildMessages: (_args, lang) => userText(
27
+ `Sign me up on Shakers following the sign-up flow in your Shakers instructions. Start with \`signup_start\` (language: the one I write in; "${lang}" if I have not written anything yet) and show me its texts word for word. Ask me only the questions the tools return, with their options, and nothing you can infer. Never narrate internal steps or tool state to me, and speak only in my language.`,
28
+ ),
29
+ },
30
+ {
31
+ name: 'find_projects',
32
+ description: 'Find open Shakers projects/positions matched to you.',
33
+ tools: ['find_projects'],
34
+ buildMessages: () => userText(
35
+ 'Find open Shakers positions that match me: call `find_projects` and summarize them by title/company (never show internal ids). Offer `save_project` / `show_project` for ones I like.',
36
+ ),
37
+ },
38
+ {
39
+ name: 'certify',
40
+ description: 'See which role dimensions you can certify and start a certification.',
41
+ tools: ['list_certifiable_dimensions'],
42
+ buildMessages: () => userText(
43
+ 'Help me certify on Shakers: call `list_certifiable_dimensions`, present the dimensions I can certify by name, and guide me through certifying the one I pick.',
44
+ ),
45
+ },
46
+ {
47
+ name: 'ask',
48
+ description: 'Ask Alma, the Shakers AI assistant, a question.',
49
+ tools: ['alma_chat'],
50
+ arguments: [{ name: 'message', description: 'What to ask Alma.', required: false }],
51
+ buildMessages: (args = {}) => userText(
52
+ `Ask Alma (the Shakers assistant) with \`alma_chat\` and relay her answer as-is. My question: ${typeof args.message === 'string' && args.message ? args.message : '(ask me what I want to know first)'}`,
53
+ ),
54
+ },
55
+ {
56
+ name: 'my_profile',
57
+ description: 'Show your Shakers profile (role, rate, availability, "My work with AI").',
58
+ tools: ['get_profile'],
59
+ buildMessages: () => userText(
60
+ 'Show my Shakers profile: call `get_profile` (and `get_rate` / `get_availability` if useful) and present it in plain terms. Any ids/codes are internal — never show them.',
61
+ ),
62
+ },
63
+ ];
64
+
65
+ function makePrompts(toolNames = null, lang = 'en') {
66
+ const available = toolNames instanceof Set ? toolNames : null;
67
+ // Keep a prompt only if at least one of its tools is present in this server.
68
+ return PROMPTS
69
+ .filter((p) => !available || p.tools.some((t) => available.has(t)))
70
+ .map((p) => ({ ...p, buildMessages: (args) => p.buildMessages(args, lang) }));
71
+ }
72
+
73
+ module.exports = { makePrompts, PROMPTS };
@@ -0,0 +1,44 @@
1
+ 'use strict';
2
+
3
+ // MCP: view the talent's rates (hourly and annual, as the web shows them). Read-only.
4
+
5
+ const GET_RATE_SCHEMA = { type: 'object', properties: {} };
6
+
7
+ function makeRateTools(deps = {}) {
8
+ const {
9
+ fetchPricingRate = (opts) => require('./rate-client').fetchPricingRate({}, 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 — sign in with social_signin_start (or run `shakers login`) first (get_rate is session-gated).');
18
+ }
19
+ return session;
20
+ };
21
+
22
+ async function getRate() {
23
+ const session = requireLoginSession();
24
+ const res = await fetchPricingRate({ hubAccessToken: session.hubAccessToken });
25
+ if (!res.ok) return { ok: false, reason: res.reason };
26
+ return { ok: true, pricing: res.pricing };
27
+ }
28
+
29
+ return [
30
+ {
31
+ name: 'get_rate',
32
+ title: 'Your rates',
33
+ description: 'Shows your hourly rate and your yearly target on Shakers.',
34
+ guidance: "View the talent's rates as the web shows them: partTimePrice is the hourly rate (per hour) and fullTimePrice the annual target (per year); a variant not selected is not offered. Read-only. Requires an active session. Show only the human-facing amounts/currency; any internal ids in the result are handles for tools only — never show or mention them to the talent.",
35
+ inputSchema: GET_RATE_SCHEMA,
36
+ handler: getRate,
37
+ },
38
+ ];
39
+ }
40
+
41
+ module.exports = {
42
+ makeRateTools,
43
+ GET_RATE_SCHEMA,
44
+ };