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,388 @@
1
+ 'use strict';
2
+
3
+ const path = require('path');
4
+
5
+ const LOGIN_SCHEMA = {
6
+ type: 'object',
7
+ properties: {
8
+ method: { type: 'string', enum: ['email', 'google', 'linkedin'], description: "Optional auth method. 'email' opens a local email-and-password form on the talent's machine (they type the password there, never in the chat). 'google'/'linkedin' do not sign in here — they point you to social_signin_start/social_signin_poll (the device flow) with that provider. Omit it to open the local form." },
9
+ },
10
+ };
11
+
12
+ const LOGOUT_SCHEMA = { type: 'object', properties: {} };
13
+
14
+ const LIST_ADDABLE_SCHEMA = { type: 'object', properties: {} };
15
+
16
+ const ADD_SKILL_SCHEMA = {
17
+ type: 'object',
18
+ properties: {
19
+ skillId: { type: 'integer', description: 'The skillId to declare, from list_addable.skills[].skillId.' },
20
+ relatedPortfolioIds: {
21
+ type: 'array',
22
+ items: { type: 'string' },
23
+ 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.",
24
+ },
25
+ },
26
+ required: ['skillId'],
27
+ };
28
+
29
+ const ADD_AGENT_SCHEMA = {
30
+ type: 'object',
31
+ properties: {
32
+ agentName: { type: 'string', description: 'The agent name to declare, from list_addable.agents[].name.' },
33
+ whatItDoes: { type: 'string' },
34
+ humanDecides: { type: 'string' },
35
+ relatedPortfolios: {
36
+ type: 'array',
37
+ items: {
38
+ type: 'object',
39
+ properties: {
40
+ portfolioId: { type: 'string', description: "A portfolio/experience id (from a prior response's `portfolios[].id`, or from add_project)." },
41
+ },
42
+ required: ['portfolioId'],
43
+ },
44
+ 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.",
45
+ },
46
+ },
47
+ required: ['agentName'],
48
+ };
49
+
50
+ const ADD_PROJECT_SCHEMA = {
51
+ type: 'object',
52
+ properties: {
53
+ root: { type: 'string', description: 'Repository root. Defaults to the server working directory. Its name and remote URL seed the portfolio entry.' },
54
+ name: { type: 'string', description: 'Portfolio entry name. Defaults to the repository directory name.' },
55
+ type: { type: 'string', enum: ['PORTFOLIO', 'EXPERIENCE'], description: 'Defaults to PORTFOLIO.' },
56
+ description: { type: 'string' },
57
+ url: { type: 'string', description: 'Defaults to the git remote origin URL when available.' },
58
+ clientName: { type: 'string', description: 'Client name (optional, e.g. for freelance work).' },
59
+ 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." },
60
+ skillIds: { type: 'array', items: { type: 'integer' } },
61
+ },
62
+ };
63
+
64
+ function makeOnboardingTools(deps = {}) {
65
+ const {
66
+ getLoginEndpoint = require('./config').getLoginEndpoint,
67
+ clearAuthSession = require('./auth-session-store').clearAuthSession,
68
+ getUsageDiscoveredInventoryEndpoint = require('./config').getUsageDiscoveredInventoryEndpoint,
69
+ getSkillsDeclareEndpoint = require('./config').getSkillsDeclareEndpoint,
70
+ getAgentsDeclareEndpoint = require('./config').getAgentsDeclareEndpoint,
71
+ getAgentsListEndpoint = require('./config').getAgentsListEndpoint,
72
+ getPortfoliosDeclareEndpoint = require('./config').getPortfoliosDeclareEndpoint,
73
+ getPortfoliosListEndpoint = require('./config').getPortfoliosListEndpoint,
74
+ getPortfolioUpdateEndpoint = require('./config').getPortfolioUpdateEndpoint,
75
+ getAgentPortfoliosEndpoint = require('./config').getAgentPortfoliosEndpoint,
76
+ requestLogin = require('./auth-client').requestLogin,
77
+ saveAuthSession = require('./auth-session-store').saveAuthSession,
78
+ loadAuthSession = require('./auth-session-store').loadAuthSession,
79
+ sessionStatus = require('./auth-session-store').sessionStatus,
80
+ requestDiscoveredInventory = require('./inventory-client').requestDiscoveredInventory,
81
+ requestDiscoveredInventoryWithRefresh = require('./inventory-client').requestDiscoveredInventoryWithRefresh,
82
+ ensureFreshSession = require('./session-refresh').ensureFreshSession,
83
+ requestDeclareSkill = require('./skills-client').requestDeclareSkill,
84
+ requestDeclareAgent = require('./agents-client').requestDeclareAgent,
85
+ requestListAgents = require('./agents-client').requestListAgents,
86
+ requestRelateAgentPortfolios = require('./agents-client').requestRelateAgentPortfolios,
87
+ requestDeclarePortfolio = require('./portfolio-client').requestDeclarePortfolio,
88
+ requestListPortfolios = require('./portfolio-client').requestListPortfolios,
89
+ requestUpdatePortfolioSkills = require('./portfolio-client').requestUpdatePortfolioSkills,
90
+ getRemoteUrl = require('./portfolio-git-info').getRemoteUrl,
91
+ getCommitDateRange = require('./portfolio-git-info').getCommitDateRange,
92
+ normalizeDomain = require('./start-add-portfolio').normalizeDomain,
93
+ isValidEmail = require('./share').isValidEmail,
94
+ normalizeEmail = require('./share').normalizeEmail,
95
+ runLoopbackAuth = require('./mcp-auth-form').runLoopbackAuth,
96
+ } = deps;
97
+
98
+ const requireSession = () => {
99
+ const session = loadAuthSession();
100
+ if (sessionStatus(session) !== 'active') {
101
+ throw new Error('no active Shakers session — call the login tool (or run `shakers login`) first.');
102
+ }
103
+ return session;
104
+ };
105
+
106
+ // Mint a fresh hub JWT (email/better-auth sessions) so an expired token doesn't
107
+ // fail the inventory read — mirrors the CLI add-agent and usage-poll retry.
108
+ const refreshTokenFor = async () => {
109
+ const fresh = await ensureFreshSession(process.env);
110
+ return fresh ? { accessToken: fresh.accessToken, hubAccessToken: fresh.hubAccessToken } : null;
111
+ };
112
+
113
+ const requireEndpoint = (getter, name) => {
114
+ const endpoint = getter();
115
+ if (!endpoint) {
116
+ throw new Error(`no ${name} endpoint configured — set SHAKERS_CLI_INGEST_ENDPOINT.`);
117
+ }
118
+ return endpoint;
119
+ };
120
+
121
+ // Google/LinkedIn run through the device flow (start/poll tools); a blocking form can't surface the code.
122
+ const socialSigninPointer = (mode, provider = 'google') => ({
123
+ ok: false,
124
+ reason: 'use-device-flow',
125
+ next: 'social_signin_start',
126
+ mode,
127
+ provider,
128
+ message: `For ${provider}, call social_signin_start (provider "${provider}", mode "${mode}") to get a URL and code to show the talent, then social_signin_poll.`,
129
+ });
130
+
131
+ // No explicit `method`: the loopback form opens on its email/social choice screen.
132
+ async function loginViaForm({ method = null } = {}) {
133
+ const result = await runLoopbackAuth(method ? { mode: 'login', method } : { mode: 'login' });
134
+ if (result.viaGoogle) return socialSigninPointer('login', 'google');
135
+ if (!result.ok) return { ok: false, reason: result.reason || 'login-failed' };
136
+ return { ok: true, method: 'email', email: result.email || null };
137
+ }
138
+
139
+ async function login(args = {}) {
140
+ if (args.method === 'google' || args.method === 'linkedin') return socialSigninPointer('login', args.method);
141
+ if (args.method === 'email') return loginViaForm({ method: 'email' });
142
+ return loginViaForm();
143
+ }
144
+
145
+ async function logout() {
146
+ const wasLoggedIn = loadAuthSession() !== null;
147
+ try { clearAuthSession(); } catch { /* idempotent — nothing to clear */ }
148
+ return { ok: true, wasLoggedIn };
149
+ }
150
+
151
+ async function listAddable() {
152
+ const session = requireSession();
153
+ const endpoint = requireEndpoint(getUsageDiscoveredInventoryEndpoint, 'discovered-inventory');
154
+ const inv = await requestDiscoveredInventoryWithRefresh(
155
+ { accessToken: session.accessToken, refreshToken: refreshTokenFor },
156
+ { endpoint, request: requestDiscoveredInventory },
157
+ );
158
+ if (!inv.ok) {
159
+ return { ok: false, reason: inv.reason || 'inventory-failed' };
160
+ }
161
+ return { ok: true, skills: inv.skills, agents: inv.agents };
162
+ }
163
+
164
+ async function addSkill(args = {}) {
165
+ const session = requireSession();
166
+ const endpoint = requireEndpoint(getSkillsDeclareEndpoint, 'skills-declare');
167
+ const result = await requestDeclareSkill(
168
+ { skillId: args.skillId, accessToken: session.accessToken, hubAccessToken: session.hubAccessToken },
169
+ { endpoint },
170
+ );
171
+ const skillExisted = !result.ok && result.reason === 'skill-exists';
172
+ if (!result.ok && !skillExisted) return { ok: false, reason: result.reason || 'declare-failed' };
173
+
174
+ const listEndpoint = getPortfoliosListEndpoint();
175
+ const updateEndpoint = getPortfolioUpdateEndpoint();
176
+ if (!listEndpoint || !updateEndpoint) return skillExisted ? { ok: true, skillId: args.skillId, alreadyExisted: true } : { ok: true, skillId: args.skillId };
177
+
178
+ const listed = await requestListPortfolios(
179
+ { accessToken: session.accessToken, hubAccessToken: session.hubAccessToken },
180
+ { endpoint: listEndpoint },
181
+ );
182
+ if (!listed.ok) return { ok: true, skillId: args.skillId };
183
+ const portfolios = listed.portfolios.map((p) => ({ id: p.id, name: p.name, type: p.type }));
184
+
185
+ const relatedIds = Array.isArray(args.relatedPortfolioIds) ? args.relatedPortfolioIds : [];
186
+ if (relatedIds.length === 0) return { ok: true, skillId: args.skillId, portfolios, ...(skillExisted ? { alreadyExisted: true } : {}) };
187
+
188
+ const related = [];
189
+ const relateFailures = [];
190
+ for (const portfolioId of relatedIds) {
191
+ const portfolio = listed.portfolios.find((p) => p.id === portfolioId);
192
+ if (!portfolio) {
193
+ relateFailures.push({ portfolioId, reason: 'unknown-portfolio' });
194
+ continue;
195
+ }
196
+ const skillIds = Array.from(new Set([...(portfolio.skillIds || []), args.skillId]));
197
+ const r = await requestUpdatePortfolioSkills(
198
+ { portfolioId, skillIds, hubAccessToken: session.hubAccessToken },
199
+ { endpoint: updateEndpoint },
200
+ );
201
+ if (r.ok) related.push(portfolioId);
202
+ else relateFailures.push({ portfolioId, reason: r.reason });
203
+ }
204
+ return { ok: true, skillId: args.skillId, portfolios, related, relateFailures, ...(skillExisted ? { alreadyExisted: true } : {}) };
205
+ }
206
+
207
+ async function inventoryWhatItDoes(accessToken, agentName) {
208
+ try {
209
+ const endpoint = getUsageDiscoveredInventoryEndpoint();
210
+ if (!endpoint) return undefined;
211
+ const inv = await requestDiscoveredInventoryWithRefresh(
212
+ { accessToken, refreshToken: refreshTokenFor },
213
+ { endpoint, request: requestDiscoveredInventory },
214
+ );
215
+ if (!inv.ok || !Array.isArray(inv.agents)) return undefined;
216
+ const match = inv.agents.find((a) => a && a.name === agentName);
217
+ return match && typeof match.whatItDoes === 'string' && match.whatItDoes
218
+ ? match.whatItDoes
219
+ : undefined;
220
+ } catch {
221
+ return undefined;
222
+ }
223
+ }
224
+
225
+ async function resolveExistingAgentId(session, agentName) {
226
+ const listEndpoint = getAgentsListEndpoint();
227
+ if (!listEndpoint) return null;
228
+ const listed = await requestListAgents(
229
+ { accessToken: session.accessToken, hubAccessToken: session.hubAccessToken },
230
+ { endpoint: listEndpoint },
231
+ );
232
+ if (!listed.ok) return null;
233
+ const wanted = String(agentName || '').toLowerCase();
234
+ const match = listed.agents.find((a) => a.name && a.name.toLowerCase() === wanted);
235
+ return match && match.id ? match.id : null;
236
+ }
237
+
238
+ async function addAgent(args = {}) {
239
+ const session = requireSession();
240
+ const endpoint = requireEndpoint(getAgentsDeclareEndpoint, 'agents-declare');
241
+ if (!args.agentName) throw new Error('agentName is required (from list_addable.agents[].name).');
242
+ const whatItDoes =
243
+ typeof args.whatItDoes === 'string'
244
+ ? args.whatItDoes
245
+ : await inventoryWhatItDoes(session.accessToken, args.agentName);
246
+ const result = await requestDeclareAgent(
247
+ {
248
+ name: args.agentName,
249
+ whatItDoes,
250
+ humanDecides: typeof args.humanDecides === 'string' ? args.humanDecides : undefined,
251
+ accessToken: session.accessToken,
252
+ hubAccessToken: session.hubAccessToken,
253
+ },
254
+ { endpoint },
255
+ );
256
+
257
+ let agentId = result.ok ? (result.agentId || null) : null;
258
+ let alreadyExisted = false;
259
+ if (!result.ok) {
260
+ const existingId = await resolveExistingAgentId(session, args.agentName);
261
+ if (!existingId) return { ok: false, reason: result.reason || 'declare-failed' };
262
+ agentId = existingId;
263
+ alreadyExisted = true;
264
+ }
265
+ const base = { ok: true, agentName: args.agentName, agentId };
266
+ if (alreadyExisted) base.alreadyExisted = true;
267
+
268
+ // Echo the talent's current experiences (id/name/type) so a follow-up call
269
+ // can relate by id — same shape/purpose as add_skill's `portfolios` echo.
270
+ const listEndpoint = getPortfoliosListEndpoint();
271
+ if (!listEndpoint) return base;
272
+ const listed = await requestListPortfolios(
273
+ { accessToken: session.accessToken, hubAccessToken: session.hubAccessToken },
274
+ { endpoint: listEndpoint },
275
+ );
276
+ if (!listed.ok) return base;
277
+ const portfolios = listed.portfolios.map((p) => ({ id: p.id, name: p.name, type: p.type }));
278
+
279
+ const relatedInput = Array.isArray(args.relatedPortfolios) ? args.relatedPortfolios : [];
280
+ if (relatedInput.length === 0 || !agentId) return { ...base, portfolios };
281
+
282
+ // ADR-041 replace-set: validate every portfolioId against the talent's own
283
+ // list, then PATCH the whole set in ONE call.
284
+ const items = [];
285
+ const relateFailures = [];
286
+ for (const entry of relatedInput) {
287
+ const portfolioId = entry && entry.portfolioId;
288
+ const portfolio = listed.portfolios.find((p) => p.id === portfolioId);
289
+ if (!portfolio) {
290
+ relateFailures.push({ portfolioId, reason: 'unknown-portfolio' });
291
+ continue;
292
+ }
293
+ items.push({ portfolioId });
294
+ }
295
+ let related = [];
296
+ if (items.length > 0) {
297
+ const relateEndpoint = getAgentPortfoliosEndpoint(agentId);
298
+ const r = await requestRelateAgentPortfolios(
299
+ { agentId, items, hubAccessToken: session.hubAccessToken },
300
+ { endpoint: relateEndpoint },
301
+ );
302
+ if (r.ok) related = items.map((it) => it.portfolioId);
303
+ else for (const it of items) relateFailures.push({ portfolioId: it.portfolioId, reason: r.reason });
304
+ }
305
+ return { ...base, portfolios, related, relateFailures };
306
+ }
307
+
308
+ async function addProject(args = {}) {
309
+ const session = requireSession();
310
+ const endpoint = requireEndpoint(getPortfoliosDeclareEndpoint, 'portfolios-declare');
311
+ const root = typeof args.root === 'string' && args.root ? args.root : process.cwd();
312
+ const name = typeof args.name === 'string' && args.name ? args.name : path.basename(root);
313
+ const type = args.type === 'EXPERIENCE' ? 'EXPERIENCE' : 'PORTFOLIO';
314
+ let url = typeof args.url === 'string' && args.url ? args.url : null;
315
+ if (!url) {
316
+ try { url = getRemoteUrl(root) || null; } catch { url = null; }
317
+ }
318
+ let startDate;
319
+ let endDate;
320
+ try { ({ startDate, endDate } = getCommitDateRange(root)); } catch { startDate = null; endDate = null; }
321
+ const result = await requestDeclarePortfolio(
322
+ {
323
+ name,
324
+ type,
325
+ description: typeof args.description === 'string' ? args.description : undefined,
326
+ url: url || undefined,
327
+ clientName: typeof args.clientName === 'string' && args.clientName ? args.clientName : undefined,
328
+ clientDomain: normalizeDomain(args.clientDomain) || undefined,
329
+ startDate: startDate || undefined,
330
+ endDate: endDate || undefined,
331
+ skillIds: Array.isArray(args.skillIds) ? args.skillIds : undefined,
332
+ accessToken: session.accessToken,
333
+ hubAccessToken: session.hubAccessToken,
334
+ },
335
+ { endpoint },
336
+ );
337
+ return result.ok ? { ok: true, name, type } : { ok: false, reason: result.reason || 'declare-failed' };
338
+ }
339
+
340
+ return [
341
+ {
342
+ name: 'login',
343
+ description: "Sign the talent in to Shakers with email and password and persist the session so the add_* and certify tools can run. The talent types their password into a local window on their own machine (127.0.0.1), never in the chat. For Google or LinkedIn, use social_signin_start + social_signin_poll instead (this tool with method 'google'/'linkedin' just points you there). Returns { ok, method, email } or a structured error.",
344
+ inputSchema: LOGIN_SCHEMA,
345
+ handler: login,
346
+ },
347
+ {
348
+ name: 'logout',
349
+ description: "Sign the talent out of Shakers by clearing the persisted session. Idempotent — returns { ok: true, wasLoggedIn } whether or not a session existed.",
350
+ inputSchema: LOGOUT_SCHEMA,
351
+ handler: logout,
352
+ },
353
+ {
354
+ name: 'list_addable',
355
+ description: "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.",
356
+ inputSchema: LIST_ADDABLE_SCHEMA,
357
+ handler: listAddable,
358
+ },
359
+ {
360
+ name: 'add_skill',
361
+ description: "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.",
362
+ inputSchema: ADD_SKILL_SCHEMA,
363
+ handler: addSkill,
364
+ },
365
+ {
366
+ name: 'add_agent',
367
+ description: "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.",
368
+ inputSchema: ADD_AGENT_SCHEMA,
369
+ handler: addAgent,
370
+ },
371
+ {
372
+ name: 'add_project',
373
+ description: "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). Requires an active session. Any ids/codes in the result are internal handles for tools only — never show or mention them to the talent.",
374
+ inputSchema: ADD_PROJECT_SCHEMA,
375
+ handler: addProject,
376
+ },
377
+ ];
378
+ }
379
+
380
+ module.exports = {
381
+ makeOnboardingTools,
382
+ LOGIN_SCHEMA,
383
+ LOGOUT_SCHEMA,
384
+ LIST_ADDABLE_SCHEMA,
385
+ ADD_SKILL_SCHEMA,
386
+ ADD_AGENT_SCHEMA,
387
+ ADD_PROJECT_SCHEMA,
388
+ };
@@ -0,0 +1,46 @@
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 — call the login tool (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
+ description: "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.",
34
+ inputSchema: SCHEMA,
35
+ handler: lister('experiences', 'list_experiences'),
36
+ },
37
+ {
38
+ name: 'list_portfolios',
39
+ description: "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.",
40
+ inputSchema: SCHEMA,
41
+ handler: lister('portfolios', 'list_portfolios'),
42
+ },
43
+ ];
44
+ }
45
+
46
+ module.exports = { makePortfolioTools, SCHEMA };
@@ -0,0 +1,46 @@
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 — call the login tool (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
+ description: "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.",
37
+ inputSchema: GET_PROFILE_SCHEMA,
38
+ handler: getProfile,
39
+ },
40
+ ];
41
+ }
42
+
43
+ module.exports = {
44
+ makeProfileTools,
45
+ GET_PROFILE_SCHEMA,
46
+ };
@@ -0,0 +1,42 @@
1
+ 'use strict';
2
+
3
+ // MCP: view the talent's per-project pricing. 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 — call the login tool (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
+ description: "View the talent's per-project pricing (full-time and part-time project rates). 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.",
33
+ inputSchema: GET_RATE_SCHEMA,
34
+ handler: getRate,
35
+ },
36
+ ];
37
+ }
38
+
39
+ module.exports = {
40
+ makeRateTools,
41
+ GET_RATE_SCHEMA,
42
+ };