shakers-cli 0.15.0 → 0.17.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.
- package/README.md +56 -1203
- package/bin/ai-usage.js +32 -58
- package/bin/login.js +10 -10
- package/bin/mcp.js +17 -12
- package/bin/mcpb.js +1 -1
- package/bin/onboarding.js +3 -2
- package/bin/register.js +19 -5
- package/bin/report-html.js +2 -2
- package/bin/shakers-cli.js +352 -0
- package/bin/shakers.js +20 -344
- package/bin/share.js +1 -1
- package/bin/superadmin.js +5 -5
- package/bin/version.js +3 -0
- package/package.json +12 -3
- package/src/account-marker.js +40 -0
- package/src/account-trace.js +23 -0
- package/src/agent-category.js +1 -1
- package/src/agent-evaluation.js +3 -3
- package/src/agent-org-chart.js +5 -5
- package/src/agent-synthesis.js +3 -3
- package/src/agent-usage.js +1 -1
- package/src/agents-client.js +4 -4
- package/src/ai-usage-review.js +101 -0
- package/src/ai.js +1 -1
- package/src/ansi.js +1 -1
- package/src/auth-client.js +18 -4
- package/src/auth-method-picker.js +1 -1
- package/src/auth-methods.js +1 -1
- package/src/auth-session-store.js +18 -8
- package/src/authorship-mismatch.js +1 -1
- package/src/authorship.js +2 -2
- package/src/automations-detector.js +1 -1
- package/src/backend-request.js +5 -3
- package/src/brand-ansi.js +1 -1
- package/src/browser-tools-detector.js +1 -1
- package/src/certify-disclaimer.js +1 -1
- package/src/certify-remediation-prompt.js +1 -1
- package/src/cli-args.js +2 -2
- package/src/command-audit.js +8 -5
- package/src/config-dir.js +1 -1
- package/src/config.js +114 -48
- package/src/consent-flow.js +7 -6
- package/src/consent-skip.js +1 -1
- package/src/detection-evidence.js +2 -2
- package/src/detection-noise.js +1 -1
- package/src/detectors.js +1 -1
- package/src/device-login.js +12 -1
- package/src/email-verification.js +3 -3
- package/src/env-paths.js +1 -1
- package/src/find-projects-client.js +12 -0
- package/src/find-projects-flow.js +1 -13
- package/src/git-activity.js +1 -1
- package/src/graph-certs.js +3 -3
- package/src/graph-scan.js +9 -9
- package/src/i18n.js +148 -138
- package/src/interactive-select.js +1 -1
- package/src/interview-client.js +6 -1
- package/src/interview-transcript.js +1 -1
- package/src/legal-notice.js +1 -1
- package/src/maturity.js +6 -6
- package/src/mcp-ai-usage-result-tool.js +34 -12
- package/src/mcp-ai-usage-tool.js +282 -163
- package/src/mcp-auth-form.js +7 -3
- package/src/mcp-detector.js +3 -3
- package/src/mcp-device-login-tools.js +2 -1
- package/src/mcp-find-projects-tools.js +1 -2
- package/src/mcp-install.js +279 -44
- package/src/mcp-instructions.js +1 -1
- package/src/mcp-onboarding-tools.js +4 -4
- package/src/mcp-prompts.js +4 -4
- package/src/mcp-register-tools.js +1 -0
- package/src/mcp-role-tools.js +24 -1
- package/src/mcp-server.js +5 -1
- package/src/mcp-signup-client.js +2 -2
- package/src/mcp-signup-tools.js +39 -9
- package/src/memory-structure-detector.js +1 -1
- package/src/model-call-record.js +2 -2
- package/src/onboarding-flow.js +63 -42
- package/src/onboarding-funnel.js +153 -20
- package/src/open-file.js +1 -1
- package/src/osc-link.js +1 -1
- package/src/portfolio-client.js +11 -1
- package/src/portfolio-git-info.js +1 -1
- package/src/profile-flow.js +1 -1
- package/src/profile-switch.js +5 -12
- package/src/render-certification.js +10 -10
- package/src/render-html.js +6 -6
- package/src/render-report-text.js +1 -1
- package/src/render-sheet.js +10 -10
- package/src/render-terminal.js +6 -6
- package/src/repo-scope.js +132 -0
- package/src/report-gating.js +5 -3
- package/src/report-store.js +13 -8
- package/src/roadmap-content.js +5 -5
- package/src/roadmap-personalization.js +3 -3
- package/src/roadmap-prompt.js +2 -2
- package/src/roles-flow.js +8 -5
- package/src/scan-exclusions.js +1 -1
- package/src/scanner.js +14 -14
- package/src/secret-prompt.js +1 -1
- package/src/session-gate.js +1 -1
- package/src/session-refresh.js +33 -3
- package/src/session-scan.js +1 -1
- package/src/shakers-wordmark.js +1 -1
- package/src/share-card.js +2 -2
- package/src/share-certification.js +4 -4
- package/src/share-consent-state.js +9 -9
- package/src/share-payload.js +7 -7
- package/src/share.js +7 -7
- package/src/signup-client.js +1 -1
- package/src/signup-copy.js +177 -2
- package/src/skills-client.js +1 -1
- package/src/start-add-agents.js +2 -2
- package/src/start-add-portfolio.js +3 -13
- package/src/start-add-skills.js +2 -2
- package/src/stdin-ask.js +16 -4
- package/src/strip-internal-comments.js +1 -1
- package/src/tech-detector.js +2 -2
- package/src/templates/report-sheet.html +11 -13
- package/src/terminal-agents.js +8 -8
- package/src/terminal-format.js +3 -3
- package/src/terminal-sections.js +11 -11
- package/src/tier-engine.js +3 -3
- package/src/tier-projection.js +2 -2
- package/src/transcript-turns.js +1 -1
- package/src/typewriter.js +1 -1
- package/src/usage-model-calls.js +6 -6
- package/src/usage-oneshot-commands.js +5 -5
- package/src/usage-repo-scope.js +11 -129
- package/src/usage-run-steps.js +82 -181
- package/src/usage-scan.js +235 -0
- package/src/work-streams.js +1 -1
|
@@ -3,8 +3,7 @@
|
|
|
3
3
|
// MCP: list the Positions/Projects available to the talent (Find Positions),
|
|
4
4
|
// paginated + filtered server-side. Same params as the CLI so an agent can page.
|
|
5
5
|
|
|
6
|
-
const { TABS, ATTENDANCE, DEFAULT_LIMIT } = require('./find-projects-client');
|
|
7
|
-
const { splitRecommended } = require('./find-projects-flow');
|
|
6
|
+
const { TABS, ATTENDANCE, DEFAULT_LIMIT, splitRecommended } = require('./find-projects-client');
|
|
8
7
|
|
|
9
8
|
const FIND_PROJECTS_SCHEMA = {
|
|
10
9
|
type: 'object',
|
package/src/mcp-install.js
CHANGED
|
@@ -38,11 +38,57 @@ function stableNodePath(env, execPath) {
|
|
|
38
38
|
return execPath;
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
-
|
|
41
|
+
const pathFor = (platform) => (platform === 'win32' ? path.win32 : path.posix);
|
|
42
|
+
|
|
43
|
+
function realpathOr(dir) {
|
|
44
|
+
try {
|
|
45
|
+
return fs.realpathSync(dir);
|
|
46
|
+
} catch {
|
|
47
|
+
return dir;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function insideNpxCache(file, { env, platform, homedir }) {
|
|
52
|
+
const p = pathFor(platform);
|
|
53
|
+
const defaultCache = platform === 'win32'
|
|
54
|
+
? p.join(env.LOCALAPPDATA || p.join(homedir, 'AppData', 'Local'), 'npm-cache')
|
|
55
|
+
: p.join(homedir, '.npm');
|
|
56
|
+
return [env.npm_config_cache, defaultCache].filter(Boolean).flatMap((dir) => [dir, realpathOr(dir)]).some((dir) => {
|
|
57
|
+
const rel = p.relative(p.join(dir, '_npx'), file);
|
|
58
|
+
return rel !== '' && !rel.startsWith('..') && !p.isAbsolute(rel);
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function withoutNpxBins(value, where) {
|
|
63
|
+
const p = pathFor(where.platform);
|
|
64
|
+
return value.split(p.delimiter).filter((dir) => {
|
|
65
|
+
const parts = p.normalize(dir).split(p.sep);
|
|
66
|
+
const [last, parent] = [parts[parts.length - 1], parts[parts.length - 2]];
|
|
67
|
+
return !(last === 'node-gyp-bin' || (last === '.bin' && parent === 'node_modules') || insideNpxCache(dir, where));
|
|
68
|
+
}).join(p.delimiter);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function npxNextTo(nodePath) {
|
|
72
|
+
const candidate = path.join(path.dirname(nodePath), 'npx');
|
|
73
|
+
try {
|
|
74
|
+
fs.accessSync(candidate, fs.constants.X_OK);
|
|
75
|
+
return candidate;
|
|
76
|
+
} catch {
|
|
77
|
+
return 'npx';
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function serverEntry({ nodePath, cliPath, env, platform = process.platform, homedir = os.homedir(), packageName }) {
|
|
82
|
+
const where = { env, platform, homedir };
|
|
83
|
+
const viaNpx = insideNpxCache(cliPath, where);
|
|
42
84
|
const entryEnv = {};
|
|
43
|
-
|
|
85
|
+
const pathValue = env.PATH && viaNpx ? withoutNpxBins(env.PATH, where) : env.PATH;
|
|
86
|
+
if (pathValue) entryEnv.PATH = pathValue;
|
|
44
87
|
for (const key of FORWARDED_ENV) if (env[key]) entryEnv[key] = env[key];
|
|
45
|
-
return { command: nodePath, args: [cliPath, 'mcp'], env: entryEnv };
|
|
88
|
+
if (!viaNpx) return { command: nodePath, args: [cliPath, 'mcp'], env: entryEnv };
|
|
89
|
+
const npxArgs = ['-y', packageName, 'mcp'];
|
|
90
|
+
if (platform === 'win32') return { command: 'cmd', args: ['/c', 'npx', ...npxArgs], env: entryEnv };
|
|
91
|
+
return { command: npxNextTo(nodePath), args: npxArgs, env: entryEnv };
|
|
46
92
|
}
|
|
47
93
|
|
|
48
94
|
function claudeDesktopConfigPath({ platform, homedir, env }) {
|
|
@@ -51,11 +97,11 @@ function claudeDesktopConfigPath({ platform, homedir, env }) {
|
|
|
51
97
|
return path.join(homedir, '.config', 'Claude', 'claude_desktop_config.json');
|
|
52
98
|
}
|
|
53
99
|
|
|
54
|
-
function
|
|
100
|
+
function writeFileAtomic(file, text, mode) {
|
|
55
101
|
const tmp = path.join(path.dirname(file), `.${path.basename(file)}.${process.pid}.${Date.now()}.tmp`);
|
|
56
102
|
const fd = fs.openSync(tmp, 'wx', mode);
|
|
57
103
|
try {
|
|
58
|
-
fs.writeSync(fd,
|
|
104
|
+
fs.writeSync(fd, text);
|
|
59
105
|
fs.fsyncSync(fd);
|
|
60
106
|
} catch (e) {
|
|
61
107
|
fs.closeSync(fd);
|
|
@@ -78,14 +124,14 @@ function installIntoJsonConfig(file, entry, key = 'mcpServers') {
|
|
|
78
124
|
try {
|
|
79
125
|
config = raw.trim() ? JSON.parse(raw) : {};
|
|
80
126
|
} catch {
|
|
81
|
-
return { status: 'invalid-
|
|
127
|
+
return { status: 'invalid-config' };
|
|
82
128
|
}
|
|
83
|
-
if (!config || typeof config !== 'object' || Array.isArray(config)) return { status: 'invalid-
|
|
129
|
+
if (!config || typeof config !== 'object' || Array.isArray(config)) return { status: 'invalid-config' };
|
|
84
130
|
mode = fs.statSync(file).mode & 0o777;
|
|
85
131
|
}
|
|
86
132
|
const current = config[key];
|
|
87
133
|
if (current !== undefined && (!current || typeof current !== 'object' || Array.isArray(current))) {
|
|
88
|
-
return { status: 'invalid-
|
|
134
|
+
return { status: 'invalid-config' };
|
|
89
135
|
}
|
|
90
136
|
const servers = current || {};
|
|
91
137
|
const legacy = Object.prototype.hasOwnProperty.call(servers, LEGACY_SERVER_NAME);
|
|
@@ -94,19 +140,156 @@ function installIntoJsonConfig(file, entry, key = 'mcpServers') {
|
|
|
94
140
|
const next = { ...config, [key]: { ...servers, [SERVER_NAME]: entry } };
|
|
95
141
|
delete next[key][LEGACY_SERVER_NAME];
|
|
96
142
|
if (fs.existsSync(file)) fs.copyFileSync(file, `${file}.bak`);
|
|
97
|
-
|
|
143
|
+
writeFileAtomic(file, `${JSON.stringify(next, null, 2)}\n`, mode);
|
|
98
144
|
return { status: 'configured', removedLegacy: legacy };
|
|
99
145
|
}
|
|
100
146
|
|
|
147
|
+
const TOML_ESCAPES = { '"': '\\"', '\\': '\\\\', '\b': '\\b', '\t': '\\t', '\n': '\\n', '\f': '\\f', '\r': '\\r' };
|
|
148
|
+
const tomlString = (s) => `"${String(s).replace(/["\\\u0000-\u001f\u007f]/g, (ch) => TOML_ESCAPES[ch] || `\\u${ch.charCodeAt(0).toString(16).padStart(4, '0')}`)}"`;
|
|
149
|
+
const tomlKey = (k) => (/^[A-Za-z0-9_-]+$/.test(k) ? k : tomlString(k));
|
|
150
|
+
|
|
151
|
+
function tomlServerBlock(entry, eol = '\n') {
|
|
152
|
+
const env = Object.entries(entry.env || {});
|
|
153
|
+
return [
|
|
154
|
+
`[mcp_servers.${SERVER_NAME}]`,
|
|
155
|
+
`command = ${tomlString(entry.command)}`,
|
|
156
|
+
`args = [${entry.args.map(tomlString).join(', ')}]`,
|
|
157
|
+
...(env.length ? [`env = { ${env.map(([k, v]) => `${tomlKey(k)} = ${tomlString(v)}`).join(', ')} }`] : []),
|
|
158
|
+
'startup_timeout_sec = 30',
|
|
159
|
+
'',
|
|
160
|
+
].join(eol);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function parseTomlKeyPath(text) {
|
|
164
|
+
const keys = [];
|
|
165
|
+
let rest = text.trim();
|
|
166
|
+
while (rest) {
|
|
167
|
+
const m = rest.match(/^(?:([A-Za-z0-9_-]+)|"((?:[^"\\]|\\.)*)"|'([^']*)')\s*/);
|
|
168
|
+
if (!m) return null;
|
|
169
|
+
keys.push(m[1] ?? (m[2] !== undefined ? JSON.parse(`"${m[2]}"`) : m[3]));
|
|
170
|
+
rest = rest.slice(m[0].length);
|
|
171
|
+
if (!rest) break;
|
|
172
|
+
if (rest[0] !== '.') return null;
|
|
173
|
+
rest = rest.slice(1).trimStart();
|
|
174
|
+
if (!rest) return null;
|
|
175
|
+
}
|
|
176
|
+
return keys.length ? keys : null;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function scanTomlValue(line, state) {
|
|
180
|
+
let i = 0;
|
|
181
|
+
while (i < line.length) {
|
|
182
|
+
if (state.multiline) {
|
|
183
|
+
const end = line.indexOf(state.multiline, i);
|
|
184
|
+
if (end === -1) return;
|
|
185
|
+
i = end + 3;
|
|
186
|
+
state.multiline = null;
|
|
187
|
+
continue;
|
|
188
|
+
}
|
|
189
|
+
const ch = line[i];
|
|
190
|
+
if (ch === '#') return;
|
|
191
|
+
if (line.startsWith('"""', i) || line.startsWith("'''", i)) {
|
|
192
|
+
state.multiline = line.slice(i, i + 3);
|
|
193
|
+
i += 3;
|
|
194
|
+
} else if (ch === '"' || ch === "'") {
|
|
195
|
+
i += 1;
|
|
196
|
+
while (i < line.length && line[i] !== ch) i += ch === '"' && line[i] === '\\' ? 2 : 1;
|
|
197
|
+
i += 1;
|
|
198
|
+
} else {
|
|
199
|
+
if (ch === '[' || ch === '{') state.depth += 1;
|
|
200
|
+
if (ch === ']' || ch === '}') state.depth -= 1;
|
|
201
|
+
i += 1;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
const OUR_TOML_NAMES = [SERVER_NAME, LEGACY_SERVER_NAME];
|
|
207
|
+
const isOurs = (keys) => keys.length >= 2 && keys[0] === 'mcp_servers' && OUR_TOML_NAMES.includes(keys[1]);
|
|
208
|
+
|
|
209
|
+
function removeTomlServers(text) {
|
|
210
|
+
const lines = text.match(/[^\n]*\n|[^\n]+$/g) || [];
|
|
211
|
+
const state = { multiline: null, depth: 0 };
|
|
212
|
+
const kept = [];
|
|
213
|
+
let table = [];
|
|
214
|
+
let ours = false;
|
|
215
|
+
let tail = [];
|
|
216
|
+
let removedLegacy = false;
|
|
217
|
+
const flushTail = () => {
|
|
218
|
+
const firstComment = tail.findIndex((l) => l.trim().startsWith('#'));
|
|
219
|
+
if (firstComment !== -1) kept.push(...tail.slice(firstComment));
|
|
220
|
+
tail = [];
|
|
221
|
+
};
|
|
222
|
+
for (const line of lines) {
|
|
223
|
+
const bare = line.replace(/\r?\n$/, '');
|
|
224
|
+
const trimmed = bare.trim();
|
|
225
|
+
if (!state.multiline && state.depth === 0 && trimmed.startsWith('[')) {
|
|
226
|
+
const header = trimmed.match(/^(\[\[?)\s*(.*?)\s*(\]\]?)\s*(#.*)?$/);
|
|
227
|
+
const keys = header && header[1].length === header[3].length ? parseTomlKeyPath(header[2]) : null;
|
|
228
|
+
if (!keys) return null;
|
|
229
|
+
if (header[1] === '[[' && keys[0] === 'mcp_servers' && (keys.length === 1 || isOurs(keys))) return null;
|
|
230
|
+
if (isOurs(keys)) {
|
|
231
|
+
if (keys[1] === LEGACY_SERVER_NAME) removedLegacy = true;
|
|
232
|
+
ours = true;
|
|
233
|
+
tail = [];
|
|
234
|
+
continue;
|
|
235
|
+
}
|
|
236
|
+
if (ours) flushTail();
|
|
237
|
+
ours = false;
|
|
238
|
+
table = keys;
|
|
239
|
+
kept.push(line);
|
|
240
|
+
continue;
|
|
241
|
+
}
|
|
242
|
+
if (!state.multiline && state.depth === 0 && trimmed && !trimmed.startsWith('#')) {
|
|
243
|
+
const eq = bare.match(/^\s*((?:[A-Za-z0-9_.\s-]|"(?:[^"\\]|\\.)*"|'[^']*')+?)\s*=/);
|
|
244
|
+
const keys = eq && parseTomlKeyPath(eq[1]);
|
|
245
|
+
if (!keys) return null;
|
|
246
|
+
const full = [...table, ...keys];
|
|
247
|
+
if (!ours && full[0] === 'mcp_servers' && (full.length === 1 || OUR_TOML_NAMES.includes(full[1]))) return null;
|
|
248
|
+
scanTomlValue(bare.slice(eq[0].length), state);
|
|
249
|
+
} else if (state.multiline || state.depth > 0) {
|
|
250
|
+
scanTomlValue(bare, state);
|
|
251
|
+
}
|
|
252
|
+
if (!ours) kept.push(line);
|
|
253
|
+
else if (!trimmed || trimmed.startsWith('#')) tail.push(line);
|
|
254
|
+
else tail = [];
|
|
255
|
+
}
|
|
256
|
+
if (state.multiline || state.depth !== 0) return null;
|
|
257
|
+
if (ours) flushTail();
|
|
258
|
+
return { text: kept.join(''), removedLegacy };
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
function installIntoTomlConfig(file, entry) {
|
|
262
|
+
if (!fs.existsSync(path.dirname(file))) return { status: 'not-found' };
|
|
263
|
+
const exists = fs.existsSync(file);
|
|
264
|
+
const raw = exists ? fs.readFileSync(file, 'utf8') : '';
|
|
265
|
+
const removed = removeTomlServers(raw);
|
|
266
|
+
if (!removed) return { status: 'invalid-config' };
|
|
267
|
+
const eol = raw.includes('\r\n') ? '\r\n' : '\n';
|
|
268
|
+
let base = removed.text;
|
|
269
|
+
if (base && !base.endsWith('\n')) base += eol;
|
|
270
|
+
if (base && !/\r?\n\r?\n$/.test(base)) base += eol;
|
|
271
|
+
const next = base + tomlServerBlock(entry, eol);
|
|
272
|
+
if (next === raw) return { status: 'unchanged' };
|
|
273
|
+
const mode = exists ? fs.statSync(file).mode & 0o777 : 0o600;
|
|
274
|
+
if (exists) fs.copyFileSync(file, `${file}.bak`);
|
|
275
|
+
writeFileAtomic(file, next, mode);
|
|
276
|
+
return { status: 'configured', removedLegacy: removed.removedLegacy };
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
function shellQuote(arg, platform) {
|
|
280
|
+
if (platform === 'win32') return /^[A-Za-z0-9_\-./=@\\:]+$/.test(arg) ? arg : `"${arg}"`;
|
|
281
|
+
return /^[A-Za-z0-9_\-./=@%+,]+$/.test(arg) ? arg : `'${arg.replace(/'/g, "'\\''")}'`;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
function claudeCodeAddArgs(entry) {
|
|
285
|
+
const envFlags = Object.entries(entry.env).flatMap(([k, v]) => ['-e', `${k}=${v}`]);
|
|
286
|
+
// Name before `-e`: it swallows every value up to `--`.
|
|
287
|
+
return ['mcp', 'add', SERVER_NAME, '--scope', 'user', ...envFlags, '--', entry.command, ...entry.args];
|
|
288
|
+
}
|
|
289
|
+
|
|
101
290
|
function installIntoClaudeCode(entry, { spawn }) {
|
|
102
291
|
spawn('claude', ['mcp', 'remove', '--scope', 'user', SERVER_NAME], { encoding: 'utf8' });
|
|
103
|
-
const
|
|
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
|
-
);
|
|
292
|
+
const added = spawn('claude', claudeCodeAddArgs(entry), { encoding: 'utf8' });
|
|
110
293
|
if (added.error || added.status !== 0) return { status: 'failed' };
|
|
111
294
|
return { status: 'configured' };
|
|
112
295
|
}
|
|
@@ -133,18 +316,23 @@ function codexSkill() {
|
|
|
133
316
|
}
|
|
134
317
|
|
|
135
318
|
function installCodexSkill({ env, homedir }) {
|
|
136
|
-
const dir = path.join(env
|
|
319
|
+
const dir = path.join(codexHomeDir({ env, homedir }), 'skills', SERVER_NAME);
|
|
137
320
|
fs.mkdirSync(dir, { recursive: true });
|
|
138
321
|
fs.writeFileSync(path.join(dir, 'SKILL.md'), codexSkill());
|
|
139
322
|
}
|
|
140
323
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
324
|
+
const codexHomeDir = ({ env, homedir }) => env.CODEX_HOME || path.join(homedir, '.codex');
|
|
325
|
+
|
|
326
|
+
function chatgptAppInstalled({ platform, homedir }) {
|
|
327
|
+
return platform === 'darwin' && fs.existsSync(path.join(homedir, 'Library', 'Application Support', 'com.openai.chat'));
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
function installIntoCodex(entry, { env, homedir }) {
|
|
331
|
+
const dir = codexHomeDir({ env, homedir });
|
|
332
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
333
|
+
const result = installIntoTomlConfig(path.join(dir, 'config.toml'), entry);
|
|
334
|
+
if (result.status === 'configured' || result.status === 'unchanged') installCodexSkill({ env, homedir });
|
|
335
|
+
return result;
|
|
148
336
|
}
|
|
149
337
|
|
|
150
338
|
function vscodeUserDir({ platform, homedir, env }) {
|
|
@@ -165,10 +353,20 @@ function clientTable({ env, platform, homedir, spawn }) {
|
|
|
165
353
|
});
|
|
166
354
|
return [
|
|
167
355
|
jsonClient('claudeDesktop', claudeDesktopConfigPath({ platform, homedir, env })),
|
|
168
|
-
{
|
|
356
|
+
{
|
|
357
|
+
client: 'claudeCode',
|
|
358
|
+
found: () => hasCli('claude') || fs.existsSync(path.join(homedir, '.claude.json')),
|
|
359
|
+
install: (entry) => (hasCli('claude')
|
|
360
|
+
? installIntoClaudeCode(entry, { spawn })
|
|
361
|
+
: { status: 'manual', command: ['claude', ...claudeCodeAddArgs(entry)].map((a) => shellQuote(a, platform)).join(' ') }),
|
|
362
|
+
},
|
|
169
363
|
jsonClient('cursor', path.join(homedir, '.cursor', 'mcp.json')),
|
|
170
364
|
jsonClient('geminiCli', path.join(homedir, '.gemini', 'settings.json')),
|
|
171
|
-
{
|
|
365
|
+
{
|
|
366
|
+
client: 'codex',
|
|
367
|
+
found: () => fs.existsSync(codexHomeDir({ env, homedir })) || hasCli('codex') || chatgptAppInstalled({ platform, homedir }),
|
|
368
|
+
install: (entry) => installIntoCodex(entry, { env, homedir }),
|
|
369
|
+
},
|
|
172
370
|
// VS Code keys servers under "servers" and wants the transport spelled out.
|
|
173
371
|
jsonClient('vscode', path.join(vscodeUserDir({ platform, homedir, env }), 'mcp.json'), 'servers', (entry) => ({ type: 'stdio', ...entry })),
|
|
174
372
|
jsonClient('windsurf', path.join(homedir, '.codeium', 'windsurf', 'mcp_config.json')),
|
|
@@ -196,9 +394,10 @@ function installMcpClients({
|
|
|
196
394
|
homedir = os.homedir(),
|
|
197
395
|
nodePath = stableNodePath(env, process.execPath),
|
|
198
396
|
cliPath = fs.realpathSync(path.join(__dirname, '..', 'bin', 'shakers.js')),
|
|
397
|
+
packageName = require('../package.json').name,
|
|
199
398
|
spawn = spawnSync,
|
|
200
399
|
} = {}) {
|
|
201
|
-
const entry = serverEntry({ nodePath, cliPath, env });
|
|
400
|
+
const entry = serverEntry({ nodePath, cliPath, env, platform, homedir, packageName });
|
|
202
401
|
return {
|
|
203
402
|
entry,
|
|
204
403
|
results: clientTable({ env, platform, homedir, spawn }).map((c) => ({
|
|
@@ -231,45 +430,81 @@ function askTerminal(question) {
|
|
|
231
430
|
});
|
|
232
431
|
}
|
|
233
432
|
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
433
|
+
const NEEDS_RESTART = new Set(['configured', 'manual']);
|
|
434
|
+
|
|
435
|
+
function parseInstallArgs(args) {
|
|
436
|
+
const flags = new Set(args);
|
|
437
|
+
return {
|
|
438
|
+
yes: flags.has('--yes') || flags.has('-y'),
|
|
439
|
+
json: flags.has('--json'),
|
|
440
|
+
help: flags.has('--help') || flags.has('-h'),
|
|
441
|
+
};
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
async function connectClients({ c, opts, out, ask, deps }) {
|
|
241
445
|
const env = deps.env || process.env;
|
|
242
446
|
const found = detectMcpClients(deps);
|
|
243
447
|
out(`\n ${c.title}\n\n`);
|
|
244
448
|
if (found.length === 0) {
|
|
245
449
|
out(` ${c.noneFound}\n\n`);
|
|
246
|
-
return { ok: true, results: [] };
|
|
450
|
+
return { ok: true, found, results: [], reason: 'no-apps' };
|
|
247
451
|
}
|
|
248
452
|
out(` ${c.found(found.map((client) => c.clients[client]).join(', '))}\n`);
|
|
249
|
-
let yes = env.SHAKERS_MCP_INSTALL === '1';
|
|
453
|
+
let yes = opts.yes || env.SHAKERS_MCP_INSTALL === '1';
|
|
250
454
|
if (!yes) {
|
|
251
455
|
const answer = await ask(` ${c.confirm} `);
|
|
252
456
|
if (answer === null) {
|
|
253
|
-
out(`\n ${c.noTerminal}\n\n`);
|
|
254
|
-
return { ok: true, results: [], declined: true };
|
|
457
|
+
out(`\n ${c.noTerminal}\n ${c.yesHint}\n\n`);
|
|
458
|
+
return { ok: true, found, results: [], declined: true, reason: 'no-terminal' };
|
|
255
459
|
}
|
|
256
460
|
yes = !/^\s*(n|no)\s*$/i.test(answer);
|
|
257
461
|
}
|
|
258
462
|
if (!yes) {
|
|
259
463
|
out(`\n ${c.declined}\n\n`);
|
|
260
|
-
return { ok: true, results: [], declined: true };
|
|
464
|
+
return { ok: true, found, results: [], declined: true, reason: 'declined' };
|
|
261
465
|
}
|
|
262
466
|
|
|
263
467
|
const { results } = installMcpClients(deps);
|
|
264
468
|
out('\n');
|
|
265
469
|
for (const r of results.filter((x) => x.status !== 'not-found')) {
|
|
266
470
|
out(` ${c.clients[r.client]}: ${c.status[r.status]}\n`);
|
|
471
|
+
if (r.command) out(` ${r.command}\n`);
|
|
267
472
|
if (r.removedLegacy) out(` ${c.removedLegacy}\n`);
|
|
268
473
|
}
|
|
269
|
-
const touched = results.some((r) =>
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
474
|
+
const touched = results.some((r) => ['configured', 'unchanged', 'manual'].includes(r.status));
|
|
475
|
+
const restart = results.filter((r) => NEEDS_RESTART.has(r.status)).map((r) => c.clients[r.client]);
|
|
476
|
+
const closing = !touched ? c.failedAll : restart.length ? c.restart(restart.join(', ')) : c.alreadyConnected;
|
|
477
|
+
out(`\n ${closing}\n\n`);
|
|
478
|
+
if (results.some((r) => r.status === 'failed' || r.status === 'invalid-config')) process.exitCode = 1;
|
|
479
|
+
return { ok: true, found, results, reason: null };
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
function jsonReport({ found, results, reason }, c) {
|
|
483
|
+
const rows = reason
|
|
484
|
+
? found.map((client) => ({ client, status: 'skipped' }))
|
|
485
|
+
: results.filter((r) => r.status !== 'not-found');
|
|
486
|
+
const clients = rows.map((r) => ({
|
|
487
|
+
client: r.client,
|
|
488
|
+
name: c.clients[r.client],
|
|
489
|
+
status: r.status,
|
|
490
|
+
restartRequired: NEEDS_RESTART.has(r.status),
|
|
491
|
+
manualCommand: r.command || null,
|
|
492
|
+
}));
|
|
493
|
+
return { reason, clients, restart: clients.filter((r) => r.restartRequired).map((r) => r.name) };
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
// SHAKERS_MCP_INSTALL=1 answers yes; with no terminal, nothing is touched.
|
|
497
|
+
async function runMcpInstall({ args = [], out = (s) => process.stdout.write(s), lang = null, ask = askTerminal, ...deps } = {}) {
|
|
498
|
+
const { getCatalog, detectFlowLang } = require('./i18n');
|
|
499
|
+
const c = getCatalog(lang || detectFlowLang()).mcpInstall;
|
|
500
|
+
const opts = parseInstallArgs(args);
|
|
501
|
+
if (opts.help) {
|
|
502
|
+
out(`\n ${c.help}\n\n`);
|
|
503
|
+
return { ok: true, results: [] };
|
|
504
|
+
}
|
|
505
|
+
const outcome = await connectClients({ c, opts, out: opts.json ? () => {} : out, ask, deps });
|
|
506
|
+
if (opts.json) out(`${JSON.stringify(jsonReport(outcome, c))}\n`);
|
|
507
|
+
return outcome;
|
|
273
508
|
}
|
|
274
509
|
|
|
275
|
-
module.exports = { installCodexSkill, detectMcpClients, installMcpClients, stableNodePath, runMcpInstall, serverEntry, claudeDesktopConfigPath, SERVER_NAME, LEGACY_SERVER_NAME };
|
|
510
|
+
module.exports = { installCodexSkill, removeTomlServers, tomlServerBlock, detectMcpClients, installMcpClients, stableNodePath, runMcpInstall, serverEntry, claudeDesktopConfigPath, SERVER_NAME, LEGACY_SERVER_NAME };
|
package/src/mcp-instructions.js
CHANGED
|
@@ -17,7 +17,7 @@ function buildServerInstructions({ elicitation = false } = {}) {
|
|
|
17
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
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 45) 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
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
|
|
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 scans in the background and sends nothing. Then ai_usage_upload (waitSeconds 30) shows the talent what would be sent and what Shakers does with it, and asks; it uploads only on their own pick, and in the chat confirm:true only after they picked the upload option. If it answers scan-running, carry on and call it again right after the interview step. If they skip the analysis, its fixed line comes with the next question.',
|
|
21
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
22
|
'9. Main role, also when they skip the interview: list_my_roles (waitSeconds 45, 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
23
|
'10. Finish with open_web: it opens Shakers with the talent signed in.',
|
|
@@ -76,6 +76,7 @@ function makeOnboardingTools(deps = {}) {
|
|
|
76
76
|
requestDiscoveredInventory = require('./inventory-client').requestDiscoveredInventory,
|
|
77
77
|
requestDiscoveredInventoryWithRefresh = require('./inventory-client').requestDiscoveredInventoryWithRefresh,
|
|
78
78
|
ensureFreshSession = require('./session-refresh').ensureFreshSession,
|
|
79
|
+
endSession = require('./session-refresh').endSession,
|
|
79
80
|
requestDeclareSkill = require('./skills-client').requestDeclareSkill,
|
|
80
81
|
requestDeclareAgent = require('./agents-client').requestDeclareAgent,
|
|
81
82
|
requestListAgents = require('./agents-client').requestListAgents,
|
|
@@ -86,7 +87,7 @@ function makeOnboardingTools(deps = {}) {
|
|
|
86
87
|
getRemoteUrl = require('./portfolio-git-info').getRemoteUrl,
|
|
87
88
|
getCommitDateRange = require('./portfolio-git-info').getCommitDateRange,
|
|
88
89
|
consentCopy = () => require('./signup-copy').signupCopy(require('./signup-language').signupLanguage(require('./i18n').detectFlowLang())).consent,
|
|
89
|
-
normalizeDomain = require('./
|
|
90
|
+
normalizeDomain = require('./portfolio-client').normalizeDomain,
|
|
90
91
|
isValidEmail = require('./share').isValidEmail,
|
|
91
92
|
normalizeEmail = require('./share').normalizeEmail,
|
|
92
93
|
} = deps;
|
|
@@ -115,8 +116,7 @@ function makeOnboardingTools(deps = {}) {
|
|
|
115
116
|
};
|
|
116
117
|
|
|
117
118
|
async function logout() {
|
|
118
|
-
const wasLoggedIn =
|
|
119
|
-
try { clearAuthSession(); } catch { /* idempotent — nothing to clear */ }
|
|
119
|
+
const wasLoggedIn = await endSession(process.env, { loadAuthSession, clearAuthSession });
|
|
120
120
|
return { ok: true, wasLoggedIn };
|
|
121
121
|
}
|
|
122
122
|
|
|
@@ -251,7 +251,7 @@ function makeOnboardingTools(deps = {}) {
|
|
|
251
251
|
const relatedInput = Array.isArray(args.relatedPortfolios) ? args.relatedPortfolios : [];
|
|
252
252
|
if (relatedInput.length === 0 || !agentId) return { ...base, portfolios };
|
|
253
253
|
|
|
254
|
-
//
|
|
254
|
+
// Replace-set: validate every portfolioId against the talent's own
|
|
255
255
|
// list, then PATCH the whole set in ONE call.
|
|
256
256
|
const items = [];
|
|
257
257
|
const relateFailures = [];
|
package/src/mcp-prompts.js
CHANGED
|
@@ -14,9 +14,9 @@ const PROMPTS = [
|
|
|
14
14
|
{
|
|
15
15
|
name: 'ai_usage',
|
|
16
16
|
description: 'Evaluate and showcase how you work with AI (scans this machine, builds your "My work with AI").',
|
|
17
|
-
tools: ['ai_usage'],
|
|
17
|
+
tools: ['ai_usage', 'ai_usage_upload'],
|
|
18
18
|
buildMessages: () => userText(
|
|
19
|
-
'Evaluate how I work with AI for my Shakers profile. First ask me for consent and the email to attribute it to, then call the `ai_usage` tool and show me the resulting report verbatim. Never show internal ids/codes.',
|
|
19
|
+
'Evaluate how I work with AI for my Shakers profile. First ask me for consent and the email to attribute it to, then call the `ai_usage` tool. Before anything is uploaded it shows me what would be sent and asks me: show that review verbatim, let me answer myself, and only then use `ai_usage_upload`. Show me the resulting report verbatim. Never show internal ids/codes.',
|
|
20
20
|
),
|
|
21
21
|
},
|
|
22
22
|
{
|
|
@@ -37,10 +37,10 @@ const PROMPTS = [
|
|
|
37
37
|
},
|
|
38
38
|
{
|
|
39
39
|
name: 'certify',
|
|
40
|
-
description: 'See which role dimensions you can certify
|
|
40
|
+
description: 'See which role dimensions you can certify; the certification itself is taken on the Shakers web.',
|
|
41
41
|
tools: ['list_certifiable_dimensions'],
|
|
42
42
|
buildMessages: () => userText(
|
|
43
|
-
'Help me certify on Shakers: call `list_certifiable_dimensions
|
|
43
|
+
'Help me certify on Shakers: call `list_certifiable_dimensions` and present the dimensions I can certify by name, and when I can retry the others. The certification interview is taken on the Shakers web, not in this chat: offer to open it with `open_web`. Never show internal ids/codes.',
|
|
44
44
|
),
|
|
45
45
|
},
|
|
46
46
|
{
|
|
@@ -204,6 +204,7 @@ function makeRegisterTools(deps = {}) {
|
|
|
204
204
|
|
|
205
205
|
// Creates the interview, joins its room and reads the greeting; `room` lets a timed-out caller close what opened late.
|
|
206
206
|
async function openRoom(args, room) {
|
|
207
|
+
if (typeof resolved.makeInterviewClient !== 'function') return onWeb('connect', 'livekit-not-installed');
|
|
207
208
|
const start = await flow.createAndStartLivekitOnboarding(resolved, {
|
|
208
209
|
candidateId: args.candidateId,
|
|
209
210
|
language: args.language || lang,
|
package/src/mcp-role-tools.js
CHANGED
|
@@ -112,7 +112,24 @@ function makeRoleTools(deps = {}) {
|
|
|
112
112
|
sleep = (ms) => new Promise((resolve) => { setTimeout(resolve, ms); }),
|
|
113
113
|
now = () => Date.now(),
|
|
114
114
|
funnel = require('./onboarding-funnel').NOOP_FUNNEL,
|
|
115
|
+
inSignup = () => require('./mcp-signup-tools').signupPhase() === 'account',
|
|
116
|
+
completeOnboarding = (session) => require('./onboarding-client').requestCompleteOnboarding(
|
|
117
|
+
{ accessToken: session.accessToken, hubAccessToken: session.hubAccessToken },
|
|
118
|
+
{ endpoint: require('./config').getCompleteOnboardingEndpoint() },
|
|
119
|
+
),
|
|
115
120
|
} = deps;
|
|
121
|
+
let clustersCount;
|
|
122
|
+
let onboardingCompleted = false;
|
|
123
|
+
|
|
124
|
+
async function finishSignup(session) {
|
|
125
|
+
if (onboardingCompleted || !inSignup()) return;
|
|
126
|
+
const res = await completeOnboarding(session).catch(() => null);
|
|
127
|
+
onboardingCompleted = !!(res && res.ok);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function trackSuggestions(roles) {
|
|
131
|
+
funnel.viewed('supercluster', { suggested_superclusters: roles.map((r) => r.clusterId), suggestion_source: 'assigned_clusters', suggestions_count: roles.length });
|
|
132
|
+
}
|
|
116
133
|
|
|
117
134
|
const requireSession = () => {
|
|
118
135
|
const session = loadAuthSession();
|
|
@@ -167,6 +184,7 @@ function makeRoleTools(deps = {}) {
|
|
|
167
184
|
}
|
|
168
185
|
const assigned = await requestAssignedClusters({ hubAccessToken }, { endpoint: getAssignedClustersEndpoint() });
|
|
169
186
|
const clusters = assigned.ok ? assigned.clusters || [] : [];
|
|
187
|
+
if (assigned.ok) clustersCount = clusters.length;
|
|
170
188
|
const recommended = clusters
|
|
171
189
|
.filter((c) => c.type === 'RECOMMENDED')
|
|
172
190
|
.sort((a, b) => (SOURCE_ORDER[a.source] ?? 2) - (SOURCE_ORDER[b.source] ?? 2));
|
|
@@ -193,6 +211,7 @@ function makeRoleTools(deps = {}) {
|
|
|
193
211
|
next = 'set_main_role';
|
|
194
212
|
message = 'Call set_main_role without clusterId: it asks the talent which recommended role is their main one, in a dialog or with options it returns for you to show.';
|
|
195
213
|
} else if (read.mainClusterId) {
|
|
214
|
+
await finishSignup(session);
|
|
196
215
|
next = 'open_web';
|
|
197
216
|
message = 'The talent already has a main role and nothing new was recommended: do not ask it again, go on to open_web. Only if the talent asks to change it, call set_main_role with catalogue:true.';
|
|
198
217
|
} else {
|
|
@@ -230,11 +249,13 @@ function makeRoleTools(deps = {}) {
|
|
|
230
249
|
const read = await readRoles(session.hubAccessToken);
|
|
231
250
|
const recommended = byName(read.recommended);
|
|
232
251
|
if (!recommended.length && read.mainClusterId) {
|
|
252
|
+
await finishSignup(session);
|
|
233
253
|
return { reply: { ok: true, alreadySet: true, clusterId: read.mainClusterId, message: 'The talent already has a main role and nothing new was recommended: nothing to ask. Go on to open_web; only if they ask to change it, call set_main_role with catalogue:true.' } };
|
|
234
254
|
}
|
|
235
255
|
if (!recommended.length) return chooseFromCatalogue(args, ctx);
|
|
236
256
|
const c = copy();
|
|
237
257
|
const counts = await openCounts(session.hubAccessToken, recommended);
|
|
258
|
+
trackSuggestions(recommended);
|
|
238
259
|
const evidence = args.evidence && typeof args.evidence === 'object' ? args.evidence : {};
|
|
239
260
|
const blocks = recommended.map((r, i) => renderRole(lang(), { name: r.name, source: r.source, evidence: evidence[r.clusterId], openProjects: counts[i], recommended: i === 0 }));
|
|
240
261
|
const asked = await askAfterTexts(ctx, {
|
|
@@ -296,6 +317,7 @@ function makeRoleTools(deps = {}) {
|
|
|
296
317
|
typeof counts[i] === 'number' && counts[i] > 0 ? c.openProjects(counts[i]) : null,
|
|
297
318
|
].filter(Boolean).join('\n'));
|
|
298
319
|
const texts = [[...(blocks.length ? [c.rolesIntro, ...blocks] : []), c.rolesOutro].join('\n\n')];
|
|
320
|
+
trackSuggestions([]);
|
|
299
321
|
const asked = await askChoice(ctx, { texts, question: c.mainRoleQuestion, options: [...options.map((r) => r.name), c.mainRoleNotListed] });
|
|
300
322
|
if (asked.answer === c.mainRoleNotListed) return askOwnRole();
|
|
301
323
|
const role = asked.answer && options.find((r) => r.name === asked.answer);
|
|
@@ -330,8 +352,9 @@ function makeRoleTools(deps = {}) {
|
|
|
330
352
|
const res = await requestSetMainRole({ clusterId, hubAccessToken: session.hubAccessToken }, { endpoint: getSetMainRoleEndpoint() });
|
|
331
353
|
if (res.ok === false && res.reason === 'http-400') return { ok: false, reason: res.reason, message: 'The talent does not hold that role yet: call add_role with it, then set_main_role again.' };
|
|
332
354
|
if (!res.ok) return { ok: false, reason: res.reason };
|
|
333
|
-
funnel.completed('supercluster', { supercluster: clusterId, was_suggestion_accepted: suggestion });
|
|
355
|
+
funnel.completed('supercluster', { supercluster: clusterId, clusters_count: clustersCount, was_suggestion_accepted: suggestion });
|
|
334
356
|
funnel.flush();
|
|
357
|
+
await finishSignup(session);
|
|
335
358
|
return { ok: true, clusterId };
|
|
336
359
|
}
|
|
337
360
|
|
package/src/mcp-server.js
CHANGED
|
@@ -34,7 +34,7 @@ function errorResponse(id, code, message, data) {
|
|
|
34
34
|
return { jsonrpc: JSONRPC, id: id === undefined ? null : id, error };
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
-
function createMcpServer({ tools = [], prompts = [], serverInfo = {}, protocolVersion, beforeCall = null, onInitialize = null, instructions = null, elicitationTimeoutMs = ELICITATION_TIMEOUT_MS } = {}) {
|
|
37
|
+
function createMcpServer({ tools = [], prompts = [], serverInfo = {}, protocolVersion, beforeCall = null, afterCall = null, onInitialize = null, instructions = null, elicitationTimeoutMs = ELICITATION_TIMEOUT_MS } = {}) {
|
|
38
38
|
const registry = new Map();
|
|
39
39
|
for (const t of tools) {
|
|
40
40
|
if (!t || !t.name || typeof t.handler !== 'function') {
|
|
@@ -185,6 +185,10 @@ function createMcpServer({ tools = [], prompts = [], serverInfo = {}, protocolVe
|
|
|
185
185
|
content: [{ type: 'text', text: `${name} tool error: ${e && e.message ? e.message : String(e)}` }],
|
|
186
186
|
isError: true,
|
|
187
187
|
};
|
|
188
|
+
} finally {
|
|
189
|
+
if (typeof afterCall === 'function') {
|
|
190
|
+
try { afterCall(name); } catch { /* hook must never break a tool call */ }
|
|
191
|
+
}
|
|
188
192
|
}
|
|
189
193
|
}
|
|
190
194
|
|
package/src/mcp-signup-client.js
CHANGED
|
@@ -2,12 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
const { postJsonWithTimeout, postMultipartWithTimeout } = require('./backend-request');
|
|
4
4
|
|
|
5
|
-
// Hub calls of the MCP sign-up
|
|
5
|
+
// Hub calls of the MCP sign-up: public sign-up + claim code, the signed-in talent's import status, and the one-time web login token.
|
|
6
6
|
|
|
7
7
|
const SIGNUP_TIMEOUT_MS = 60000;
|
|
8
8
|
const DEFAULT_TIMEOUT_MS = 15000;
|
|
9
9
|
|
|
10
|
-
// Hub's coded errors on the sign-up
|
|
10
|
+
// Hub's coded errors on the sign-up; none says whether an account exists.
|
|
11
11
|
const CODE_TO_REASON = {
|
|
12
12
|
MISSING_SOURCE: 'missing-source',
|
|
13
13
|
INVALID_CV: 'invalid-cv',
|