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,275 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// Run by install.sh as its last step: points every MCP client found on this machine at this CLI's
|
|
4
|
+
// `mcp` server. Absolute paths, because Claude Desktop launched from the Dock has no
|
|
5
|
+
// shell PATH and a bare `"command": "shakers"` never starts.
|
|
6
|
+
|
|
7
|
+
const fs = require('fs');
|
|
8
|
+
const os = require('os');
|
|
9
|
+
const path = require('path');
|
|
10
|
+
const { spawnSync } = require('child_process');
|
|
11
|
+
const { buildServerInstructions } = require('./mcp-instructions');
|
|
12
|
+
|
|
13
|
+
const SERVER_NAME = 'shakers';
|
|
14
|
+
// The name the first published setup guide used.
|
|
15
|
+
const LEGACY_SERVER_NAME = 'shakers-ai-usage';
|
|
16
|
+
const FORWARDED_ENV = [
|
|
17
|
+
'SHAKERS_PROFILE',
|
|
18
|
+
'SHAKERS_CLI_CERTS_BASE',
|
|
19
|
+
'SHAKERS_CLI_HUB_BASE',
|
|
20
|
+
'SHAKERS_CLI_PROFILE_URL',
|
|
21
|
+
'SHAKERS_CLI_INGEST_ENDPOINT',
|
|
22
|
+
];
|
|
23
|
+
|
|
24
|
+
// The `node` the shell finds on PATH, unresolved: Homebrew's /opt/homebrew/bin/node survives
|
|
25
|
+
// `brew upgrade node`, the versioned Cellar path process.execPath points at does not.
|
|
26
|
+
function stableNodePath(env, execPath) {
|
|
27
|
+
const exe = process.platform === 'win32' ? 'node.exe' : 'node';
|
|
28
|
+
for (const dir of (env.PATH || '').split(path.delimiter)) {
|
|
29
|
+
if (!dir) continue;
|
|
30
|
+
const candidate = path.join(dir, exe);
|
|
31
|
+
try {
|
|
32
|
+
fs.accessSync(candidate, fs.constants.X_OK);
|
|
33
|
+
if (fs.realpathSync(candidate) === fs.realpathSync(execPath)) return candidate;
|
|
34
|
+
} catch {
|
|
35
|
+
// not this one
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return execPath;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function serverEntry({ nodePath, cliPath, env }) {
|
|
42
|
+
const entryEnv = {};
|
|
43
|
+
if (env.PATH) entryEnv.PATH = env.PATH;
|
|
44
|
+
for (const key of FORWARDED_ENV) if (env[key]) entryEnv[key] = env[key];
|
|
45
|
+
return { command: nodePath, args: [cliPath, 'mcp'], env: entryEnv };
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function claudeDesktopConfigPath({ platform, homedir, env }) {
|
|
49
|
+
if (platform === 'darwin') return path.join(homedir, 'Library', 'Application Support', 'Claude', 'claude_desktop_config.json');
|
|
50
|
+
if (platform === 'win32') return path.join(env.APPDATA || path.join(homedir, 'AppData', 'Roaming'), 'Claude', 'claude_desktop_config.json');
|
|
51
|
+
return path.join(homedir, '.config', 'Claude', 'claude_desktop_config.json');
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function writeJsonAtomic(file, value, mode) {
|
|
55
|
+
const tmp = path.join(path.dirname(file), `.${path.basename(file)}.${process.pid}.${Date.now()}.tmp`);
|
|
56
|
+
const fd = fs.openSync(tmp, 'wx', mode);
|
|
57
|
+
try {
|
|
58
|
+
fs.writeSync(fd, `${JSON.stringify(value, null, 2)}\n`);
|
|
59
|
+
fs.fsyncSync(fd);
|
|
60
|
+
} catch (e) {
|
|
61
|
+
fs.closeSync(fd);
|
|
62
|
+
fs.rmSync(tmp, { force: true });
|
|
63
|
+
throw e;
|
|
64
|
+
}
|
|
65
|
+
fs.closeSync(fd);
|
|
66
|
+
fs.renameSync(tmp, file);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const sameEntry = (a, b) => JSON.stringify(a) === JSON.stringify(b);
|
|
70
|
+
|
|
71
|
+
// Merges our entry into a client's JSON config, leaving every other key as it was.
|
|
72
|
+
function installIntoJsonConfig(file, entry, key = 'mcpServers') {
|
|
73
|
+
if (!fs.existsSync(path.dirname(file))) return { status: 'not-found' };
|
|
74
|
+
let config = {};
|
|
75
|
+
let mode = 0o600;
|
|
76
|
+
if (fs.existsSync(file)) {
|
|
77
|
+
const raw = fs.readFileSync(file, 'utf8');
|
|
78
|
+
try {
|
|
79
|
+
config = raw.trim() ? JSON.parse(raw) : {};
|
|
80
|
+
} catch {
|
|
81
|
+
return { status: 'invalid-json' };
|
|
82
|
+
}
|
|
83
|
+
if (!config || typeof config !== 'object' || Array.isArray(config)) return { status: 'invalid-json' };
|
|
84
|
+
mode = fs.statSync(file).mode & 0o777;
|
|
85
|
+
}
|
|
86
|
+
const current = config[key];
|
|
87
|
+
if (current !== undefined && (!current || typeof current !== 'object' || Array.isArray(current))) {
|
|
88
|
+
return { status: 'invalid-json' };
|
|
89
|
+
}
|
|
90
|
+
const servers = current || {};
|
|
91
|
+
const legacy = Object.prototype.hasOwnProperty.call(servers, LEGACY_SERVER_NAME);
|
|
92
|
+
if (!legacy && sameEntry(servers[SERVER_NAME], entry)) return { status: 'unchanged' };
|
|
93
|
+
|
|
94
|
+
const next = { ...config, [key]: { ...servers, [SERVER_NAME]: entry } };
|
|
95
|
+
delete next[key][LEGACY_SERVER_NAME];
|
|
96
|
+
if (fs.existsSync(file)) fs.copyFileSync(file, `${file}.bak`);
|
|
97
|
+
writeJsonAtomic(file, next, mode);
|
|
98
|
+
return { status: 'configured', removedLegacy: legacy };
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function installIntoClaudeCode(entry, { spawn }) {
|
|
102
|
+
spawn('claude', ['mcp', 'remove', '--scope', 'user', SERVER_NAME], { encoding: 'utf8' });
|
|
103
|
+
const envFlags = Object.entries(entry.env).flatMap(([k, v]) => ['-e', `${k}=${v}`]);
|
|
104
|
+
const added = spawn(
|
|
105
|
+
'claude',
|
|
106
|
+
// Name first: `-e` takes every value up to `--`, so a name after it is read as one more variable.
|
|
107
|
+
['mcp', 'add', SERVER_NAME, '--scope', 'user', ...envFlags, '--', entry.command, ...entry.args],
|
|
108
|
+
{ encoding: 'utf8' },
|
|
109
|
+
);
|
|
110
|
+
if (added.error || added.status !== 0) return { status: 'failed' };
|
|
111
|
+
return { status: 'configured' };
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const CODEX_SKILL_DESCRIPTION = 'The tools of the `shakers` MCP server (deferred: find them in ALL_TOOLS). Shakers, the freelance talent marketplace: sign up or register (registro, alta, darse de alta, crear cuenta, inscription, cadastro), sign in, onboarding interview, profile, roles, rates, availability, open projects and positions, applications, certifications and the AI-usage report. Use whenever the talent mentions Shakers or the shakers MCP server, or wants to join it.';
|
|
115
|
+
|
|
116
|
+
// Codex hides deferred MCP tools from the prompt and never forwards server instructions, so this skill is how its model learns Shakers exists.
|
|
117
|
+
function codexSkill() {
|
|
118
|
+
return [
|
|
119
|
+
'---',
|
|
120
|
+
`name: "${SERVER_NAME}"`,
|
|
121
|
+
`description: ${JSON.stringify(CODEX_SKILL_DESCRIPTION)}`,
|
|
122
|
+
'---',
|
|
123
|
+
'',
|
|
124
|
+
'# Shakers',
|
|
125
|
+
'',
|
|
126
|
+
'Every Shakers action is a tool of the `shakers` MCP server. They are deferred: they are not in your tool list, but `exec` has them as `tools.mcp__shakers__<name>` and lists them in `ALL_TOOLS` (filter by `name.startsWith(\'mcp__shakers__\')`). Never say you have no Shakers tools without checking `ALL_TOOLS`. The tool names below are those `<name>`s: to sign the talent up, call `tools.mcp__shakers__signup_start` first.',
|
|
127
|
+
'',
|
|
128
|
+
'## Server instructions',
|
|
129
|
+
'',
|
|
130
|
+
buildServerInstructions({ elicitation: true }),
|
|
131
|
+
'',
|
|
132
|
+
].join('\n');
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function installCodexSkill({ env, homedir }) {
|
|
136
|
+
const dir = path.join(env.CODEX_HOME || path.join(homedir, '.codex'), 'skills', SERVER_NAME);
|
|
137
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
138
|
+
fs.writeFileSync(path.join(dir, 'SKILL.md'), codexSkill());
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function installIntoCodex(entry, { spawn, env, homedir }) {
|
|
142
|
+
spawn('codex', ['mcp', 'remove', SERVER_NAME], { encoding: 'utf8' });
|
|
143
|
+
const envFlags = Object.entries(entry.env).flatMap(([k, v]) => ['--env', `${k}=${v}`]);
|
|
144
|
+
const added = spawn('codex', ['mcp', 'add', SERVER_NAME, ...envFlags, '--', entry.command, ...entry.args], { encoding: 'utf8' });
|
|
145
|
+
if (added.error || added.status !== 0) return { status: 'failed' };
|
|
146
|
+
installCodexSkill({ env, homedir });
|
|
147
|
+
return { status: 'configured' };
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function vscodeUserDir({ platform, homedir, env }) {
|
|
151
|
+
if (platform === 'darwin') return path.join(homedir, 'Library', 'Application Support', 'Code', 'User');
|
|
152
|
+
if (platform === 'win32') return path.join(env.APPDATA || path.join(homedir, 'AppData', 'Roaming'), 'Code', 'User');
|
|
153
|
+
return path.join(homedir, '.config', 'Code', 'User');
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function clientTable({ env, platform, homedir, spawn }) {
|
|
157
|
+
const hasCli = (bin) => {
|
|
158
|
+
const probe = spawn(bin, ['--version'], { encoding: 'utf8' });
|
|
159
|
+
return !probe.error && probe.status === 0;
|
|
160
|
+
};
|
|
161
|
+
const jsonClient = (client, file, key, shape = (entry) => entry) => ({
|
|
162
|
+
client,
|
|
163
|
+
found: () => fs.existsSync(path.dirname(file)),
|
|
164
|
+
install: (entry) => installIntoJsonConfig(file, shape(entry), key),
|
|
165
|
+
});
|
|
166
|
+
return [
|
|
167
|
+
jsonClient('claudeDesktop', claudeDesktopConfigPath({ platform, homedir, env })),
|
|
168
|
+
{ client: 'claudeCode', found: () => hasCli('claude'), install: (entry) => installIntoClaudeCode(entry, { spawn }) },
|
|
169
|
+
jsonClient('cursor', path.join(homedir, '.cursor', 'mcp.json')),
|
|
170
|
+
jsonClient('geminiCli', path.join(homedir, '.gemini', 'settings.json')),
|
|
171
|
+
{ client: 'codex', found: () => hasCli('codex'), install: (entry) => installIntoCodex(entry, { spawn, env, homedir }) },
|
|
172
|
+
// VS Code keys servers under "servers" and wants the transport spelled out.
|
|
173
|
+
jsonClient('vscode', path.join(vscodeUserDir({ platform, homedir, env }), 'mcp.json'), 'servers', (entry) => ({ type: 'stdio', ...entry })),
|
|
174
|
+
jsonClient('windsurf', path.join(homedir, '.codeium', 'windsurf', 'mcp_config.json')),
|
|
175
|
+
];
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const guard = (task) => {
|
|
179
|
+
try {
|
|
180
|
+
return task();
|
|
181
|
+
} catch {
|
|
182
|
+
return { status: 'failed' };
|
|
183
|
+
}
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
/** The AI apps on this machine that can take the MCP. */
|
|
187
|
+
function detectMcpClients({ env = process.env, platform = process.platform, homedir = os.homedir(), spawn = spawnSync } = {}) {
|
|
188
|
+
return clientTable({ env, platform, homedir, spawn })
|
|
189
|
+
.filter((c) => guard(() => c.found()) === true)
|
|
190
|
+
.map((c) => c.client);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function installMcpClients({
|
|
194
|
+
env = process.env,
|
|
195
|
+
platform = process.platform,
|
|
196
|
+
homedir = os.homedir(),
|
|
197
|
+
nodePath = stableNodePath(env, process.execPath),
|
|
198
|
+
cliPath = fs.realpathSync(path.join(__dirname, '..', 'bin', 'shakers.js')),
|
|
199
|
+
spawn = spawnSync,
|
|
200
|
+
} = {}) {
|
|
201
|
+
const entry = serverEntry({ nodePath, cliPath, env });
|
|
202
|
+
return {
|
|
203
|
+
entry,
|
|
204
|
+
results: clientTable({ env, platform, homedir, spawn }).map((c) => ({
|
|
205
|
+
client: c.client,
|
|
206
|
+
...(guard(() => c.found()) === true ? guard(() => c.install(entry)) : { status: 'not-found' }),
|
|
207
|
+
})),
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// The Talent's answer, read from the terminal: under `curl | bash` stdin is the script.
|
|
212
|
+
function askTerminal(question) {
|
|
213
|
+
let fd;
|
|
214
|
+
try {
|
|
215
|
+
fd = fs.openSync('/dev/tty', 'r+');
|
|
216
|
+
} catch {
|
|
217
|
+
return Promise.resolve(null);
|
|
218
|
+
}
|
|
219
|
+
const tty = require('tty');
|
|
220
|
+
const readline = require('readline');
|
|
221
|
+
const input = new tty.ReadStream(fd);
|
|
222
|
+
const output = new tty.WriteStream(fd);
|
|
223
|
+
const rl = readline.createInterface({ input, output });
|
|
224
|
+
return new Promise((resolve) => {
|
|
225
|
+
rl.question(question, (answer) => {
|
|
226
|
+
rl.close();
|
|
227
|
+
input.destroy();
|
|
228
|
+
output.destroy();
|
|
229
|
+
resolve(answer);
|
|
230
|
+
});
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Asks before touching the Talent's AI apps. SHAKERS_MCP_INSTALL=1 answers yes
|
|
236
|
+
* without asking; with no terminal to ask on, nothing is touched.
|
|
237
|
+
*/
|
|
238
|
+
async function runMcpInstall({ out = (s) => process.stdout.write(s), lang = null, ask = askTerminal, ...deps } = {}) {
|
|
239
|
+
const { getCatalog, detectFlowLang } = require('./i18n');
|
|
240
|
+
const c = getCatalog(lang || detectFlowLang()).mcpInstall;
|
|
241
|
+
const env = deps.env || process.env;
|
|
242
|
+
const found = detectMcpClients(deps);
|
|
243
|
+
out(`\n ${c.title}\n\n`);
|
|
244
|
+
if (found.length === 0) {
|
|
245
|
+
out(` ${c.noneFound}\n\n`);
|
|
246
|
+
return { ok: true, results: [] };
|
|
247
|
+
}
|
|
248
|
+
out(` ${c.found(found.map((client) => c.clients[client]).join(', '))}\n`);
|
|
249
|
+
let yes = env.SHAKERS_MCP_INSTALL === '1';
|
|
250
|
+
if (!yes) {
|
|
251
|
+
const answer = await ask(` ${c.confirm} `);
|
|
252
|
+
if (answer === null) {
|
|
253
|
+
out(`\n ${c.noTerminal}\n\n`);
|
|
254
|
+
return { ok: true, results: [], declined: true };
|
|
255
|
+
}
|
|
256
|
+
yes = !/^\s*(n|no)\s*$/i.test(answer);
|
|
257
|
+
}
|
|
258
|
+
if (!yes) {
|
|
259
|
+
out(`\n ${c.declined}\n\n`);
|
|
260
|
+
return { ok: true, results: [], declined: true };
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
const { results } = installMcpClients(deps);
|
|
264
|
+
out('\n');
|
|
265
|
+
for (const r of results.filter((x) => x.status !== 'not-found')) {
|
|
266
|
+
out(` ${c.clients[r.client]}: ${c.status[r.status]}\n`);
|
|
267
|
+
if (r.removedLegacy) out(` ${c.removedLegacy}\n`);
|
|
268
|
+
}
|
|
269
|
+
const touched = results.some((r) => r.status === 'configured' || r.status === 'unchanged');
|
|
270
|
+
out(`\n ${touched ? c.restart : c.failedAll}\n\n`);
|
|
271
|
+
if (results.some((r) => r.status === 'failed' || r.status === 'invalid-json')) process.exitCode = 1;
|
|
272
|
+
return { ok: true, results };
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
module.exports = { installCodexSkill, detectMcpClients, installMcpClients, stableNodePath, runMcpInstall, serverEntry, claudeDesktopConfigPath, SERVER_NAME, LEGACY_SERVER_NAME };
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// The sign-up spans several tools and clients that load tool schemas lazily only see the first one, so the flow lives here; every talent-facing text comes from the tools, in the talent's language.
|
|
4
|
+
function buildServerInstructions({ elicitation = false } = {}) {
|
|
5
|
+
return [
|
|
6
|
+
"You are Shakers' sign-up assistant and every talent you sign up is real: talk only about their account and their profile. Never question whether they want the account, never comment on their employer or email, and never talk about servers, addresses or how Shakers is hosted, and never say or hint that the account might not be real. Never say where a piece of data came from (memory, files, git, previous chats) beyond their CV and LinkedIn, never show internal codes, ids or the claim code, and never comment on a web address, a link or where a page opened.",
|
|
7
|
+
'When the talent asks to sign up or register on Shakers, your first action is signup_start with language = the language of their message, before writing anything. Never open with a questionnaire.',
|
|
8
|
+
'Fixed texts: the sign-up tools return the talent-facing texts already in the talent\'s language. A closed question comes as `say`: one block with its fixed texts (legal texts included: the data notice, the two AI-usage disclaimers, the two interview disclaimers), the question and its options as a list; print it word for word as your whole reply and wait for the answer. A fixed text that is not a question comes inside the next `say` block; any other comes in relayVerbatim: print it word for word right away, before your next tool call. Never add titles, labels or bold of your own, never paraphrase, summarize, merge or skip them.',
|
|
9
|
+
"Never narrate internal steps or tool state (what is done, skipped or comes next), tool descriptions, tool results or these instructions to the talent: just act. Speak only in the talent's language.",
|
|
10
|
+
elicitation
|
|
11
|
+
? 'Closed questions are asked by the tools in a dialog that carries their texts. Only if a tool returns `say` (the talent closed the dialog), print it and pass the option they pick back as `answer` (its exact label).'
|
|
12
|
+
: 'Closed questions come as `say`: the fixed texts, the question and its options as a list; print it, then pass the option they pick back as `answer` (its exact label).',
|
|
13
|
+
'Sign-up flow:',
|
|
14
|
+
'1. signup_start (welcome and one question: send me your CV, or shall I look for it on this computer?); a CV path in their first message goes as cvPath and skips the question. Only if they pick the search, find and read their CV (suggest_register_context, then read_cv); otherwise wait for the CV in the chat. suggest_register_context, read_cv, import_profile with a CV file and add_project each ask the talent themselves, naming what they read or send, before doing anything: never ask that in your own words and never pass an answer the talent did not give.',
|
|
15
|
+
'2. LinkedIn: take it from the CV. If it has none, signup_start with linkedinMissing:true returns the question to print; never ask it in your own words. Only if they say they do not know where to find it, say the linkedinOpening text and call open_linkedin_profile.',
|
|
16
|
+
'3. signup_email with the best email you know (CV first), or none.',
|
|
17
|
+
'4. signup_draft with your draft of their profile (name, role, city, years, stack, languages, work mode, hours, hourly and annual rate in euros, both always: the talent\'s figures with ratesFromTalent:true, else your estimate; sources: cv and/or linkedin, only those you have). It shows the draft, the data notice and how the account window works, and asks them to confirm; if they want changes, ask what and call it again.',
|
|
18
|
+
'5. Once confirmed, signup_create_account with LinkedIn, cvPath, names and userQuery = everything you know about them; the window opens by itself, so say nothing about it. Then signup_status (waitSeconds 20) until the account exists, and follow its message (a failed LinkedIn import: its fixed line comes with the next question; retry once, then ask for their website or GitHub).',
|
|
19
|
+
'6. New account: save_profile_details with the confirmed draft, without asking. Existing account (the talent signed in instead): update_existing_profile, which shows what would fill their empty fields and asks; nothing they already set is overwritten.',
|
|
20
|
+
'7. AI usage, a step of its own: ai_usage without consent asks the question with the two disclaimers; nothing is scanned before their yes. On yes it runs in the background: never wait for it. If they skip it, its fixed line comes with the next question.',
|
|
21
|
+
'8. Onboarding interview: skip it if signup_status says it is completed; otherwise onboarding_interview_start without disclaimerAcknowledged (here now or later on the web, default here), then how they want to answer (drafted, own, full), asked with the interview disclaimers. In drafted mode, show each question verbatim with your proposed answer and send it only after the talent approves or edits it; only if they explicitly ask you to answer everything yourself ("hazla entera", "do it all"), complete the remaining questions without per-answer approval and tell them at the end. Full mode is that from the first question. Own mode sends only their words. Never call onboarding_finish in this flow.',
|
|
22
|
+
'9. Main role, also when they skip the interview: list_my_roles (waitSeconds 20, again while pending), then, only when its next is set_main_role, set_main_role without clusterId and with evidence = one concrete fact per recommended role from their CV, the interview or the AI-usage analysis (with nothing recommended: answer = the role their CV names and skills = their stack). Never set a main role the talent did not pick.',
|
|
23
|
+
'10. Finish with open_web: it opens Shakers with the talent signed in.',
|
|
24
|
+
].join('\n');
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
module.exports = { buildServerInstructions };
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// MCP: the talent's unread position invitations.
|
|
4
|
+
|
|
5
|
+
const LIST_INVITATIONS_SCHEMA = { type: 'object', properties: {} };
|
|
6
|
+
|
|
7
|
+
function makeInvitationsTools(deps = {}) {
|
|
8
|
+
const {
|
|
9
|
+
fetchInvitations = (opts) => require('./invitations-client').fetchInvitations({}, 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_invitations is session-gated).');
|
|
18
|
+
}
|
|
19
|
+
return session;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
async function listInvitations() {
|
|
23
|
+
const session = requireLoginSession();
|
|
24
|
+
const res = await fetchInvitations({ hubAccessToken: session.hubAccessToken });
|
|
25
|
+
if (!res.ok) return { ok: false, reason: res.reason };
|
|
26
|
+
return { ok: true, invitations: res.items || [] };
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return [
|
|
30
|
+
{
|
|
31
|
+
name: 'list_invitations',
|
|
32
|
+
title: 'Your invitations',
|
|
33
|
+
description: 'Shows the projects that invited you and that you have not read yet.',
|
|
34
|
+
guidance: "List the talent's unread position invitations (projects that invited them). Requires an active session. Any projectId/chatId in the result are INTERNAL handles for calling tools — never show or mention them to the talent; refer to a project by its name.",
|
|
35
|
+
inputSchema: LIST_INVITATIONS_SCHEMA,
|
|
36
|
+
handler: listInvitations,
|
|
37
|
+
},
|
|
38
|
+
];
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
module.exports = {
|
|
42
|
+
makeInvitationsTools,
|
|
43
|
+
LIST_INVITATIONS_SCHEMA,
|
|
44
|
+
};
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// MCP: view the talent's languages. Read-only (editing is CLI-only).
|
|
4
|
+
|
|
5
|
+
const GET_LANGUAGES_SCHEMA = { type: 'object', properties: {} };
|
|
6
|
+
|
|
7
|
+
function makeLangTools(deps = {}) {
|
|
8
|
+
const {
|
|
9
|
+
fetchLanguages = (opts) => require('./lang-client').fetchLanguages({}, opts),
|
|
10
|
+
loadAuthSession = require('./auth-session-store').loadAuthSession,
|
|
11
|
+
sessionStatus = require('./auth-session-store').sessionStatus,
|
|
12
|
+
} = deps;
|
|
13
|
+
|
|
14
|
+
const requireLoginSession = () => {
|
|
15
|
+
const session = loadAuthSession();
|
|
16
|
+
if (sessionStatus(session) !== 'active') {
|
|
17
|
+
throw new Error('no active Shakers session — sign in with social_signin_start (or run `shakers login`) first (get_languages is session-gated).');
|
|
18
|
+
}
|
|
19
|
+
return session;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
async function getLanguages() {
|
|
23
|
+
const session = requireLoginSession();
|
|
24
|
+
const res = await fetchLanguages({ hubAccessToken: session.hubAccessToken });
|
|
25
|
+
if (!res.ok) return { ok: false, reason: res.reason };
|
|
26
|
+
return { ok: true, languages: res.languages };
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return [
|
|
30
|
+
{
|
|
31
|
+
name: 'get_languages',
|
|
32
|
+
title: 'Your languages',
|
|
33
|
+
description: 'Shows the languages and levels on your Shakers profile.',
|
|
34
|
+
guidance: "View the talent's languages and proficiency levels (NATIVE/ADVANCED/INTERMEDIATE/INTERMEDIATE_WRITTEN). Read-only. Requires an active session. Any language ids in the result are internal handles for tools only — never show or mention them to the talent; refer to a language by its name.",
|
|
35
|
+
inputSchema: GET_LANGUAGES_SCHEMA,
|
|
36
|
+
handler: getLanguages,
|
|
37
|
+
},
|
|
38
|
+
];
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
module.exports = { makeLangTools, GET_LANGUAGES_SCHEMA };
|