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,517 @@
1
+ 'use strict';
2
+
3
+ const {
4
+ FREELANCE_TYPES, WORK_SITUATIONS, EMPLOYMENT_PARTICIPATIONS, FREELANCE_OPINIONS,
5
+ CHANGE_MOTIVATORS, MONTHLY_HOURS, LANGUAGE_CODES, LANGUAGE_LEVELS,
6
+ } = require('./onboarding-flow');
7
+ const { getCatalog } = require('./i18n');
8
+ const { TranscriptBuffer } = require('./interview-transcript');
9
+
10
+ const MONTHLY_HOURS_QUOTED = MONTHLY_HOURS.map((h) => `'${h}'`).join(', ');
11
+ const MONTHLY_HOURS_SLASHED = MONTHLY_HOURS.join('/');
12
+
13
+ // "Friendly label (CODE)" list from an i18n label map: the talent sees the label, the model passes the canonical code.
14
+ function labelledOptions(codes, labels) {
15
+ return codes.map((code) => `${labels[code] || code} (${code})`).join(', ');
16
+ }
17
+
18
+ function buildSchemas(lang) {
19
+ const L = getCatalog(lang).onboarding;
20
+ const showLabel = 'Show the talent the label, pass the code';
21
+
22
+ const ACCOUNT_FIELDS = {
23
+ name: { type: 'string', description: "The talent's first name, as they give it in the chat." },
24
+ lastName: { type: 'string', description: "The talent's last name, as they give it in the chat." },
25
+ email: { type: 'string', description: "The talent's own email address, used as their account login. They give it in the chat." },
26
+ preferredLanguage: { type: 'string', enum: ['ES', 'EN', 'IT', 'PT', 'FR'], description: 'Preferred language as an UPPERCASE ISO code. Optional — defaults to the session language.' },
27
+ newsletterConsent: { type: 'boolean', description: 'Whether the talent agrees to receive the newsletter. Optional, defaults to false.' },
28
+ freelanceType: { type: 'string', enum: FREELANCE_TYPES, description: `How the talent is set up to work. ${showLabel}: ${labelledOptions(FREELANCE_TYPES, L.freelanceTypeLabels)}. Distinct from workSituation below (that is the CURRENT freelance/employed/etc situation).` },
29
+ };
30
+
31
+ const WORK_SITUATION_SCHEMA = {
32
+ type: 'object',
33
+ description: "The talent's current work situation (ADR-036), asked once after account creation — the same question the web's registration flow asks. NOT the same thing as freelanceType above.",
34
+ properties: {
35
+ situation: { type: 'string', enum: WORK_SITUATIONS, description: `Current work situation. ${showLabel}: ${labelledOptions(WORK_SITUATIONS, L.workSituationLabels)}. 'EMPLOYED' requires participation below.` },
36
+ participation: { type: 'string', enum: EMPLOYMENT_PARTICIPATIONS, description: `Only when situation is 'EMPLOYED'. ${showLabel}: ${labelledOptions(EMPLOYMENT_PARTICIPATIONS, L.employmentParticipationLabels)}.` },
37
+ opinion: { type: 'string', enum: FREELANCE_OPINIONS, description: `The talent's opinion on freelancing. ${showLabel}: ${labelledOptions(FREELANCE_OPINIONS, L.freelanceOpinionLabels)}. Required unless situation is 'FREELANCE' (implied ALREADY_FREELANCE).` },
38
+ changeMotivators: { type: 'string', enum: CHANGE_MOTIVATORS, description: `What motivates the talent about freelancing/Shakers. ${showLabel}: ${labelledOptions(CHANGE_MOTIVATORS, L.changeMotivatorLabels)}. Required only when the talent is or is considering freelance (situation 'FREELANCE', or opinion 'WAS_FREELANCE_BEFORE'/'OPEN_TO_FREELANCE'); omit when opinion is 'NOT_INTERESTED'.` },
39
+ },
40
+ required: ['situation'],
41
+ };
42
+
43
+ const LANGUAGES_SCHEMA = {
44
+ type: 'array',
45
+ description: `Languages the talent speaks (ADR-036). ALWAYS ask this — it must not be skipped. ${showLabel}. Language options: ${labelledOptions(LANGUAGE_CODES, L.languageNames)}; and for each language a level: ${labelledOptions(LANGUAGE_LEVELS, L.languageLevelLabels)}. Bulk upsert — send the talent’s FULL language list. Omit only if the talent explicitly declines.`,
46
+ items: {
47
+ type: 'object',
48
+ properties: {
49
+ language: { type: 'string', enum: LANGUAGE_CODES, description: `Language code. ${showLabel}: ${labelledOptions(LANGUAGE_CODES, L.languageNames)}.` },
50
+ level: { type: 'string', enum: LANGUAGE_LEVELS, description: `Language level. ${showLabel}: ${labelledOptions(LANGUAGE_LEVELS, L.languageLevelLabels)}.` },
51
+ },
52
+ required: ['language', 'level'],
53
+ },
54
+ };
55
+
56
+ const AVAILABILITY_SCHEMA = {
57
+ type: 'object',
58
+ description: `Availability AND location (ADR-028). ALWAYS ask this — it must not be skipped. There is NO separate location field: the talent's LOCATION (country, timezone, region) lives here. Offer the options: available (yes/no); monthlyHours must be one of ${MONTHLY_HOURS_QUOTED}; onlyRemote (yes/no — accepts only remote work); longFullTimeProjects (yes/no — open to long full-time projects). For location, ask the talent their country and timezone (region is optional). All fields optional/nullable, but collect what you can. monthlyHours is a STRICT enum string (not an int). city is not collected here.`,
59
+ properties: {
60
+ available: { type: 'boolean', description: 'Whether the talent is currently available (yes/no).' },
61
+ monthlyHours: { type: 'string', enum: MONTHLY_HOURS, description: `Monthly hours the talent can work, exactly one of: ${MONTHLY_HOURS_QUOTED} (send '> 160' verbatim, with the space).` },
62
+ onlyRemote: { type: 'boolean', description: 'Whether the talent accepts only remote work (yes/no).' },
63
+ country: { type: 'string', description: 'LOCATION — ISO-2 country code, 2 letters (e.g. ES, US, GB).' },
64
+ timezone: { type: 'string', description: 'LOCATION — IANA timezone (e.g. Europe/Madrid, America/New_York). Max 100 chars.' },
65
+ subdivision: { type: 'string', description: 'LOCATION — optional region/state code (e.g. ES-CT). Max 10 chars.' },
66
+ longFullTimeProjects: { type: 'boolean', description: 'Whether the talent is open to long full-time projects (yes/no).' },
67
+ },
68
+ };
69
+
70
+ const REGISTER_SCHEMA = {
71
+ type: 'object',
72
+ properties: {
73
+ ...ACCOUNT_FIELDS,
74
+ profile: PROFILE_SCHEMA,
75
+ workSituation: WORK_SITUATION_SCHEMA,
76
+ pricing: PRICING_SCHEMA,
77
+ availability: AVAILABILITY_SCHEMA,
78
+ languages: LANGUAGES_SCHEMA,
79
+ phone: PHONE_SCHEMA,
80
+ },
81
+ required: ['name', 'lastName', 'email', 'freelanceType', 'profile', 'workSituation', 'pricing'],
82
+ };
83
+
84
+ return { ACCOUNT_FIELDS, WORK_SITUATION_SCHEMA, LANGUAGES_SCHEMA, AVAILABILITY_SCHEMA, REGISTER_SCHEMA };
85
+ }
86
+
87
+ // The register tool's own long description, per language: the batched
88
+ // collection guide with friendly labels inline for every choice field.
89
+ function buildRegisterDescription(lang) {
90
+ const L = getCatalog(lang).onboarding;
91
+ return "The talent registers THEMSELVES on Shakers (self-service; no prior session needed). Your job is to relay the non-secret profile details the talent gives you in the chat as arguments. Credentials never touch the chat or any tool argument: the tool opens a sign-up window on the talent's own machine (127.0.0.1) where they either Continue with Google (no password at all) or type an email and password directly into that local window.\n\nCOLLECT THE DETAILS IN A FEW GROUPED BATCHES, not one field at a time, and for every CHOICE field PRESENT THE VALID OPTIONS so the talent just picks one. For every choice field SHOW THE TALENT THE FRIENDLY LABEL, never the raw code (e.g. show 'Nativo', not 'NATIVE'), and pass the code as the value. Suggested batches:\n1. Identity — name, lastName, email (optional: preferredLanguage, newsletterConsent).\n2. Profile — `profile.linkedinUrl` (required source; optional GitHub/website/CV path).\n3. Work — freelanceType (offer: "
92
+ + labelledOptions(FREELANCE_TYPES, L.freelanceTypeLabels)
93
+ + ") and `workSituation` (offer situation: "
94
+ + labelledOptions(WORK_SITUATIONS, L.workSituationLabels)
95
+ + "; participation "
96
+ + labelledOptions(EMPLOYMENT_PARTICIPATIONS, L.employmentParticipationLabels)
97
+ + " only when EMPLOYED; opinion "
98
+ + labelledOptions(FREELANCE_OPINIONS, L.freelanceOpinionLabels)
99
+ + " unless FREELANCE; changeMotivators "
100
+ + labelledOptions(CHANGE_MOTIVATORS, L.changeMotivatorLabels)
101
+ + " when the talent is or is considering freelance).\n4. Pricing — per-PROJECT price; currency USD, EUR or GBP.\n5. Availability + languages — `availability` (offer monthlyHours "
102
+ + MONTHLY_HOURS_SLASHED
103
+ + ", onlyRemote yes/no, longFullTimeProjects yes/no, and LOCATION: country as ISO-2 e.g. ES, timezone as IANA e.g. Europe/Madrid) and `languages` (offer "
104
+ + labelledOptions(LANGUAGE_CODES, L.languageNames)
105
+ + ", each with a level "
106
+ + labelledOptions(LANGUAGE_LEVELS, L.languageLevelLabels)
107
+ + ").\n6. Contact — `phone` (telephoneCode e.g. +34 and telephoneNumber).\n\nALWAYS ask for availability (including location), languages and phone — they are expected, not optional extras; only omit one if the talent explicitly declines. There is NO separate location field: location (country/timezone/region) is part of `availability`.\n\nOnce the window completes the account exists and the session is set; the tool then starts the background profile import, saves the work situation, per-PROJECT pricing (ADR-024), and any availability/languages/phone provided. If the account already exists, the tool returns { accountAlreadyExists: true } — switch to the login tool (method 'google') instead. Returns { importJobId, next, disclaimers, profileUrl }. `next` is 'ai_usage' when the talent has no prior AI-usage evidence (offer step 4, the ai_usage tool) or 'onboarding_interview_start' when they already do (step 4 is skipped). When you run ai_usage as this step, pass the returned `usageRepoScope` ({ mode: 'all' }) — registration always measures the whole machine, never a single repo. Before running ai_usage OR onboarding_interview_start, show the talent the matching `disclaimers` block, its two parts (info accessed, goal+duration) separately. Poll register_status with the importJobId to track the import. Always finish any path by calling onboarding_finish (whether or not the talent takes steps 4 and 5) — that is what marks the onboarding complete and moves the talent's registration level.";
108
+ }
109
+
110
+ const PHONE_SCHEMA = {
111
+ type: 'object',
112
+ description: "Phone (ADR-036). ALWAYS ask this — it must not be skipped. Two parts: telephoneCode = the international dialling code (e.g. +34, +1, +44), telephoneNumber = the local number. Omit only if the talent explicitly declines to share it.",
113
+ properties: {
114
+ telephoneCode: { type: 'string', description: "International dialling code, e.g. '+34', '+1', '+44'. Max 10 chars." },
115
+ telephoneNumber: { type: 'string', description: 'Local phone number. Max 20 chars.' },
116
+ },
117
+ required: ['telephoneCode', 'telephoneNumber'],
118
+ };
119
+
120
+ const PROFILE_SCHEMA = {
121
+ type: 'object',
122
+ properties: {
123
+ linkedinUrl: { type: 'string', description: "The talent's LinkedIn URL. Required — it is the mandatory profile source." },
124
+ cvPath: { type: 'string', description: 'Absolute path to a CV PDF on the machine (optional).' },
125
+ githubUrl: { type: 'string', description: 'GitHub URL (optional).' },
126
+ websiteUrl: { type: 'string', description: 'Personal website URL (optional).' },
127
+ },
128
+ required: ['linkedinUrl'],
129
+ };
130
+
131
+ const PRICING_SCHEMA = {
132
+ type: 'object',
133
+ description: 'Per-PROJECT pricing (ADR-024). NOT hourly or annual. Amounts are the whole project price as a plain number, no thousands separators, at most 2 decimals, 0..999999.99. Currency is USD, EUR or GBP. The full-time (standard) price is the primary one and should always be captured, exactly as the CLI asks it; part-time is an additional modality. At least one variant must be selected.',
134
+ properties: {
135
+ fullTimeSelected: { type: 'boolean', description: 'Whether the talent offers full-time projects. This standard/full-time price is the primary one — capture it by default.' },
136
+ fullTimeAmount: { type: 'number', description: 'Full-time (standard) per-project price, plain number without thousands separators (e.g. 60000).' },
137
+ fullTimeCurrency: { type: 'string', enum: ['USD', 'EUR', 'GBP'], description: 'Currency for the full-time price.' },
138
+ partTimeSelected: { type: 'boolean', description: 'Whether the talent also offers part-time projects (additional modality).' },
139
+ partTimeAmount: { type: 'number', description: 'Part-time per-project price, plain number without thousands separators (e.g. 50).' },
140
+ partTimeCurrency: { type: 'string', enum: ['USD', 'EUR', 'GBP'], description: 'Currency for the part-time price.' },
141
+ },
142
+ };
143
+
144
+ // Default (English) static exports for callers/tests that read the module-level
145
+ // schemas directly. The live tools rebuild these per session language.
146
+ const { ACCOUNT_FIELDS, WORK_SITUATION_SCHEMA, LANGUAGES_SCHEMA, AVAILABILITY_SCHEMA, REGISTER_SCHEMA } = buildSchemas('en');
147
+
148
+ const STATUS_SCHEMA = {
149
+ type: 'object',
150
+ properties: { jobId: { type: 'string', description: 'The importJobId returned by register — an internal handle for polling; never shown or mentioned to the talent.' } },
151
+ required: ['jobId'],
152
+ };
153
+
154
+ const INTERVIEW_START_SCHEMA = {
155
+ type: 'object',
156
+ properties: {
157
+ candidateId: { type: 'string', description: "Talent UUID (the JWT sub). Optional — defaults to the signed-in talent's UUID derived from the session. An email is NOT accepted by the server." },
158
+ language: { type: 'string', enum: ['es', 'en'] },
159
+ disclaimerAcknowledged: {
160
+ type: 'boolean',
161
+ description: 'Must be true. Set it only after showing the talent BOTH interview disclaimers separately (info accessed AND goal+duration) from the register response, and getting their agreement.',
162
+ },
163
+ confirmRepeat: {
164
+ type: 'boolean',
165
+ description: 'Only needed when the talent has ALREADY completed onboarding. The tool then refuses with an already-completed notice unless you set this true after the talent explicitly confirms they want to repeat the interview.',
166
+ },
167
+ },
168
+ required: ['disclaimerAcknowledged'],
169
+ };
170
+
171
+ const INTERVIEW_TURN_SCHEMA = {
172
+ type: 'object',
173
+ properties: {
174
+ interviewId: { type: 'string', description: 'Internal handle for this interview — for calling the interview tools ONLY; never shown or mentioned to the talent.' },
175
+ message: { type: 'string', description: "The talent's REAL, VERBATIM answer to the current question. NEVER invent, infer, complete, translate, or answer on the talent's behalf. If the talent has not answered yet, do NOT call this — wait for their actual words." },
176
+ },
177
+ required: ['interviewId', 'message'],
178
+ };
179
+
180
+ const INTERVIEW_COMPLETE_SCHEMA = {
181
+ type: 'object',
182
+ properties: { interviewId: { type: 'string' } },
183
+ required: ['interviewId'],
184
+ };
185
+
186
+ const FINISH_SCHEMA = { type: 'object', properties: {} };
187
+
188
+ function makeRegisterTools(deps = {}) {
189
+ const flow = require('./onboarding-flow');
190
+ const resolved = flow.makeDeps(deps.flowDeps || {});
191
+ const lang = deps.lang || 'en';
192
+ const schemas = buildSchemas(lang);
193
+ const registerDescription = buildRegisterDescription(lang);
194
+ // Same confirmation copy the CLI prints ("Price saved.", …), reused verbatim.
195
+ const catalog = require('./i18n').getCatalog(lang).onboarding;
196
+ const runLoopbackAuth = deps.runLoopbackAuth || require('./mcp-auth-form').runLoopbackAuth;
197
+ const checkCompleted = deps.checkOnboardingCompleted || require('./onboarding-availability').checkOnboardingCompleted;
198
+
199
+ // The LiveKit room is a live WebRTC connection that must survive between the
200
+ // separate start/turn/complete JSON-RPC calls of this long-lived stdio server.
201
+ const interviewSessions = new Map();
202
+
203
+ function connectHint(kind) {
204
+ if (kind === 'livekit-not-installed') return catalog.livekitMissing;
205
+ if (kind === 'livekit-no-text-streams') return catalog.livekitOld;
206
+ return catalog.livekitConnectError;
207
+ }
208
+
209
+ async function teardownInterview(interviewId, { persist = false } = {}) {
210
+ const sess = interviewSessions.get(interviewId);
211
+ if (!sess) return { ok: true };
212
+ interviewSessions.delete(interviewId);
213
+ let saved = { ok: true };
214
+ if (persist && sess.buffer.length > 0) {
215
+ const durationSeconds = Math.max(0, Math.floor((Date.now() - sess.connectAt) / 1000));
216
+ saved = await flow.completeLivekitOnboarding(resolved, {
217
+ interviewId,
218
+ durationSeconds,
219
+ transcripts: sess.buffer.toTranscripts(),
220
+ });
221
+ }
222
+ try { await sess.client.disconnect(); } catch { /* room already gone */ }
223
+ return saved;
224
+ }
225
+
226
+ async function register(args = {}) {
227
+ const registerFields = {
228
+ name: args.name,
229
+ lastName: args.lastName,
230
+ email: args.email,
231
+ preferredLanguage: args.preferredLanguage,
232
+ newsletterConsent: args.newsletterConsent === true,
233
+ freelanceType: args.freelanceType,
234
+ };
235
+ const check = flow.validateRegisterFields(registerFields);
236
+ if (!check.ok) return { ok: false, step: 'signup', reason: check.reason };
237
+
238
+ const created = await runLoopbackAuth({ mode: 'register', deps: resolved, lang, registerFields });
239
+ if (created.viaGoogle) {
240
+ // Social registration is the device flow (start/poll tools), not this single blocking call.
241
+ return {
242
+ ok: false,
243
+ step: 'signup',
244
+ reason: 'use-device-flow',
245
+ next: 'social_signin_start',
246
+ mode: 'register',
247
+ message: `To register with a social provider, call social_signin_start (provider "google" or "linkedin", mode "register", freelanceType "${registerFields.freelanceType || ''}") to get a URL and code to show the talent, then social_signin_poll. Once it returns authorized, run the profile steps (import, work situation, pricing).`,
248
+ };
249
+ }
250
+ if (!created.ok) {
251
+ return { ok: false, step: 'signup', reason: created.reason };
252
+ }
253
+
254
+ const profile = args.profile || {};
255
+ const importRes = await flow.startProfileImport(resolved, {
256
+ linkedinUrl: profile.linkedinUrl,
257
+ cvPath: profile.cvPath,
258
+ githubUrl: profile.githubUrl,
259
+ websiteUrl: profile.websiteUrl,
260
+ language: lang,
261
+ });
262
+ const importOutcome = importRes.ok
263
+ ? { ok: true, jobId: importRes.jobId, state: importRes.state }
264
+ : { ok: false, reason: importRes.reason, warning: 'Profile import is unavailable right now; registration continues and the import can be retried later.' };
265
+
266
+ const workSituationRes = await flow.saveWorkSituation(resolved, args.workSituation || {});
267
+ if (!workSituationRes.ok) return { ok: false, step: 'work-situation', reason: workSituationRes.reason };
268
+
269
+ const priceRes = await flow.savePricing(resolved, args.pricing || {});
270
+ if (!priceRes.ok) return { ok: false, step: 'pricing', reason: priceRes.reason };
271
+
272
+ let availabilityOutcome = { skipped: true };
273
+ if (args.availability && typeof args.availability === 'object') {
274
+ const availRes = await flow.saveAvailability(resolved, args.availability);
275
+ availabilityOutcome = availRes.ok
276
+ ? { ok: true }
277
+ : { ok: false, reason: availRes.reason, warning: 'Availability could not be saved; registration continues and it can be set later.' };
278
+ }
279
+
280
+ let languagesOutcome = { skipped: true };
281
+ if (Array.isArray(args.languages) && args.languages.length > 0) {
282
+ const langRes = await flow.saveLanguages(resolved, args.languages);
283
+ languagesOutcome = langRes.ok
284
+ ? { ok: true }
285
+ : { ok: false, reason: langRes.reason, unresolved: langRes.unresolved, warning: 'Languages could not be saved; registration continues and they can be added later.' };
286
+ }
287
+
288
+ let phoneOutcome = { skipped: true };
289
+ if (args.phone && typeof args.phone === 'object') {
290
+ const phoneRes = await flow.savePhone(resolved, args.phone);
291
+ phoneOutcome = phoneRes.ok
292
+ ? { ok: true }
293
+ : { ok: false, reason: phoneRes.reason, warning: 'Phone could not be saved; registration continues and it can be set later.' };
294
+ }
295
+
296
+ const prior = await flow.detectPriorUsage(resolved);
297
+ const hasEvidence = prior.ok ? prior.hasEvidence : null;
298
+
299
+ // Human-readable confirmations, identical to the CLI step notices.
300
+ const confirmations = [catalog.accountReady, catalog.professionalSaved, catalog.pricingSaved];
301
+ if (importOutcome.ok) confirmations.push(catalog.importDone);
302
+ if (availabilityOutcome.ok) confirmations.push(catalog.availabilitySaved);
303
+ if (languagesOutcome.ok) confirmations.push(catalog.languagesSaved);
304
+ if (phoneOutcome.ok) confirmations.push(catalog.phoneSaved);
305
+
306
+ return {
307
+ ok: true,
308
+ message: catalog.accountReady,
309
+ confirmations,
310
+ import: importOutcome,
311
+ importJobId: importOutcome.jobId || null,
312
+ availability: availabilityOutcome,
313
+ languages: languagesOutcome,
314
+ phone: phoneOutcome,
315
+ priorUsage: { hasEvidence },
316
+ next: hasEvidence === true ? 'onboarding_interview_start' : 'ai_usage',
317
+ usageRepoScope: { mode: 'all' },
318
+ disclaimers: {
319
+ usage: flow.usageDisclaimers(lang),
320
+ interview: flow.interviewDisclaimers(lang),
321
+ },
322
+ profileUrl: flow.profileUrl(resolved),
323
+ };
324
+ }
325
+
326
+ async function registerStatus(args = {}) {
327
+ const res = await flow.pollProfileImport(resolved, { jobId: args.jobId });
328
+ return res.ok ? { ok: true, state: res.state } : { ok: false, reason: res.reason };
329
+ }
330
+
331
+ async function interviewStart(args = {}) {
332
+ let alreadyCompleted = false;
333
+ try {
334
+ alreadyCompleted = await checkCompleted({});
335
+ } catch {
336
+ alreadyCompleted = false;
337
+ }
338
+ if (alreadyCompleted && args.confirmRepeat !== true) {
339
+ return {
340
+ ok: false,
341
+ reason: 'onboarding-already-completed',
342
+ alreadyCompleted: true,
343
+ message:
344
+ 'You have already completed the onboarding interview. Re-running it is optional. If the talent explicitly wants to repeat it, call again with confirmRepeat:true.',
345
+ };
346
+ }
347
+ if (args.disclaimerAcknowledged !== true) {
348
+ return {
349
+ ok: false,
350
+ reason: 'disclaimers-not-acknowledged',
351
+ disclaimers: flow.interviewDisclaimers(lang),
352
+ message: 'Show the talent BOTH interview disclaimers separately (info accessed AND goal+duration), then call again with disclaimerAcknowledged:true.',
353
+ };
354
+ }
355
+ const start = await flow.createAndStartLivekitOnboarding(resolved, {
356
+ candidateId: args.candidateId,
357
+ language: args.language || lang,
358
+ });
359
+ if (!start.ok) return { ok: false, step: start.step || 'start', reason: start.reason };
360
+
361
+ const closing = start.closingMessage;
362
+ const isClosing = closing ? (t) => typeof t === 'string' && t.includes(closing) : null;
363
+ const client = resolved.makeInterviewClient({ isClosing });
364
+ const buffer = new TranscriptBuffer();
365
+
366
+ try {
367
+ await client.connect(start.livekitUrl, start.token);
368
+ } catch (e) {
369
+ try { await client.disconnect(); } catch { /* nothing joined */ }
370
+ const kind = (e && e.kind) || 'connect-failed';
371
+ return { ok: false, step: 'connect', reason: kind, message: connectHint(kind) };
372
+ }
373
+
374
+ let opening;
375
+ try {
376
+ opening = await client.receiveTurn();
377
+ } catch (e) {
378
+ try { await client.disconnect(); } catch { /* room already gone */ }
379
+ return { ok: false, step: 'greeting', reason: (e && e.kind) || 'no-greeting' };
380
+ }
381
+ buffer.recordAgent(opening.text);
382
+ interviewSessions.set(start.interviewId, { client, buffer, connectAt: Date.now(), ended: opening.ended === true });
383
+ return { ok: true, interviewId: start.interviewId, greeting: opening.text, ended: opening.ended === true };
384
+ }
385
+
386
+ async function interviewTurn(args = {}) {
387
+ const sess = interviewSessions.get(args.interviewId);
388
+ if (!sess) {
389
+ return { ok: false, reason: 'unknown-interview', message: 'No live interview for that interviewId (the server may have restarted). Start again with onboarding_interview_start.' };
390
+ }
391
+ if (sess.ended) return { ok: true, response: '', ended: true };
392
+
393
+ const message = typeof args.message === 'string' ? args.message : '';
394
+ sess.buffer.recordUser(message);
395
+ let res;
396
+ try {
397
+ res = await sess.client.sendTurn(message);
398
+ } catch (e) {
399
+ await teardownInterview(args.interviewId, { persist: true });
400
+ return { ok: false, reason: (e && e.kind) || 'turn-error', message: 'The interview connection failed; the transcript so far was saved. Start again with onboarding_interview_start.' };
401
+ }
402
+ sess.buffer.recordAgent(res.text);
403
+ if (res.ended === true) sess.ended = true;
404
+ return { ok: true, response: res.text, ended: res.ended === true };
405
+ }
406
+
407
+ async function interviewComplete(args = {}) {
408
+ if (!interviewSessions.has(args.interviewId)) {
409
+ return { ok: false, reason: 'unknown-interview', message: 'No live interview for that interviewId (the server may have restarted). Nothing to complete; start again if the talent still wants the interview.' };
410
+ }
411
+ const saved = await teardownInterview(args.interviewId, { persist: true });
412
+ if (!saved.ok) return { ok: false, reason: saved.reason || 'complete-failed' };
413
+ return { ok: true, state: 'taken', message: catalog.interviewComplete };
414
+ }
415
+
416
+ // OVERWRITE-repeat: reset the taken onboarding interview so it can be re-run.
417
+ async function repeatOnboarding() {
418
+ const reset = await flow.restartOnboarding(resolved);
419
+ if (!reset.ok) {
420
+ if (reset.reason === 'onboarding-not-found') {
421
+ return { ok: false, reason: 'onboarding-not-found', message: 'The talent has no onboarding interview yet — run the normal onboarding first (register + onboarding_interview_start), nothing to repeat.' };
422
+ }
423
+ return { ok: false, reason: reset.reason };
424
+ }
425
+ return {
426
+ ok: true,
427
+ interviewId: reset.interviewId,
428
+ language: reset.language,
429
+ next: 'onboarding_interview_start',
430
+ message: 'Onboarding reset — the previous interview was overwritten. Now run it again with onboarding_interview_start; do NOT run the main-role step (that belongs to first-time registration) — go straight to onboarding_finish.',
431
+ };
432
+ }
433
+
434
+ async function onboardingFinish() {
435
+ const fin = await flow.completeOnboardingRegistration(resolved);
436
+ if (!fin.ok) return { ok: false, finalized: false, reason: fin.reason, profileUrl: flow.profileUrl(resolved) };
437
+ const pct = fin.completedProfilePercentage;
438
+ return {
439
+ ok: true,
440
+ finalized: true,
441
+ message: typeof pct === 'number' ? catalog.finalizedAt(pct) : catalog.finalizedGeneric,
442
+ onboardingStatus: fin.onboardingStatus,
443
+ registrationLevel: fin.registrationLevel,
444
+ completedProfilePercentage: pct,
445
+ profileUrl: flow.profileUrl(resolved),
446
+ };
447
+ }
448
+
449
+ return [
450
+ {
451
+ name: 'register',
452
+ description: registerDescription,
453
+ inputSchema: schemas.REGISTER_SCHEMA,
454
+ handler: register,
455
+ },
456
+ {
457
+ name: 'register_status',
458
+ description: 'Check the state of the background profile import started by register. Pass the importJobId (an internal handle — never shown to the talent). Returns { state } (e.g. running | done | failed).',
459
+ inputSchema: STATUS_SCHEMA,
460
+ handler: registerStatus,
461
+ },
462
+ {
463
+ name: 'onboarding_interview_start',
464
+ description: "Start the onboarding interview (step 5, optional). Runs over LiveKit: the server joins a live room and the agent conducts it, transparently resolving the branch (role/dedication only when AI-usage evidence exists, otherwise the AI-usage blocks too) — the client does NOT decide the branch. You MUST first show the talent BOTH interview disclaimers separately (from register's disclaimers.interview) and only then call with disclaimerAcknowledged:true. Returns { interviewId, greeting, ended }: relay the greeting to the talent VERBATIM, then drive the interview with onboarding_interview_turn. INTERVIEW RULE — you are a pure CONDUIT, never a participant: show every interview question to the talent VERBATIM (do not reword, clarify, summarize, translate, or add your own hints/examples/answers), and only ever pass the talent's REAL, VERBATIM words back. Do NOT opine, guide, correct, interpret, or answer for the talent at any point. `interviewId` is an internal handle for the interview tools only — never show or mention it to the talent. If it returns reason 'livekit-not-installed', relay the `message` (it tells the talent how to install the optional @livekit/rtc-node package). If the talent declines the interview, do not call this — instead run the final main-role step (list_my_roles + set_main_role) and offer them their profile URL from register. If the talent has ALREADY completed onboarding this refuses with an already-completed notice unless confirmRepeat:true; note a completed onboarding interview cannot actually be restarted (it returns reason 'interview-already-started') — in that case just run set_main_role.",
465
+ inputSchema: INTERVIEW_START_SCHEMA,
466
+ handler: interviewStart,
467
+ available: async () => {
468
+ try {
469
+ return !(await checkCompleted({ timeoutMs: 3000 }));
470
+ } catch {
471
+ return true;
472
+ }
473
+ },
474
+ },
475
+ {
476
+ name: 'onboarding_interview_turn',
477
+ description: "Send the talent's answer to the current onboarding-interview question and get the next one. You are a pure CONDUIT: pass ONLY the talent's REAL, VERBATIM answer in `message` — NEVER invent, infer, complete, or answer on their behalf; if they have not answered, do NOT call this, wait for them. Relay the returned `response` (the next question) to the talent VERBATIM — do not reword, clarify, summarize, translate, or add hints/examples/opinions of your own. Keep calling with their next `message` until `ended` is true, then call onboarding_interview_complete. Do NOT show or mention `interviewId` to the talent (internal handle). If it returns reason 'unknown-interview' (the server was restarted and the live room is gone), start again with onboarding_interview_start.",
478
+ inputSchema: INTERVIEW_TURN_SCHEMA,
479
+ handler: interviewTurn,
480
+ },
481
+ {
482
+ name: 'onboarding_interview_complete',
483
+ description: "Close the onboarding interview once the turns report ended:true: the buffered transcript is saved and the live room is disconnected. `interviewId` is an internal handle for the interview tools only — never show or mention it to the talent. Returns { state:'taken' }. If it returns reason 'unknown-interview' the room was already gone (server restart) — nothing to complete. After this, if this is first-time REGISTRATION, run the final registration step — offer the talent their main role with list_my_roles + set_main_role — then call onboarding_finish. If you got here from repeat_onboarding_interview (a re-run, not registration), SKIP the main-role step and go straight to onboarding_finish. To change the main role outside registration, the talent uses change-role.",
484
+ inputSchema: INTERVIEW_COMPLETE_SCHEMA,
485
+ handler: interviewComplete,
486
+ },
487
+ {
488
+ name: 'repeat_onboarding_interview',
489
+ description: "OVERWRITE-repeat the onboarding interview: resets the talent's completed onboarding interview IN PLACE — the previous evaluation is DISCARDED and CANNOT be recovered. You MUST clearly warn the talent that this overwrites their previous onboarding and is irreversible, and get their explicit go-ahead, BEFORE calling this. On success run the interview again (onboarding_interview_start → onboarding_interview_turn until ended → onboarding_interview_complete), then onboarding_finish. This is a RE-RUN, not registration: do NOT run the main-role step (list_my_roles/set_main_role) afterwards — to change the main role outside registration the talent uses change-role. Returns { interviewId, language } or reason 'onboarding-not-found' (the talent has no onboarding yet — do the normal onboarding first), 'http-401'/'http-403'.",
490
+ inputSchema: { type: 'object', properties: {} },
491
+ handler: repeatOnboarding,
492
+ },
493
+ {
494
+ name: 'onboarding_finish',
495
+ description: "Mark the onboarding as complete — the TERMINAL step, ALWAYS call it once the mandatory steps (register) are done, whether the talent took OR skipped the AI-usage evaluation and the interview. This flips their onboarding status to COMPLETED and lets Shakers promote their registration level (the metrics the team tracks). Idempotent: safe to call once at the end of any path. Returns { finalized, onboardingStatus, registrationLevel, completedProfilePercentage, profileUrl }: use completedProfilePercentage to tell the talent how complete their profile is, and profileUrl to point them to it.",
496
+ inputSchema: FINISH_SCHEMA,
497
+ handler: onboardingFinish,
498
+ },
499
+ ];
500
+ }
501
+
502
+ module.exports = {
503
+ makeRegisterTools,
504
+ ACCOUNT_FIELDS,
505
+ REGISTER_SCHEMA,
506
+ STATUS_SCHEMA,
507
+ INTERVIEW_START_SCHEMA,
508
+ INTERVIEW_TURN_SCHEMA,
509
+ INTERVIEW_COMPLETE_SCHEMA,
510
+ FINISH_SCHEMA,
511
+ PROFILE_SCHEMA,
512
+ WORK_SITUATION_SCHEMA,
513
+ PRICING_SCHEMA,
514
+ AVAILABILITY_SCHEMA,
515
+ LANGUAGES_SCHEMA,
516
+ PHONE_SCHEMA,
517
+ };
@@ -0,0 +1,101 @@
1
+ 'use strict';
2
+
3
+ const REPORT_SCHEMA = {
4
+ type: 'object',
5
+ properties: {
6
+ root: { type: 'string', description: 'Repository root whose report to materialize. Defaults to the server working directory. Ignored when stored is true.' },
7
+ lang: { type: 'string', enum: ['es', 'en'] },
8
+ stored: { type: 'boolean', description: "When true, return the talent's ENTIRE stored server report (inventory + activity + AI-fluency level + prompting/interaction level) as plain text, read from the certs endpoint WITHOUT re-running any scan. When false/omitted, build the local shareable HTML report and return its file path." },
9
+ },
10
+ };
11
+
12
+ const SHARE_SCHEMA = {
13
+ type: 'object',
14
+ properties: {
15
+ root: { type: 'string', description: 'Repository root whose share card to generate. Defaults to the server working directory.' },
16
+ },
17
+ };
18
+
19
+ function makeReportTools(deps = {}) {
20
+ const {
21
+ materializeProjectReport = require('./report-store').materializeProjectReport,
22
+ loadFootprint = require('./report-store').loadFootprint,
23
+ generateShareCard = require('./share-card').generateShareCard,
24
+ loadAuthSession = require('./auth-session-store').loadAuthSession,
25
+ sessionStatus = require('./auth-session-store').sessionStatus,
26
+ getUsageReportEndpoint = require('./config').getUsageReportEndpoint,
27
+ requestUsageReport = require('./report-view-client').requestUsageReport,
28
+ renderUsageReportText = require('./render-usage-report-text').renderUsageReportText,
29
+ renderFullReportText = require('./render-report-text').renderFullReportText,
30
+ detectFlowLang = require('./i18n').detectFlowLang,
31
+ } = deps;
32
+
33
+ const requireSession = () => {
34
+ const session = loadAuthSession();
35
+ if (sessionStatus(session) !== 'active') {
36
+ throw new Error('no active Shakers session — call the login tool (or run `shakers login`) first.');
37
+ }
38
+ return session;
39
+ };
40
+
41
+ async function storedReport(session, lang) {
42
+ const endpoint = getUsageReportEndpoint();
43
+ if (!endpoint) {
44
+ return { ok: false, reason: 'no-endpoint', message: 'no certs endpoint configured to read the stored report.' };
45
+ }
46
+ const res = await requestUsageReport({ accessToken: session.accessToken }, { endpoint });
47
+ if (!res.ok) {
48
+ return { ok: false, reason: res.reason || 'read-failed', message: `could not read the stored report (${res.reason || 'unknown'}).` };
49
+ }
50
+ if (!res.report) {
51
+ return { ok: true, hasReport: false, message: 'no stored report for this talent yet — run ai_usage first (and share the report).' };
52
+ }
53
+ return { ok: true, hasReport: true, text: renderUsageReportText(res, { lang }) };
54
+ }
55
+
56
+ async function report(args = {}) {
57
+ const session = requireSession();
58
+ if (args.stored === true) {
59
+ return storedReport(session, args.lang || detectFlowLang());
60
+ }
61
+ const result = materializeProjectReport({ root: args.root || null, lang: args.lang || null });
62
+ if (!result.hasData) {
63
+ return { ok: false, reason: 'no-data', message: 'no report yet — run ai_usage (and optionally certify) for this repo first.' };
64
+ }
65
+ const ret = { ok: true, path: result.htmlPath, fileUrl: result.fileUrl };
66
+ const footprint = loadFootprint({ root: args.root || null });
67
+ if (footprint && footprint.report) {
68
+ const preview = footprint.report.aiWorkPreview;
69
+ const aiProfile = preview ? { status: 'ready', preview } : { status: 'unavailable', preview: null };
70
+ const text = renderFullReportText(footprint.report, footprint.maturity, aiProfile, { lang: args.lang || detectFlowLang() });
71
+ if (text) ret.text = text;
72
+ }
73
+ return ret;
74
+ }
75
+
76
+ async function share(args = {}) {
77
+ requireSession();
78
+ const result = generateShareCard({ root: args.root || null });
79
+ if (!result.ok) {
80
+ return { ok: false, reason: 'no-footprint', message: 'no report yet — run ai_usage for this repo first.' };
81
+ }
82
+ return { ok: true, path: result.htmlPath, fileUrl: result.fileUrl };
83
+ }
84
+
85
+ return [
86
+ {
87
+ name: 'report',
88
+ description: "Two modes. stored:true returns the talent's ENTIRE stored server report (inventory + activity + AI-fluency level + prompting/interaction level) as plain text, read from certs WITHOUT re-running any scan — says so cleanly when none is stored (this stored view does NOT include the Setup×Usage matrix or the vision/how-I-work narratives). Otherwise builds the local report for this repo from local state: it returns the full human-readable report in `text` (Markdown mirroring the CLI terminal — Setup level and rank rationale, tools, MCP servers, agents, activity, plus the Setup×Usage matrix and vision/how-I-work when available) AND the HTML file path (no browser). Render `text` verbatim to the talent; keep the fenced blocks fenced so the tables stay aligned. Requires an active session. Any internal ids/codes are handles for tools only — never show or mention them to the talent.",
89
+ inputSchema: REPORT_SCHEMA,
90
+ handler: report,
91
+ },
92
+ {
93
+ name: 'share',
94
+ description: "Generate the Shakers-branded LinkedIn share card for this repo and return its file path. Does not open a browser. Requires an active session.",
95
+ inputSchema: SHARE_SCHEMA,
96
+ handler: share,
97
+ },
98
+ ];
99
+ }
100
+
101
+ module.exports = { makeReportTools, REPORT_SCHEMA, SHARE_SCHEMA };