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,290 @@
1
+ 'use strict';
2
+
3
+ const readline = require('readline');
4
+
5
+ const JSONRPC = '2.0';
6
+ const DEFAULT_PROTOCOL_VERSION = '2025-06-18';
7
+ // A talent answering a dialog takes human time; past this the tool falls back to asking in the chat.
8
+ const ELICITATION_TIMEOUT_MS = 5 * 60 * 1000;
9
+ const ELICITATION_ACTIONS = new Set(['accept', 'decline', 'cancel']);
10
+
11
+ const ERROR = {
12
+ PARSE: -32700,
13
+ INVALID_REQUEST: -32600,
14
+ METHOD_NOT_FOUND: -32601,
15
+ INVALID_PARAMS: -32602,
16
+ INTERNAL: -32603,
17
+ };
18
+
19
+ function isResponse(msg) {
20
+ return !!msg && msg.jsonrpc === JSONRPC && msg.method === undefined && msg.id !== undefined && msg.id !== null && ('result' in msg || 'error' in msg);
21
+ }
22
+
23
+ function isNotification(msg) {
24
+ return !msg || msg.id === undefined || msg.id === null;
25
+ }
26
+
27
+ function resultResponse(id, result) {
28
+ return { jsonrpc: JSONRPC, id, result };
29
+ }
30
+
31
+ function errorResponse(id, code, message, data) {
32
+ const error = { code, message };
33
+ if (data !== undefined) error.data = data;
34
+ return { jsonrpc: JSONRPC, id: id === undefined ? null : id, error };
35
+ }
36
+
37
+ function createMcpServer({ tools = [], prompts = [], serverInfo = {}, protocolVersion, beforeCall = null, onInitialize = null, instructions = null, elicitationTimeoutMs = ELICITATION_TIMEOUT_MS } = {}) {
38
+ const registry = new Map();
39
+ for (const t of tools) {
40
+ if (!t || !t.name || typeof t.handler !== 'function') {
41
+ throw new Error('each tool needs a name and a handler function');
42
+ }
43
+ registry.set(t.name, t);
44
+ }
45
+
46
+ const promptRegistry = new Map();
47
+ for (const p of prompts) {
48
+ if (!p || !p.name || typeof p.buildMessages !== 'function') {
49
+ throw new Error('each prompt needs a name and a buildMessages function');
50
+ }
51
+ promptRegistry.set(p.name, p);
52
+ }
53
+
54
+ const advertisedVersion = protocolVersion || DEFAULT_PROTOCOL_VERSION;
55
+
56
+ let clientCapabilities = {};
57
+ let send = null;
58
+ let lastRequestId = 0;
59
+ const pending = new Map();
60
+
61
+ function request(method, params, timeoutMs) {
62
+ if (!send) return Promise.reject(new Error('no transport to the client'));
63
+ lastRequestId += 1;
64
+ const id = `shakers-${lastRequestId}`;
65
+ return new Promise((resolve, reject) => {
66
+ const timer = setTimeout(() => { pending.delete(id); reject(new Error(`${method} timed out`)); }, timeoutMs);
67
+ pending.set(id, { resolve, reject, timer });
68
+ send({ jsonrpc: JSONRPC, id, method, params });
69
+ });
70
+ }
71
+
72
+ function settle(msg) {
73
+ const waiter = pending.get(msg.id);
74
+ if (!waiter) return;
75
+ pending.delete(msg.id);
76
+ clearTimeout(waiter.timer);
77
+ if (msg.error) waiter.reject(new Error((msg.error && msg.error.message) || 'client error'));
78
+ else waiter.resolve(msg.result);
79
+ }
80
+
81
+ function rejectPending(reason) {
82
+ for (const [id, waiter] of pending) {
83
+ pending.delete(id);
84
+ clearTimeout(waiter.timer);
85
+ waiter.reject(new Error(reason));
86
+ }
87
+ }
88
+
89
+ // The spec (2025-06-18) advertises elicitation support as a presence flag: `capabilities.elicitation: {}`.
90
+ // So any advertised elicitation can ask a closed question — except an explicit url-only object
91
+ // (`{ url }` without `{ form }`), which only opens links and falls back to the chat.
92
+ function canElicitForm() {
93
+ const e = clientCapabilities.elicitation;
94
+ if (!e) return false;
95
+ if (typeof e === 'object' && e.url !== undefined && e.form === undefined) return false;
96
+ return true;
97
+ }
98
+
99
+ // MCP elicitation/create: the client's { action, content }, or null when it has no dialog or the exchange failed.
100
+ async function elicit({ message, requestedSchema }) {
101
+ if (!send || !canElicitForm()) return null;
102
+ try {
103
+ const result = await request('elicitation/create', { message, requestedSchema }, elicitationTimeoutMs);
104
+ return result && ELICITATION_ACTIONS.has(result.action) ? result : null;
105
+ } catch {
106
+ return null;
107
+ }
108
+ }
109
+
110
+ // `description` and `title` are what the talent reads in the client; `guidance` is how the model must use the tool, so it rides in the schema.
111
+ function shapeTool(t) {
112
+ const schema = t.inputSchema || { type: 'object' };
113
+ return {
114
+ name: t.name,
115
+ ...(t.title ? { title: t.title } : {}),
116
+ description: t.description || '',
117
+ inputSchema: t.guidance ? { ...schema, description: [t.guidance, schema.description].filter(Boolean).join(' ') } : schema,
118
+ };
119
+ }
120
+
121
+ function listTools() {
122
+ return { tools: [...registry.values()].map(shapeTool) };
123
+ }
124
+
125
+ function shapePrompt(p) {
126
+ const out = { name: p.name, description: p.description || '' };
127
+ if (Array.isArray(p.arguments)) out.arguments = p.arguments;
128
+ return out;
129
+ }
130
+
131
+ function listPrompts() {
132
+ return { prompts: [...promptRegistry.values()].map(shapePrompt) };
133
+ }
134
+
135
+ function getPrompt(params) {
136
+ const name = params && params.name;
137
+ const prompt = name && promptRegistry.get(name);
138
+ if (!prompt) {
139
+ const err = new Error(`unknown prompt: ${name}`);
140
+ err.rpcCode = ERROR.INVALID_PARAMS;
141
+ throw err;
142
+ }
143
+ return { description: prompt.description || '', messages: prompt.buildMessages((params && params.arguments) || {}) };
144
+ }
145
+
146
+ async function listAvailableTools() {
147
+ const out = [];
148
+ for (const t of registry.values()) {
149
+ if (typeof t.available === 'function') {
150
+ let visible = true;
151
+ try {
152
+ visible = (await t.available()) !== false;
153
+ } catch {
154
+ visible = true;
155
+ }
156
+ if (!visible) continue;
157
+ }
158
+ out.push(shapeTool(t));
159
+ }
160
+ return { tools: out };
161
+ }
162
+
163
+ async function callTool(params) {
164
+ const name = params && params.name;
165
+ const tool = name && registry.get(name);
166
+ if (!tool) {
167
+ const err = new Error(`unknown tool: ${name}`);
168
+ err.rpcCode = ERROR.INVALID_PARAMS;
169
+ throw err;
170
+ }
171
+ const args = (params && params.arguments) || {};
172
+ // Optional per-call hook (auth-agnostic): the MCP server is long-lived, so this is where the email session's hub JWT gets refreshed before each tool runs.
173
+ if (typeof beforeCall === 'function') {
174
+ try { await beforeCall(name); } catch { /* hook must never break a tool call */ }
175
+ }
176
+ try {
177
+ const value = await tool.handler(args, { elicit, elicitation: !!send && canElicitForm() });
178
+ return {
179
+ content: [{ type: 'text', text: JSON.stringify(value, null, 2) }],
180
+ isError: false,
181
+ };
182
+ } catch (e) {
183
+ return {
184
+ content: [{ type: 'text', text: `${name} tool error: ${e && e.message ? e.message : String(e)}` }],
185
+ isError: true,
186
+ };
187
+ }
188
+ }
189
+
190
+ async function handleMessage(msg) {
191
+ if (isResponse(msg)) {
192
+ settle(msg);
193
+ return null;
194
+ }
195
+ if (!msg || msg.jsonrpc !== JSONRPC || typeof msg.method !== 'string') {
196
+ if (isNotification(msg)) return null;
197
+ return errorResponse(msg && msg.id, ERROR.INVALID_REQUEST, 'invalid JSON-RPC request');
198
+ }
199
+
200
+ const { method, id } = msg;
201
+
202
+ if (method.startsWith('notifications/')) {
203
+ return null;
204
+ }
205
+
206
+ try {
207
+ switch (method) {
208
+ case 'initialize': {
209
+ const requested = msg.params && msg.params.protocolVersion;
210
+ clientCapabilities = (msg.params && msg.params.capabilities) || {};
211
+ if (typeof onInitialize === 'function') {
212
+ try { onInitialize({ clientInfo: (msg.params && msg.params.clientInfo) || {} }); } catch { /* never breaks the handshake */ }
213
+ }
214
+ // One line per connection on stderr (never stdout: it carries JSON-RPC), so the host's
215
+ // declared elicitation shape and the resulting decision are visible in the MCP log.
216
+ process.stderr.write(` [mcp] initialize elicitation=${JSON.stringify(clientCapabilities.elicitation)} -> elicit=${canElicitForm()}\n`);
217
+ const text = typeof instructions === 'function' ? instructions({ elicitation: canElicitForm() }) : instructions;
218
+ return resultResponse(id, {
219
+ protocolVersion: requested || advertisedVersion,
220
+ capabilities: {
221
+ tools: { listChanged: false },
222
+ ...(promptRegistry.size ? { prompts: { listChanged: false } } : {}),
223
+ },
224
+ serverInfo: {
225
+ name: serverInfo.name || 'shakers-mcp',
226
+ version: serverInfo.version || '0.0.0',
227
+ },
228
+ // Server-level guidance the client keeps in the model's context even
229
+ // when it loads tool schemas lazily (Claude Code defers MCP tools).
230
+ ...(typeof text === 'string' && text ? { instructions: text } : {}),
231
+ });
232
+ }
233
+ case 'ping':
234
+ return resultResponse(id, {});
235
+ case 'tools/list':
236
+ return resultResponse(id, await listAvailableTools());
237
+ case 'tools/call':
238
+ return resultResponse(id, await callTool(msg.params));
239
+ case 'prompts/list':
240
+ return resultResponse(id, listPrompts());
241
+ case 'prompts/get':
242
+ return resultResponse(id, getPrompt(msg.params));
243
+ default:
244
+ if (isNotification(msg)) return null;
245
+ return errorResponse(id, ERROR.METHOD_NOT_FOUND, `method not found: ${method}`);
246
+ }
247
+ } catch (e) {
248
+ if (isNotification(msg)) return null;
249
+ const code = (e && e.rpcCode) || ERROR.INTERNAL;
250
+ return errorResponse(id, code, e && e.message ? e.message : 'internal error');
251
+ }
252
+ }
253
+
254
+ function start({ input = process.stdin, output = process.stdout, onError } = {}) {
255
+ const rl = readline.createInterface({ input, crlfDelay: Infinity });
256
+ const write = (obj) => output.write(`${JSON.stringify(obj)}\n`);
257
+ send = write;
258
+ rl.on('close', () => {
259
+ send = null;
260
+ rejectPending('the client closed the connection');
261
+ });
262
+
263
+ rl.on('line', async (line) => {
264
+ const trimmed = line.trim();
265
+ if (!trimmed) return;
266
+ let msg;
267
+ try {
268
+ msg = JSON.parse(trimmed);
269
+ } catch {
270
+ write(errorResponse(null, ERROR.PARSE, 'parse error: invalid JSON'));
271
+ return;
272
+ }
273
+ try {
274
+ const response = await handleMessage(msg);
275
+ if (response) write(response);
276
+ } catch (e) {
277
+ if (onError) onError(e);
278
+ if (!isNotification(msg)) {
279
+ write(errorResponse(msg.id, ERROR.INTERNAL, e && e.message ? e.message : 'internal error'));
280
+ }
281
+ }
282
+ });
283
+
284
+ return rl;
285
+ }
286
+
287
+ return { handleMessage, listTools, listAvailableTools, callTool, start };
288
+ }
289
+
290
+ module.exports = { createMcpServer, ERROR, DEFAULT_PROTOCOL_VERSION };
@@ -0,0 +1,52 @@
1
+ 'use strict';
2
+
3
+ // MCP: detail of one Position/Project (Find Positions detail view).
4
+
5
+ const SHOW_PROJECT_SCHEMA = {
6
+ type: 'object',
7
+ properties: {
8
+ id: { type: 'string', description: 'Position id (UUID or legacy Mongo id) — an internal handle from a prior list; never shown to the talent.' },
9
+ },
10
+ required: ['id'],
11
+ };
12
+
13
+ function makeShowProjectTools(deps = {}) {
14
+ const {
15
+ fetchPositionDetail = (opts) => require('./show-project-client').fetchPositionDetail({}, opts),
16
+ loadAuthSession = require('./auth-session-store').loadAuthSession,
17
+ sessionStatus = require('./auth-session-store').sessionStatus,
18
+ } = deps;
19
+
20
+ const requireLoginSession = () => {
21
+ const session = loadAuthSession();
22
+ if (sessionStatus(session) !== 'active') {
23
+ throw new Error('no active Shakers session — sign in with social_signin_start (or run `shakers login`) first (show_project is session-gated).');
24
+ }
25
+ return session;
26
+ };
27
+
28
+ async function showProject(args = {}) {
29
+ const session = requireLoginSession();
30
+ const positionId = typeof args.id === 'string' ? args.id.trim() : null;
31
+ if (!positionId) return { ok: false, reason: 'no-id' };
32
+ const res = await fetchPositionDetail({ hubAccessToken: session.hubAccessToken, positionId });
33
+ if (!res.ok) return { ok: false, reason: res.reason };
34
+ return { ok: true, position: res.position, redirectUrl: res.redirectUrl };
35
+ }
36
+
37
+ return [
38
+ {
39
+ name: 'show_project',
40
+ title: 'See a project',
41
+ description: 'Shows the full details of one Shakers project.',
42
+ guidance: "Get the full detail of one Position/Project available to the talent (Shakers Find Positions detail): title, parent project, company, budget, work mode, required skills/languages, description, goals, FAQs, and the talent's saved/applied/canApply state. Requires an active session and the position id. The `id` (and any positionId/projectId in the result) is an INTERNAL handle for calling tools — NEVER show or mention it to the talent; refer to the project by its title/company.",
43
+ inputSchema: SHOW_PROJECT_SCHEMA,
44
+ handler: showProject,
45
+ },
46
+ ];
47
+ }
48
+
49
+ module.exports = {
50
+ makeShowProjectTools,
51
+ SHOW_PROJECT_SCHEMA,
52
+ };
@@ -0,0 +1,118 @@
1
+ 'use strict';
2
+
3
+ const { postJsonWithTimeout, postMultipartWithTimeout } = require('./backend-request');
4
+
5
+ // Hub calls of the MCP sign-up (ticket 39540): public sign-up + claim code, the signed-in talent's import status, and the one-time web login token.
6
+
7
+ const SIGNUP_TIMEOUT_MS = 60000;
8
+ const DEFAULT_TIMEOUT_MS = 15000;
9
+
10
+ // Hub's coded errors on the sign-up, verified against the works-ai exception filter (hub MR !2198); none says whether an account exists.
11
+ const CODE_TO_REASON = {
12
+ MISSING_SOURCE: 'missing-source',
13
+ INVALID_CV: 'invalid-cv',
14
+ INVALID_LINKEDIN_URL: 'invalid-linkedin-url',
15
+ RATE_LIMITED: 'rate-limited',
16
+ };
17
+ const IMPORT_SOURCES = ['linkedin', 'cv'];
18
+
19
+ function parseJson(raw) {
20
+ try {
21
+ return raw ? JSON.parse(raw) : null;
22
+ } catch {
23
+ return null;
24
+ }
25
+ }
26
+
27
+ function failure(res) {
28
+ const body = parseJson(res.raw);
29
+ const code = body && typeof body.code === 'string' ? body.code : null;
30
+ return { ok: false, reason: (code && CODE_TO_REASON[code]) || `http-${res.status}`, status: res.status };
31
+ }
32
+
33
+ function envelopeData(raw) {
34
+ const json = parseJson(raw);
35
+ return json && json.status === 'OK' && json.data && typeof json.data === 'object' ? json.data : null;
36
+ }
37
+
38
+ function nonEmpty(value) {
39
+ return typeof value === 'string' && value.trim() ? value.trim() : undefined;
40
+ }
41
+
42
+ async function requestMcpSignup(
43
+ { linkedinUrl, cv, email, firstName, lastName, userQuery, language } = {},
44
+ { endpoint, timeoutMs = SIGNUP_TIMEOUT_MS } = {},
45
+ ) {
46
+ if (!endpoint) return { ok: false, reason: 'no-endpoint' };
47
+ const fields = {
48
+ linkedinUrl: nonEmpty(linkedinUrl),
49
+ email: nonEmpty(email),
50
+ firstName: nonEmpty(firstName),
51
+ lastName: nonEmpty(lastName),
52
+ userQuery: nonEmpty(userQuery),
53
+ language: nonEmpty(language),
54
+ file: cv && Buffer.isBuffer(cv.data) ? cv : undefined,
55
+ };
56
+ let res;
57
+ try {
58
+ res = await postMultipartWithTimeout(endpoint, fields, timeoutMs, 'POST');
59
+ } catch (e) {
60
+ return { ok: false, reason: (e && e.kind) || 'network-error' };
61
+ }
62
+ if (res.status < 200 || res.status >= 300) return failure(res);
63
+ const data = envelopeData(res.raw);
64
+ if (!data || typeof data.claimCode !== 'string' || !data.claimCode) return { ok: false, reason: 'bad-response' };
65
+ return { ok: true, claimCode: data.claimCode, claimCodeExpiresAt: data.claimCodeExpiresAt || null };
66
+ }
67
+
68
+ // { state: running | done | failed, sources: { linkedin?, cv? } }, the shape signup_status reports.
69
+ function importStateOf(data) {
70
+ const status = data.profileImportStatus;
71
+ const reported = Array.isArray(data.sources) ? data.sources : [];
72
+ const sources = {};
73
+ for (const entry of reported) {
74
+ if (!entry || !IMPORT_SOURCES.includes(entry.source) || !['imported', 'failed'].includes(entry.status)) continue;
75
+ const failed = entry.status === 'failed';
76
+ sources[entry.source] = { state: failed ? 'failed' : 'done', ...(failed && typeof entry.code === 'string' ? { code: entry.code } : {}) };
77
+ }
78
+ if (status === 'PENDING' || status === 'RUNNING') return { state: 'running', sources: {} };
79
+ if (status === 'FAILED' || status === 'SKIPPED') return { state: 'failed', code: data.errorCode || null, sources };
80
+ if (status === 'DONE') return { state: 'done', sources };
81
+ return { state: 'none', sources: {} };
82
+ }
83
+
84
+ async function requestMyImportStatus({ hubAccessToken } = {}, { endpoint, timeoutMs = DEFAULT_TIMEOUT_MS } = {}) {
85
+ if (!endpoint) return { ok: false, reason: 'no-endpoint' };
86
+ if (!hubAccessToken) return { ok: false, reason: 'no-session' };
87
+ let res;
88
+ try {
89
+ res = await postJsonWithTimeout(endpoint, null, timeoutMs, 'GET', null, { Authorization: `Bearer ${hubAccessToken}` });
90
+ } catch (e) {
91
+ return { ok: false, reason: (e && e.kind) || 'network-error' };
92
+ }
93
+ if (res.status < 200 || res.status >= 300) return { ok: false, reason: `http-${res.status}`, status: res.status };
94
+ const data = envelopeData(res.raw);
95
+ if (!data) return { ok: false, reason: 'bad-response' };
96
+ return { ok: true, ...importStateOf(data) };
97
+ }
98
+
99
+ // better-auth reads the session from the cookie (email sign-in) or the Bearer session token (device flow).
100
+ async function requestOneTimeToken({ cookie = null, sessionToken = null } = {}, { endpoint, timeoutMs = DEFAULT_TIMEOUT_MS } = {}) {
101
+ if (!endpoint) return { ok: false, reason: 'no-endpoint' };
102
+ const headers = {};
103
+ if (cookie) headers.Cookie = cookie;
104
+ else if (sessionToken) headers.Authorization = `Bearer ${sessionToken}`;
105
+ else return { ok: false, reason: 'no-session' };
106
+ let res;
107
+ try {
108
+ res = await postJsonWithTimeout(endpoint, null, timeoutMs, 'GET', null, headers);
109
+ } catch (e) {
110
+ return { ok: false, reason: (e && e.kind) || 'network-error' };
111
+ }
112
+ if (res.status < 200 || res.status >= 300) return { ok: false, reason: `http-${res.status}`, status: res.status };
113
+ const body = parseJson(res.raw);
114
+ const token = body && (typeof body.token === 'string' ? body.token : (body.data && body.data.token));
115
+ return typeof token === 'string' && token ? { ok: true, token } : { ok: false, reason: 'bad-response' };
116
+ }
117
+
118
+ module.exports = { requestMcpSignup, requestMyImportStatus, requestOneTimeToken, CODE_TO_REASON };