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.
- package/LICENSE +59 -0
- package/README.md +1233 -2
- package/bin/add-agent.js +88 -0
- package/bin/add-project.js +88 -0
- package/bin/add-role.js +61 -0
- package/bin/add-skill.js +88 -0
- package/bin/ai-usage.js +99 -0
- package/bin/alma.js +99 -0
- package/bin/applications.js +69 -0
- package/bin/availability.js +70 -0
- package/bin/certifications.js +69 -0
- package/bin/certify.js +73 -0
- package/bin/change-role.js +61 -0
- package/bin/config.js +124 -0
- package/bin/experiences.js +69 -0
- package/bin/find-projects.js +89 -0
- package/bin/invitations.js +69 -0
- package/bin/lang.js +70 -0
- package/bin/login.js +216 -0
- package/bin/mcp.js +124 -0
- package/bin/mcpb.js +5 -0
- package/bin/me.js +69 -0
- package/bin/onboarding.js +134 -0
- package/bin/portfolios.js +69 -0
- package/bin/rate.js +70 -0
- package/bin/register.js +337 -0
- package/bin/report-html.js +134 -0
- package/bin/save.js +77 -0
- package/bin/shakers.js +363 -0
- package/bin/share.js +72 -0
- package/bin/show-project.js +72 -0
- package/bin/socials.js +70 -0
- package/bin/superadmin.js +351 -0
- package/bin/update-check-worker.js +70 -0
- package/bin/update.js +111 -0
- package/package.json +40 -4
- package/src/agent-category.js +13 -0
- package/src/agent-evaluation.js +177 -0
- package/src/agent-org-chart.js +355 -0
- package/src/agent-synthesis.js +132 -0
- package/src/agent-usage.js +118 -0
- package/src/agents-client.js +165 -0
- package/src/ai-fluency-cell.js +72 -0
- package/src/ai-profile-client.js +45 -0
- package/src/ai-profile-preview.js +154 -0
- package/src/ai.js +66 -0
- package/src/alma-client.js +123 -0
- package/src/alma-flow.js +32 -0
- package/src/alma-repo-context.js +47 -0
- package/src/alma-shell.js +161 -0
- package/src/ansi.js +62 -0
- package/src/applications-client.js +79 -0
- package/src/applications-flow.js +58 -0
- package/src/auth-client.js +144 -0
- package/src/auth-device-client.js +118 -0
- package/src/auth-method-picker.js +44 -0
- package/src/auth-methods.js +35 -0
- package/src/auth-session-store.js +215 -0
- package/src/authorship-mismatch.js +62 -0
- package/src/authorship.js +204 -0
- package/src/automations-detector.js +170 -0
- package/src/availability-client.js +93 -0
- package/src/availability-flow.js +124 -0
- package/src/backend-request.js +191 -0
- package/src/blind-approval-extract.js +48 -0
- package/src/brand-ansi.js +14 -0
- package/src/brand-svg.js +9 -0
- package/src/browser-tools-detector.js +21 -0
- package/src/build-next-level.js +56 -0
- package/src/certifications-client.js +89 -0
- package/src/certifications-flow.js +75 -0
- package/src/certify-dimension-client.js +163 -0
- package/src/certify-dimension-flow.js +135 -0
- package/src/certify-disclaimer.js +51 -0
- package/src/certify-remediation-prompt.js +23 -0
- package/src/cli-args.js +83 -0
- package/src/command-audit.js +105 -0
- package/src/config-dir.js +120 -0
- package/src/config.js +877 -0
- package/src/consent-flow.js +115 -0
- package/src/consent-skip.js +27 -0
- package/src/cv-file.js +62 -0
- package/src/cv-text.js +92 -0
- package/src/decisions-extract.js +43 -0
- package/src/detect-repos.js +145 -0
- package/src/detected-tools.js +15 -0
- package/src/detection-evidence.js +105 -0
- package/src/detection-noise.js +132 -0
- package/src/detectors.js +221 -0
- package/src/device-auth-store.js +100 -0
- package/src/device-login.js +120 -0
- package/src/email-verification.js +152 -0
- package/src/env-paths.js +30 -0
- package/src/find-projects-cache.js +89 -0
- package/src/find-projects-client.js +148 -0
- package/src/find-projects-flow.js +164 -0
- package/src/git-activity.js +124 -0
- package/src/git-log-prefetch.js +53 -0
- package/src/graph-certs.js +58 -0
- package/src/graph-generator.js +230 -0
- package/src/graph-scan.js +239 -0
- package/src/i18n.js +3341 -0
- package/src/inter-fonts.js +6 -0
- package/src/interactive-select.js +183 -0
- package/src/interview-client.js +226 -0
- package/src/interview-transcript.js +56 -0
- package/src/inventory-client.js +90 -0
- package/src/invitations-client.js +66 -0
- package/src/invitations-flow.js +50 -0
- package/src/lang-client.js +147 -0
- package/src/lang-codes.js +14 -0
- package/src/lang-flow.js +85 -0
- package/src/legal-notice.js +53 -0
- package/src/locale.js +72 -0
- package/src/markdown-ansi.js +50 -0
- package/src/maturity.js +111 -0
- package/src/mcp-ai-usage-result-tool.js +111 -0
- package/src/mcp-ai-usage-tool.js +401 -0
- package/src/mcp-alma-tools.js +95 -0
- package/src/mcp-applications-tools.js +44 -0
- package/src/mcp-auth-form.js +480 -0
- package/src/mcp-availability-tools.js +45 -0
- package/src/mcp-certifications-tools.js +44 -0
- package/src/mcp-certify-dimension-tools.js +66 -0
- package/src/mcp-choice.js +95 -0
- package/src/mcp-detector.js +223 -0
- package/src/mcp-device-login-tools.js +177 -0
- package/src/mcp-find-projects-tools.js +69 -0
- package/src/mcp-install.js +275 -0
- package/src/mcp-instructions.js +27 -0
- package/src/mcp-invitations-tools.js +44 -0
- package/src/mcp-lang-tools.js +41 -0
- package/src/mcp-onboarding-tools.js +379 -0
- package/src/mcp-portfolios-tools.js +50 -0
- package/src/mcp-profile-tools.js +48 -0
- package/src/mcp-prompts.js +73 -0
- package/src/mcp-rate-tools.js +44 -0
- package/src/mcp-register-tools.js +441 -0
- package/src/mcp-report-tools.js +105 -0
- package/src/mcp-role-tools.js +372 -0
- package/src/mcp-saved-positions-tools.js +59 -0
- package/src/mcp-server.js +290 -0
- package/src/mcp-show-project-tools.js +52 -0
- package/src/mcp-signup-client.js +118 -0
- package/src/mcp-signup-tools.js +1171 -0
- package/src/mcp-socials-tools.js +41 -0
- package/src/memory-structure-detector.js +108 -0
- package/src/model-call-record.js +198 -0
- package/src/onboarding-availability.js +26 -0
- package/src/onboarding-client.js +374 -0
- package/src/onboarding-flow.js +1162 -0
- package/src/onboarding-funnel.js +272 -0
- package/src/onboarding-livekit-client.js +102 -0
- package/src/onboarding-status-client.js +57 -0
- package/src/open-file.js +32 -0
- package/src/osc-link.js +16 -0
- package/src/portfolio-client.js +164 -0
- package/src/portfolio-git-info.js +61 -0
- package/src/portfolios-client.js +85 -0
- package/src/portfolios-flow.js +79 -0
- package/src/profile-client.js +133 -0
- package/src/profile-flow.js +115 -0
- package/src/profile-switch.js +46 -0
- package/src/prompt-select.js +66 -0
- package/src/rate-client.js +81 -0
- package/src/rate-flow.js +137 -0
- package/src/register-context.js +145 -0
- package/src/render-ai-fluency.js +266 -0
- package/src/render-certification.js +177 -0
- package/src/render-html.js +140 -0
- package/src/render-report-text.js +27 -0
- package/src/render-sheet.js +261 -0
- package/src/render-terminal.js +109 -0
- package/src/render-usage-report-text.js +155 -0
- package/src/repo-context.js +215 -0
- package/src/report-gating.js +49 -0
- package/src/report-store.js +226 -0
- package/src/report-theme.js +177 -0
- package/src/report-view-client.js +50 -0
- package/src/reveal-output.js +29 -0
- package/src/roadmap-content.js +453 -0
- package/src/roadmap-personalization.js +132 -0
- package/src/roadmap-prompt.js +154 -0
- package/src/roles-client.js +112 -0
- package/src/roles-flow.js +192 -0
- package/src/sanitize-network-text.js +36 -0
- package/src/saved-positions-client.js +71 -0
- package/src/saved-positions-flow.js +50 -0
- package/src/scan-exclusions.js +50 -0
- package/src/scanner.js +624 -0
- package/src/scope-signals.js +148 -0
- package/src/secret-prompt.js +72 -0
- package/src/session-gate.js +30 -0
- package/src/session-normalizers.js +174 -0
- package/src/session-refresh.js +51 -0
- package/src/session-scan.js +475 -0
- package/src/session-signals.js +36 -0
- package/src/shakers-wordmark.js +17 -0
- package/src/share-card.js +362 -0
- package/src/share-certification.js +111 -0
- package/src/share-consent-state.js +157 -0
- package/src/share-payload.js +300 -0
- package/src/share.js +185 -0
- package/src/show-project-client.js +58 -0
- package/src/show-project-flow.js +119 -0
- package/src/signup-client.js +103 -0
- package/src/signup-copy.js +450 -0
- package/src/signup-language.js +21 -0
- package/src/skills-client.js +102 -0
- package/src/socials-client.js +91 -0
- package/src/socials-flow.js +83 -0
- package/src/specificity-extract.js +41 -0
- package/src/start-add-agents.js +306 -0
- package/src/start-add-portfolio.js +388 -0
- package/src/start-add-skills.js +260 -0
- package/src/stdin-ask.js +129 -0
- package/src/steering-extract.js +43 -0
- package/src/strip-internal-comments.js +45 -0
- package/src/tech-detector.js +295 -0
- package/src/templates/report-sheet.html +572 -0
- package/src/terminal-agents.js +207 -0
- package/src/terminal-format.js +85 -0
- package/src/terminal-progress.js +106 -0
- package/src/terminal-sections.js +219 -0
- package/src/tier-analysis.js +126 -0
- package/src/tier-engine.js +114 -0
- package/src/tier-projection.js +117 -0
- package/src/transcript-turns.js +49 -0
- package/src/translations-client.js +84 -0
- package/src/typewriter.js +154 -0
- package/src/update-flow.js +223 -0
- package/src/update-notifier.js +113 -0
- package/src/usage-model-calls.js +119 -0
- package/src/usage-oneshot-commands.js +180 -0
- package/src/usage-repo-scope.js +162 -0
- package/src/usage-run-steps.js +354 -0
- package/src/work-streams.js +79 -0
|
@@ -0,0 +1,401 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const { execFileSync } = require('child_process');
|
|
5
|
+
const { isRefusedWalkRoot } = require('./scan-exclusions');
|
|
6
|
+
const { askChoice, chatChoice, queueNotice } = require('./mcp-choice');
|
|
7
|
+
|
|
8
|
+
function isExistingDir(p) {
|
|
9
|
+
try {
|
|
10
|
+
return fs.statSync(p).isDirectory();
|
|
11
|
+
} catch {
|
|
12
|
+
return false;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function defaultCwdIsRepo(cwd) {
|
|
17
|
+
try {
|
|
18
|
+
execFileSync('git', ['-C', cwd, 'rev-parse', '--show-toplevel'], {
|
|
19
|
+
stdio: ['ignore', 'ignore', 'ignore'],
|
|
20
|
+
timeout: 5000,
|
|
21
|
+
});
|
|
22
|
+
return true;
|
|
23
|
+
} catch {
|
|
24
|
+
return false;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const AI_USAGE_INPUT_SCHEMA = {
|
|
29
|
+
type: 'object',
|
|
30
|
+
properties: {
|
|
31
|
+
consent: {
|
|
32
|
+
type: 'object',
|
|
33
|
+
description:
|
|
34
|
+
"The talent's explicit answer to the AI-usage question this tool asks together with both disclaimers. Nothing is scanned without granted:true. A yes to anything else is not this answer.",
|
|
35
|
+
properties: {
|
|
36
|
+
granted: { type: 'boolean', description: 'true ONLY after the talent explicitly said yes to the AI-usage question; false when they said no. Omit it to get the disclaimers and the question to ask.' },
|
|
37
|
+
email: { type: 'string', description: 'The talent email the report is attributed to. Required when granted is true.' },
|
|
38
|
+
},
|
|
39
|
+
required: ['granted'],
|
|
40
|
+
},
|
|
41
|
+
repoScope: {
|
|
42
|
+
type: 'object',
|
|
43
|
+
description: 'Which repositories feed the evaluation. Defaults to the current repository only.',
|
|
44
|
+
properties: {
|
|
45
|
+
mode: {
|
|
46
|
+
type: 'string',
|
|
47
|
+
enum: ['current', 'all', 'list'],
|
|
48
|
+
description: "'current' = only the repo at root; 'all' = machine-wide session signals; 'list' = the repos named in `repos`.",
|
|
49
|
+
},
|
|
50
|
+
repos: {
|
|
51
|
+
type: 'array',
|
|
52
|
+
items: { type: 'string' },
|
|
53
|
+
description: "Repo names or paths, used only when mode is 'list'.",
|
|
54
|
+
},
|
|
55
|
+
},
|
|
56
|
+
},
|
|
57
|
+
root: {
|
|
58
|
+
type: 'string',
|
|
59
|
+
description: 'Absolute path to the repository to evaluate. Defaults to the server working directory.',
|
|
60
|
+
},
|
|
61
|
+
lang: { type: 'string', enum: ['es', 'en'], description: 'Report language.' },
|
|
62
|
+
},
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
function buildArgv({ root, repoScope, lang }) {
|
|
66
|
+
const argv = ['--json', '--no-save', '--no-ai'];
|
|
67
|
+
if (root) argv.push('--root', root);
|
|
68
|
+
const mode = repoScope && repoScope.mode;
|
|
69
|
+
if (mode === 'all') {
|
|
70
|
+
argv.push('--all-repos');
|
|
71
|
+
} else if (mode === 'list' && Array.isArray(repoScope.repos) && repoScope.repos.length > 0) {
|
|
72
|
+
argv.push('--repos', repoScope.repos.join(','));
|
|
73
|
+
}
|
|
74
|
+
if (lang === 'es' || lang === 'en') argv.push('--lang', lang);
|
|
75
|
+
return argv;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function makeAiUsageTool(deps = {}) {
|
|
79
|
+
const {
|
|
80
|
+
scanUsage = (argv) => require('../bin/ai-usage').scanUsage(argv),
|
|
81
|
+
autoShare = require('./share').autoShare,
|
|
82
|
+
recordConsent = require('./share').recordConsent,
|
|
83
|
+
isValidEmail = require('./share').isValidEmail,
|
|
84
|
+
persistFootprint = require('./report-store').persistFootprint,
|
|
85
|
+
cwdIsRepo = defaultCwdIsRepo,
|
|
86
|
+
loadAuthSession = require('./auth-session-store').loadAuthSession,
|
|
87
|
+
sessionStatus = require('./auth-session-store').sessionStatus,
|
|
88
|
+
getUsageDiscoveredInventoryEndpoint = require('./config').getUsageDiscoveredInventoryEndpoint,
|
|
89
|
+
requestDiscoveredInventory = require('./inventory-client').requestDiscoveredInventory,
|
|
90
|
+
pollClassifiedInventory = require('./mcp-ai-usage-result-tool').pollClassifiedInventory,
|
|
91
|
+
tierLadder = require('./tier-engine').tierLadder,
|
|
92
|
+
inlineWaitMs = 35000,
|
|
93
|
+
// "My work with AI" preview, from the same hub endpoint the front paints.
|
|
94
|
+
resolveAiProfilePreview = require('./ai-profile-preview').resolveAiProfilePreview,
|
|
95
|
+
ensureFreshSession = require('./session-refresh').ensureFreshSession,
|
|
96
|
+
getAiProfileEndpoint = require('./config').getAiProfileEndpoint,
|
|
97
|
+
// Plain-text render of the full report (terminal sections + matrix/vision/howIWork), shared with the CLI (no ANSI).
|
|
98
|
+
renderFullReportText = require('./render-report-text').renderFullReportText,
|
|
99
|
+
detectFlowLang = require('./i18n').detectFlowLang,
|
|
100
|
+
aiFluencyWaitMs = 30000,
|
|
101
|
+
signupPhase = () => require('./mcp-signup-tools').signupPhase(),
|
|
102
|
+
usageDisclaimers = (lang) => require('./onboarding-flow').usageDisclaimers(lang),
|
|
103
|
+
signupCopy = require('./signup-copy').signupCopy,
|
|
104
|
+
signupLanguage = require('./signup-language').signupLanguage,
|
|
105
|
+
} = deps;
|
|
106
|
+
|
|
107
|
+
// The sign-up's one scan: started on the talent's yes once the account exists, uploaded in the background.
|
|
108
|
+
let job = null;
|
|
109
|
+
// The talent's answer in the consent dialog, so a later call cannot contradict it.
|
|
110
|
+
let dialogConsent = null;
|
|
111
|
+
|
|
112
|
+
function startSignupScan(argv, root) {
|
|
113
|
+
if (job) return job;
|
|
114
|
+
job = { state: 'scanning', send: null, uploading: null };
|
|
115
|
+
// performScan is synchronous: start it after this answer has gone out.
|
|
116
|
+
job.scan = new Promise((resolve) => { setImmediate(resolve); }).then(() => scanUsage(argv)).then((scan) => {
|
|
117
|
+
try { persistFootprint({ root, report: scan.report, maturity: scan.maturity }); } catch { /* local copy is best-effort */ }
|
|
118
|
+
return scan;
|
|
119
|
+
});
|
|
120
|
+
job.scan.catch(() => { job.state = 'failed'; });
|
|
121
|
+
job.root = root;
|
|
122
|
+
return job;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function uploadSignupScan() {
|
|
126
|
+
if (!job) return Promise.resolve(null);
|
|
127
|
+
if (job.uploading) return job.uploading;
|
|
128
|
+
const current = job;
|
|
129
|
+
current.uploading = current.scan.then(async (scan) => {
|
|
130
|
+
const session = loadAuthSession();
|
|
131
|
+
if (sessionStatus(session) !== 'active' || !session || !isValidEmail(session.email)) {
|
|
132
|
+
current.uploading = null;
|
|
133
|
+
return { ok: false, skipped: true, reason: 'no-session' };
|
|
134
|
+
}
|
|
135
|
+
current.state = 'uploading';
|
|
136
|
+
// Nobody awaits this promise: a local write error must end as a failed send, never as an
|
|
137
|
+
// unhandled rejection that takes the MCP process down.
|
|
138
|
+
let send;
|
|
139
|
+
try {
|
|
140
|
+
recordConsent('granted', session.email, { verified: true });
|
|
141
|
+
send = await autoShare(scan.report, scan.maturity, { root: current.root, bypassThrottle: true });
|
|
142
|
+
} catch (error) {
|
|
143
|
+
send = { ok: false, reason: 'upload-failed', message: error && error.message ? error.message : String(error) };
|
|
144
|
+
}
|
|
145
|
+
current.state = send && send.ok ? 'sent' : 'failed';
|
|
146
|
+
current.send = send;
|
|
147
|
+
return send;
|
|
148
|
+
}, () => ({ ok: false, skipped: true, reason: 'scan-failed' }));
|
|
149
|
+
return current.uploading;
|
|
150
|
+
}
|
|
151
|
+
function declined(lang) {
|
|
152
|
+
recordConsent('denied');
|
|
153
|
+
if (signupPhase() === 'account') {
|
|
154
|
+
queueNotice(signupCopy(lang).aiUsageSkipped);
|
|
155
|
+
return { ok: false, reason: 'consent-declined', message: 'Nothing was scanned. Say nothing about it: its fixed line for the talent comes with the next question. Carry on with the interview offer.' };
|
|
156
|
+
}
|
|
157
|
+
return { ok: false, reason: 'consent-declined', relayVerbatim: signupCopy(lang).aiUsageSkipped, message: 'Nothing was scanned. Show relayVerbatim once, word for word.' };
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
async function askConsent(lang, ctx) {
|
|
161
|
+
const c = signupCopy(lang);
|
|
162
|
+
const texts = Object.values(usageDisclaimers(lang));
|
|
163
|
+
const asked = ctx && ctx.elicitation
|
|
164
|
+
? await askChoice(ctx, { texts, question: c.aiUsageQuestion, options: [c.aiUsageYes, c.aiUsageSkip] })
|
|
165
|
+
: { chat: chatChoice(c.aiUsageQuestion, [c.aiUsageYes, c.aiUsageSkip], texts) };
|
|
166
|
+
if (asked.answer === c.aiUsageSkip) {
|
|
167
|
+
dialogConsent = 'denied';
|
|
168
|
+
return declined(lang);
|
|
169
|
+
}
|
|
170
|
+
if (asked.answer === c.aiUsageYes) {
|
|
171
|
+
dialogConsent = 'granted';
|
|
172
|
+
return { ok: true, reason: 'consent-granted', consent: { granted: true }, message: 'The talent said yes in the dialog: do not ask again. Call ai_usage with consent.granted:true (and their email) to run it.' };
|
|
173
|
+
}
|
|
174
|
+
return {
|
|
175
|
+
ok: false,
|
|
176
|
+
reason: 'consent-required',
|
|
177
|
+
...(asked.dismissed ? { dismissed: asked.dismissed } : {}),
|
|
178
|
+
...asked.chat,
|
|
179
|
+
message: `Nothing was scanned. ${asked.chat.choiceMessage} Then call ai_usage again with consent.granted:true only after they pick the first option, or consent.granted:false if they skip it.`,
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
async function handler(args = {}, ctx = {}) {
|
|
184
|
+
const phase = signupPhase();
|
|
185
|
+
// The sign-up looks at the whole machine: the server rarely runs inside a repository there.
|
|
186
|
+
const repoScope = args.repoScope || (phase === 'account' ? { mode: 'all' } : undefined);
|
|
187
|
+
const mode = (repoScope && repoScope.mode) || 'current';
|
|
188
|
+
const lang = args.lang === 'es' || args.lang === 'en' ? args.lang : detectFlowLang();
|
|
189
|
+
const textLang = signupLanguage(lang);
|
|
190
|
+
if (phase === 'waiting') {
|
|
191
|
+
return { ok: false, reason: 'account-pending', message: 'Nothing was scanned. Ask about AI usage only once signup_status says the account exists.' };
|
|
192
|
+
}
|
|
193
|
+
const consent = args.consent || {};
|
|
194
|
+
if (consent.granted === false) return declined(textLang);
|
|
195
|
+
if (consent.granted === true && dialogConsent === 'denied') {
|
|
196
|
+
return { ok: false, reason: 'consent-declined', message: 'The talent skipped the AI-usage step in the dialog: nothing is scanned. Carry on without it. If they later ask for the scan themselves, call ai_usage without consent to ask them again.' };
|
|
197
|
+
}
|
|
198
|
+
if (consent.granted !== true) {
|
|
199
|
+
const answered = await askConsent(textLang, ctx);
|
|
200
|
+
// In the sign-up a yes in the dialog runs the scan right away, in the background.
|
|
201
|
+
if (!(answered.reason === 'consent-granted' && phase === 'account')) return answered;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
let root;
|
|
205
|
+
if (typeof args.root === 'string' && args.root) {
|
|
206
|
+
if (!isExistingDir(args.root)) {
|
|
207
|
+
return { ok: false, reason: 'invalid-root', message: `root is not an existing directory: ${args.root}` };
|
|
208
|
+
}
|
|
209
|
+
root = args.root;
|
|
210
|
+
} else {
|
|
211
|
+
const cwd = process.cwd();
|
|
212
|
+
if (!isRefusedWalkRoot(cwd) && cwdIsRepo(cwd)) {
|
|
213
|
+
root = cwd;
|
|
214
|
+
} else if (mode === 'all') {
|
|
215
|
+
root = cwd;
|
|
216
|
+
} else {
|
|
217
|
+
return {
|
|
218
|
+
ok: false,
|
|
219
|
+
reason: 'no-repo-root',
|
|
220
|
+
message: 'The Shakers MCP server is running from a non-repository directory, so there is no repository to evaluate. Pass an absolute path to the repository via `root`, or use repoScope.mode "all" for a machine-wide report.',
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
const email = typeof consent.email === 'string' ? consent.email.trim() : '';
|
|
226
|
+
const argv = buildArgv({ root, repoScope, lang: args.lang });
|
|
227
|
+
|
|
228
|
+
// In the sign-up the scan never holds the conversation: it runs and uploads in the background.
|
|
229
|
+
if (phase === 'account') {
|
|
230
|
+
const current = startSignupScan(argv, root);
|
|
231
|
+
uploadSignupScan();
|
|
232
|
+
return {
|
|
233
|
+
ok: true,
|
|
234
|
+
background: true,
|
|
235
|
+
send: current.send || { ok: false, skipped: true, reason: 'uploading' },
|
|
236
|
+
scope: { mode, root },
|
|
237
|
+
message: 'The analysis runs and uploads in the background; Shakers completes the profile with it in a few minutes. Do not wait for it and do not call ai_usage again: carry on with the interview offer.',
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
const verifySession = loadAuthSession();
|
|
242
|
+
const emailVerifiedByLogin =
|
|
243
|
+
sessionStatus(verifySession) === 'active'
|
|
244
|
+
&& !!verifySession
|
|
245
|
+
&& isValidEmail(verifySession.email);
|
|
246
|
+
|
|
247
|
+
if (!isValidEmail(email)) {
|
|
248
|
+
throw new Error('consent.granted is true but consent.email is missing or invalid — ask the talent for their email.');
|
|
249
|
+
}
|
|
250
|
+
if (emailVerifiedByLogin) {
|
|
251
|
+
recordConsent('granted', verifySession.email, { verified: true });
|
|
252
|
+
} else {
|
|
253
|
+
recordConsent('granted', email, { verified: false });
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
const canSubmit = emailVerifiedByLogin;
|
|
257
|
+
const submittedEmail = canSubmit ? verifySession.email : null;
|
|
258
|
+
|
|
259
|
+
const { report, maturity } = await scanUsage(argv);
|
|
260
|
+
|
|
261
|
+
let savedLocally = false;
|
|
262
|
+
try {
|
|
263
|
+
persistFootprint({ root, report, maturity });
|
|
264
|
+
savedLocally = true;
|
|
265
|
+
} catch {
|
|
266
|
+
savedLocally = false;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
const send = canSubmit
|
|
270
|
+
? await autoShare(report, maturity, { root })
|
|
271
|
+
: { ok: false, skipped: true, reason: 'email-unverified' };
|
|
272
|
+
const submitted = !!(send && send.ok);
|
|
273
|
+
|
|
274
|
+
const hasAgents = Array.isArray(report.agents) && report.agents.length > 0;
|
|
275
|
+
|
|
276
|
+
let enrichment = submitted && hasAgents ? 'pending' : 'none';
|
|
277
|
+
let enrichedAgents = null;
|
|
278
|
+
if (submitted && hasAgents) {
|
|
279
|
+
const session = loadAuthSession();
|
|
280
|
+
const endpoint = getUsageDiscoveredInventoryEndpoint();
|
|
281
|
+
if (sessionStatus(session) === 'active' && endpoint) {
|
|
282
|
+
const polled = await pollClassifiedInventory({
|
|
283
|
+
accessToken: session.accessToken,
|
|
284
|
+
endpoint,
|
|
285
|
+
requestDiscoveredInventory,
|
|
286
|
+
capMs: inlineWaitMs,
|
|
287
|
+
});
|
|
288
|
+
if (polled.status === 'ready') {
|
|
289
|
+
enrichment = 'ready';
|
|
290
|
+
enrichedAgents = polled.agents;
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// "My work with AI" preview: only after a real submit this run.
|
|
296
|
+
let aiProfile = { status: 'unavailable', preview: null };
|
|
297
|
+
if (submitted) {
|
|
298
|
+
const usageSession = loadAuthSession();
|
|
299
|
+
if (sessionStatus(usageSession) === 'active') {
|
|
300
|
+
aiProfile = await resolveAiProfilePreview(
|
|
301
|
+
{
|
|
302
|
+
session: usageSession,
|
|
303
|
+
endpoint: getAiProfileEndpoint(),
|
|
304
|
+
timeoutMs: aiFluencyWaitMs,
|
|
305
|
+
},
|
|
306
|
+
{
|
|
307
|
+
refreshToken: async () => {
|
|
308
|
+
const fresh = await ensureFreshSession(process.env);
|
|
309
|
+
return fresh ? { accessToken: fresh.accessToken, hubAccessToken: fresh.hubAccessToken } : null;
|
|
310
|
+
},
|
|
311
|
+
},
|
|
312
|
+
);
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
const displayAgents = enrichedAgents
|
|
317
|
+
? enrichedAgents.map((a) => ({ name: a.name, category: a.category, role: a.role }))
|
|
318
|
+
: (report.agents || []).map((a) => ({ name: a.name }));
|
|
319
|
+
|
|
320
|
+
// Same full report the CLI terminal prints, as plain markdown (no ANSI), so a chat client shows the talent the same thing.
|
|
321
|
+
const text = renderFullReportText(
|
|
322
|
+
report,
|
|
323
|
+
maturity,
|
|
324
|
+
{ status: aiProfile.status, preview: aiProfile.preview },
|
|
325
|
+
{ lang },
|
|
326
|
+
);
|
|
327
|
+
|
|
328
|
+
let message;
|
|
329
|
+
if (submitted) {
|
|
330
|
+
message =
|
|
331
|
+
enrichment === 'ready'
|
|
332
|
+
? 'Classification complete — agent categories and descriptions are ready.'
|
|
333
|
+
: enrichment === 'pending'
|
|
334
|
+
? 'Report submitted. Shakers is classifying the agents server-side (this is normal, not an error) — call ai_usage_result shortly to get their categories and descriptions.'
|
|
335
|
+
: 'Report submitted.';
|
|
336
|
+
} else if (send.reason === 'email-unverified') {
|
|
337
|
+
message = "Not submitted — the talent's email is not verified. Ask them to log in / verify with the Shakers CLI (`shakers login`) first, then run ai_usage again; nothing was sent to Shakers.";
|
|
338
|
+
} else if (send.skipped) {
|
|
339
|
+
message = `Not submitted (${send.reason}). The report was kept on this machine only; nothing was sent to Shakers.`;
|
|
340
|
+
} else {
|
|
341
|
+
message = `Submit failed (${send.reason}). The report was kept on this machine only; retry later.`;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
return {
|
|
345
|
+
report,
|
|
346
|
+
maturity,
|
|
347
|
+
send,
|
|
348
|
+
savedLocally,
|
|
349
|
+
enrichment,
|
|
350
|
+
agents: enrichedAgents,
|
|
351
|
+
// Plain-text "My work with AI" report (no ANSI), same content/layout as the CLI.
|
|
352
|
+
text,
|
|
353
|
+
// "My work with AI" preview: status ready | pending | unavailable.
|
|
354
|
+
aiProfile,
|
|
355
|
+
next: submitted && enrichment === 'pending' ? 'ai_usage_result' : null,
|
|
356
|
+
message,
|
|
357
|
+
display: {
|
|
358
|
+
tier: {
|
|
359
|
+
key: maturity.tierKey,
|
|
360
|
+
name: maturity.tierName,
|
|
361
|
+
setupLevel: maturity.name,
|
|
362
|
+
score: maturity.score,
|
|
363
|
+
},
|
|
364
|
+
// Matrix (3x3) + vision (E) + howIWork (F) + traction; null until projected.
|
|
365
|
+
aiProfile: {
|
|
366
|
+
status: aiProfile.status,
|
|
367
|
+
setup: aiProfile.preview ? aiProfile.preview.matrix.setupCode : null,
|
|
368
|
+
usage: aiProfile.preview ? aiProfile.preview.matrix.usageLevel : null,
|
|
369
|
+
cell: aiProfile.preview ? aiProfile.preview.matrix.cell : null,
|
|
370
|
+
isAiNative: aiProfile.preview ? aiProfile.preview.matrix.isAiNative : false,
|
|
371
|
+
vision: aiProfile.preview ? aiProfile.preview.vision : null,
|
|
372
|
+
howIWork: aiProfile.preview ? aiProfile.preview.howIWork : null,
|
|
373
|
+
traction: aiProfile.preview ? aiProfile.preview.traction : null,
|
|
374
|
+
},
|
|
375
|
+
tierLegend: tierLadder(),
|
|
376
|
+
agents: displayAgents,
|
|
377
|
+
},
|
|
378
|
+
scope: {
|
|
379
|
+
mode,
|
|
380
|
+
root,
|
|
381
|
+
},
|
|
382
|
+
consent: { granted: true, email: canSubmit ? submittedEmail : email },
|
|
383
|
+
};
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
return {
|
|
387
|
+
name: 'ai_usage',
|
|
388
|
+
title: 'Analyse how you work with AI',
|
|
389
|
+
description: 'Asks you first and, only if you say yes, looks at which AI tools you use on this computer and the history of your code projects. With your yes it sends a summary with anonymised excerpts to your Shakers profile; your code and prompts never leave your computer.',
|
|
390
|
+
guidance:
|
|
391
|
+
"Measure how a Shakers talent uses AI in their work: scans this machine and repository for AI-tool usage (subagents, MCP servers, sessions, git activity), scores their AI-maturity tier (T0-T7), and, with the talent's consent, submits the report to Shakers so their profile reflects it. Runs entirely on the talent's own machine. Before any scan, call it without consent: it asks the AI-usage question with both disclaimers in the same block (a dialog, or `say` to print word for word); only an explicit yes allows consent.granted:true, and skipping is consent.granted:false. In the sign-up it is the step right after the account exists and before the interview: on yes it runs and uploads in the background, never wait for it. Submission also requires an active, email-verified Shakers session (via `shakers login` / registration OTP): without a verified session the report stays on this machine and nothing is sent — check the `send` field for the outcome. Always fast and deterministic: it runs no LLM on the machine. Agent descriptions and catalog classification are produced by Shakers server-side. This waits briefly (~30s) and, if classification finishes, returns the enriched agents inline; otherwise it returns enrichment:'pending' (normal progress, not an error) and you should call `ai_usage_result` to get them. The `text` field is a ready-to-show plain-text 'My work with AI' report (Markdown, no ANSI) mirroring the CLI terminal — render it verbatim to the talent; the matrix grid is inside a fenced code block, keep it fenced so it stays aligned. Use the `display` block for structured access: show the tier (with the tierLegend explaining T0-T7) and the agent list by NAME and category/role. IDs and codes (agent `code`, skillId, catalogId) are internal handles for the add_* tools only — never show raw IDs or codes to the talent.",
|
|
392
|
+
inputSchema: AI_USAGE_INPUT_SCHEMA,
|
|
393
|
+
handler,
|
|
394
|
+
};
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
module.exports = {
|
|
398
|
+
makeAiUsageTool,
|
|
399
|
+
AI_USAGE_INPUT_SCHEMA,
|
|
400
|
+
buildArgv,
|
|
401
|
+
};
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const ALMA_CHAT_SCHEMA = {
|
|
4
|
+
type: 'object',
|
|
5
|
+
properties: {
|
|
6
|
+
message: { type: 'string', description: 'The message for Alma.' },
|
|
7
|
+
},
|
|
8
|
+
required: ['message'],
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
const ALMA_DECISION_SCHEMA = {
|
|
12
|
+
type: 'object',
|
|
13
|
+
properties: {
|
|
14
|
+
approved: { type: 'boolean', description: "The talent's decision on the pending confirmation: true = go ahead, false = cancel." },
|
|
15
|
+
},
|
|
16
|
+
required: ['approved'],
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
function makeAlmaTools(deps = {}) {
|
|
20
|
+
const {
|
|
21
|
+
askAlma = (opts) => require('./alma-client').askAlma({}, opts),
|
|
22
|
+
decideAlma = (opts) => require('./alma-client').decideAlma({}, opts),
|
|
23
|
+
loadAuthSession = require('./auth-session-store').loadAuthSession,
|
|
24
|
+
sessionStatus = require('./auth-session-store').sessionStatus,
|
|
25
|
+
} = deps;
|
|
26
|
+
|
|
27
|
+
const requireSession = () => {
|
|
28
|
+
const session = loadAuthSession();
|
|
29
|
+
if (sessionStatus(session) !== 'active') {
|
|
30
|
+
throw new Error('no active Shakers session — sign in with social_signin_start (or run `shakers login`) first (alma is session-gated).');
|
|
31
|
+
}
|
|
32
|
+
return session;
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
const captureEvents = () => {
|
|
36
|
+
let pending = null;
|
|
37
|
+
const links = [];
|
|
38
|
+
const onEvent = (evt) => {
|
|
39
|
+
if (!evt) return;
|
|
40
|
+
if (evt.type === 'confirmation_required') {
|
|
41
|
+
pending = { text: evt.text || '', irreversible: !!(evt.detail && String(evt.detail).length) };
|
|
42
|
+
} else if (evt.type === 'link' && evt.data && evt.data.to) {
|
|
43
|
+
links.push(String(evt.data.to));
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
return { onEvent, snapshot: () => ({ pending, links }) };
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
const relay = (res, cap) => {
|
|
50
|
+
if (!res.ok) return { ok: false, reason: res.reason };
|
|
51
|
+
const { pending, links } = cap.snapshot();
|
|
52
|
+
const out = { ok: true, answer: res.text };
|
|
53
|
+
if (pending) out.pendingConfirmation = pending;
|
|
54
|
+
if (links.length) out.webLinks = links;
|
|
55
|
+
return out;
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
async function almaChat(args = {}) {
|
|
59
|
+
const session = requireSession();
|
|
60
|
+
const message = typeof args.message === 'string' ? args.message.trim() : '';
|
|
61
|
+
if (!message) return { ok: false, reason: 'no-message' };
|
|
62
|
+
const cap = captureEvents();
|
|
63
|
+
const res = await askAlma({ hubAccessToken: session.hubAccessToken, message, onEvent: cap.onEvent });
|
|
64
|
+
return relay(res, cap);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
async function almaDecision(args = {}) {
|
|
68
|
+
const session = requireSession();
|
|
69
|
+
if (typeof args.approved !== 'boolean') return { ok: false, reason: 'no-decision' };
|
|
70
|
+
const cap = captureEvents();
|
|
71
|
+
const res = await decideAlma({ hubAccessToken: session.hubAccessToken, approved: args.approved, onEvent: cap.onEvent });
|
|
72
|
+
return relay(res, cap);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return [
|
|
76
|
+
{
|
|
77
|
+
name: 'alma_chat',
|
|
78
|
+
title: 'Chat with Alma',
|
|
79
|
+
description: 'Sends your message to Alma, the Shakers assistant, and shows her answer. If she proposes a change to your profile, you are asked before it is made.',
|
|
80
|
+
guidance: "Send a message to Alma, the Shakers AI assistant, and get her answer (about the talent's profile, rate, availability, skills, positions, applications). Continuity is automatic across turns (same session). When Alma proposes a profile change she returns `pendingConfirmation { text, irreversible }` — DO NOT auto-approve: show the talent `pendingConfirmation.text` (and warn when irreversible), ask them yes/no, then call `alma_decision` with their answer. `webLinks` are web-only navigation targets. Relay the answer as-is; never show internal Shakers ids/codes.",
|
|
81
|
+
inputSchema: ALMA_CHAT_SCHEMA,
|
|
82
|
+
handler: almaChat,
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
name: 'alma_decision',
|
|
86
|
+
title: "Confirm Alma's change",
|
|
87
|
+
description: 'Sends your yes or no to a profile change Alma proposed.',
|
|
88
|
+
guidance: "Relay the talent's decision on the pending Alma action from alma_chat: approved=true makes Alma perform the change, approved=false cancels it. Call this ONLY after the talent has explicitly answered yes/no to the pendingConfirmation — never decide on their behalf.",
|
|
89
|
+
inputSchema: ALMA_DECISION_SCHEMA,
|
|
90
|
+
handler: almaDecision,
|
|
91
|
+
},
|
|
92
|
+
];
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
module.exports = { makeAlmaTools, ALMA_CHAT_SCHEMA, ALMA_DECISION_SCHEMA };
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// MCP: the positions the talent applied to + status.
|
|
4
|
+
|
|
5
|
+
const LIST_APPLICATIONS_SCHEMA = { type: 'object', properties: {} };
|
|
6
|
+
|
|
7
|
+
function makeApplicationsTools(deps = {}) {
|
|
8
|
+
const {
|
|
9
|
+
fetchApplications = (opts) => require('./applications-client').fetchApplications({}, opts),
|
|
10
|
+
loadAuthSession = require('./auth-session-store').loadAuthSession,
|
|
11
|
+
sessionStatus = require('./auth-session-store').sessionStatus,
|
|
12
|
+
} = deps;
|
|
13
|
+
|
|
14
|
+
const requireLoginSession = () => {
|
|
15
|
+
const session = loadAuthSession();
|
|
16
|
+
if (sessionStatus(session) !== 'active') {
|
|
17
|
+
throw new Error('no active Shakers session — sign in with social_signin_start (or run `shakers login`) first (list_applications is session-gated).');
|
|
18
|
+
}
|
|
19
|
+
return session;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
async function listApplications() {
|
|
23
|
+
const session = requireLoginSession();
|
|
24
|
+
const res = await fetchApplications({ hubAccessToken: session.hubAccessToken });
|
|
25
|
+
if (!res.ok) return { ok: false, reason: res.reason };
|
|
26
|
+
return { ok: true, applications: res.items || [] };
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return [
|
|
30
|
+
{
|
|
31
|
+
name: 'list_applications',
|
|
32
|
+
title: 'Your applications',
|
|
33
|
+
description: 'Shows the projects you applied to and how each application is going.',
|
|
34
|
+
guidance: "List the positions the talent has applied to, with each candidature's status. Read-only. Requires an active session. Any positionId/projectId in the result are INTERNAL handles for calling tools — never show or mention them to the talent; refer to a project by its title/company.",
|
|
35
|
+
inputSchema: LIST_APPLICATIONS_SCHEMA,
|
|
36
|
+
handler: listApplications,
|
|
37
|
+
},
|
|
38
|
+
];
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
module.exports = {
|
|
42
|
+
makeApplicationsTools,
|
|
43
|
+
LIST_APPLICATIONS_SCHEMA,
|
|
44
|
+
};
|