shakers-cli-beta 0.13.0 → 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/README.md +28 -3
- package/bin/mcp.js +13 -7
- package/bin/register.js +5 -2
- package/package.json +1 -1
- package/src/backend-request.js +12 -8
- package/src/config.js +17 -5
- package/src/consent-flow.js +10 -10
- package/src/i18n.js +6 -6
- package/src/mcp-ai-usage-result-tool.js +4 -2
- package/src/mcp-ai-usage-tool.js +3 -1
- package/src/mcp-alma-tools.js +7 -3
- package/src/mcp-applications-tools.js +4 -2
- package/src/mcp-availability-tools.js +4 -2
- package/src/mcp-certifications-tools.js +4 -2
- package/src/mcp-certify-dimension-tools.js +4 -2
- package/src/mcp-choice.js +27 -1
- package/src/mcp-device-login-tools.js +6 -2
- package/src/mcp-find-projects-tools.js +4 -2
- package/src/mcp-instructions.js +1 -1
- package/src/mcp-invitations-tools.js +4 -2
- package/src/mcp-lang-tools.js +4 -2
- package/src/mcp-onboarding-tools.js +41 -44
- package/src/mcp-portfolios-tools.js +7 -3
- package/src/mcp-profile-tools.js +4 -2
- package/src/mcp-prompts.js +2 -2
- package/src/mcp-rate-tools.js +4 -2
- package/src/mcp-register-tools.js +52 -19
- package/src/mcp-report-tools.js +7 -3
- package/src/mcp-role-tools.js +23 -8
- package/src/mcp-saved-positions-tools.js +7 -3
- package/src/mcp-server.js +8 -2
- package/src/mcp-show-project-tools.js +4 -2
- package/src/mcp-signup-tools.js +153 -20
- package/src/mcp-socials-tools.js +4 -2
- package/src/onboarding-flow.js +112 -13
- package/src/onboarding-funnel.js +272 -0
- package/src/share.js +4 -3
- package/src/signup-copy.js +55 -1
- package/src/mcp-ask-tools.js +0 -50
package/README.md
CHANGED
|
@@ -379,7 +379,7 @@ scanned project** — the report can't slip into a commit by accident.
|
|
|
379
379
|
Desktop) can drive the same features as tools. It exposes, among others:
|
|
380
380
|
|
|
381
381
|
- **AI usage**: `ai_usage`, `ai_usage_result`.
|
|
382
|
-
- **Auth**: `
|
|
382
|
+
- **Auth**: `social_signin_start`, `social_signin_poll`, `logout`.
|
|
383
383
|
- **Sign-up** (from the talent's own AI, ticket 39540; copy from UX review
|
|
384
384
|
39574): every talent-facing text is fixed, comes from the tools in the
|
|
385
385
|
talent's language (es/en/it/pt, fixed by their first message) and the AI shows
|
|
@@ -413,17 +413,36 @@ Desktop) can drive the same features as tools. It exposes, among others:
|
|
|
413
413
|
pick directly; otherwise it returns `question` and `options` and the AI offers
|
|
414
414
|
them as buttons or a numbered list. The legal texts are always shown in the
|
|
415
415
|
chat before any consent dialog.
|
|
416
|
+
- **Asked before touching this computer**: `suggest_register_context` (look for
|
|
417
|
+
the CV), `read_cv` (read one CV file), `import_profile` with a CV file (send it)
|
|
418
|
+
and `add_project` (send the project folder's name, web address, dates and
|
|
419
|
+
client) each ask the talent themselves, naming exactly what they read or send,
|
|
420
|
+
the same way as the closed questions above. Without a dialog, the answer only
|
|
421
|
+
counts when it comes back for a question that tool asked for those exact
|
|
422
|
+
arguments; a yes the AI sets on its own is ignored and the question is asked
|
|
423
|
+
again. Nothing is read or sent before the yes.
|
|
416
424
|
- **Profile / skills / agents**: `list_addable`, `add_skill`, `add_agent`,
|
|
417
425
|
`add_project`, `get_profile`, `get_rate`, `get_languages`, `list_socials`,
|
|
418
426
|
`list_experiences`, `list_portfolios`, `get_availability`.
|
|
419
427
|
- **Marketplace**: `find_projects`, `show_project`, `save_project`,
|
|
420
428
|
`unsave_project`, `list_invitations`, `list_applications`.
|
|
421
429
|
- **Certifications**: `list_certifications`, `list_certifiable_dimensions`.
|
|
422
|
-
- **Alma**: `
|
|
430
|
+
- **Alma**: `alma_chat`, `alma_decision` (a change Alma proposes is applied only
|
|
431
|
+
after the talent says yes).
|
|
432
|
+
|
|
433
|
+
Each tool has two audiences. Its `title` and `description` are what the talent
|
|
434
|
+
reads in the client: plain words saying what it does, what it reads on the
|
|
435
|
+
machine and what it sends to Shakers. `packaging/mcpb/manifest.json` declares the
|
|
436
|
+
same tools with the same descriptions, and a test keeps them equal. How the AI
|
|
437
|
+
must use a tool (order, verbatim texts, internal ids) travels in the
|
|
438
|
+
description of its input schema.
|
|
423
439
|
|
|
424
440
|
MCP tool names are **stable** (renaming would break existing client configs); the
|
|
425
441
|
role tools above were added, not renamed. `register`, `register_preview` and
|
|
426
|
-
`register_status` were removed with the sign-up redesign, not renamed.
|
|
442
|
+
`register_status` were removed with the sign-up redesign, not renamed. `login`
|
|
443
|
+
(it only pointed to `social_signin_start`) and `ask` (a copy of `alma_chat` that
|
|
444
|
+
dropped Alma's confirmation requests) were removed to show the talent fewer
|
|
445
|
+
tools; the `/ask` prompt now uses `alma_chat`.
|
|
427
446
|
|
|
428
447
|
## Try it
|
|
429
448
|
|
|
@@ -885,6 +904,12 @@ history directory is absent or unreadable.
|
|
|
885
904
|
whether it's silently dropped or degraded on failure, is unaffected by
|
|
886
905
|
consent either way. The local **usage** read (Claude Code history) is not
|
|
887
906
|
even a network call — it never leaves the machine, consent or not.
|
|
907
|
+
- **Registration funnel analytics.** `shakers register` and the MCP sign-up
|
|
908
|
+
send one event per registration step to the Hub, which relays them to
|
|
909
|
+
Amplitude. Only steps and categories travel (never an exact rate or free
|
|
910
|
+
text), with a random device id kept in
|
|
911
|
+
`~/.config/shakers/analytics-device.json` (permissions `600`). A failed
|
|
912
|
+
send never affects the registration.
|
|
888
913
|
- The consent decision lives in `~/.config/shakers/consent.json`
|
|
889
914
|
(permissions `600`).
|
|
890
915
|
- An **unfinished interview** (agent certification, or the skill-code
|
package/bin/mcp.js
CHANGED
|
@@ -14,7 +14,6 @@ const { makeAvailabilityTools } = require('../src/mcp-availability-tools');
|
|
|
14
14
|
const { makeCertificationsTools } = require('../src/mcp-certifications-tools');
|
|
15
15
|
const { makeProfileTools } = require('../src/mcp-profile-tools');
|
|
16
16
|
const { makeRateTools } = require('../src/mcp-rate-tools');
|
|
17
|
-
const { makeAskTools } = require('../src/mcp-ask-tools');
|
|
18
17
|
const { makeAlmaTools } = require('../src/mcp-alma-tools');
|
|
19
18
|
const { makeLangTools } = require('../src/mcp-lang-tools');
|
|
20
19
|
const { makeSocialsTools } = require('../src/mcp-socials-tools');
|
|
@@ -29,6 +28,7 @@ const { makeReportTools } = require('../src/mcp-report-tools');
|
|
|
29
28
|
const { makePrompts } = require('../src/mcp-prompts');
|
|
30
29
|
const { buildServerInstructions } = require('../src/mcp-instructions');
|
|
31
30
|
const { ensureFreshSession } = require('../src/session-refresh');
|
|
31
|
+
const { createFunnel } = require('../src/onboarding-funnel');
|
|
32
32
|
|
|
33
33
|
let VERSION = '';
|
|
34
34
|
try {
|
|
@@ -43,7 +43,7 @@ const HELP = `\n shakers mcp — local MCP server (stdio)\n\n`
|
|
|
43
43
|
+ ` Usage: shakers mcp\n\n`
|
|
44
44
|
+ ` This command speaks JSON-RPC on stdio; it is meant to be launched by an\n`
|
|
45
45
|
+ ` MCP client, not run by hand.\n`
|
|
46
|
-
+ ` Tools: ai_usage, ai_usage_result,
|
|
46
|
+
+ ` Tools: ai_usage, ai_usage_result, logout, social_signin_start, social_signin_poll,\n`
|
|
47
47
|
+ ` list_addable, add_skill, add_agent,\n`
|
|
48
48
|
+ ` add_project, signup_start, signup_email, signup_draft, signup_create_account,\n`
|
|
49
49
|
+ ` signup_status, update_existing_profile, import_profile, save_profile_details, open_web,\n`
|
|
@@ -57,7 +57,7 @@ const HELP = `\n shakers mcp — local MCP server (stdio)\n\n`
|
|
|
57
57
|
+ ` save_project, unsave_project, list_invitations,\n`
|
|
58
58
|
+ ` list_applications, get_availability,\n`
|
|
59
59
|
+ ` list_certifications, get_profile, get_rate, get_languages,\n`
|
|
60
|
-
+ ` list_socials, list_experiences, list_portfolios,
|
|
60
|
+
+ ` list_socials, list_experiences, list_portfolios, alma_chat, alma_decision.\n\n`;
|
|
61
61
|
|
|
62
62
|
// The LOCAL (filesystem/machine) tools — the only ones the .mcpb bundle exposes; the
|
|
63
63
|
// server-backed tools belong to the remote connector.
|
|
@@ -65,9 +65,9 @@ const LOCAL_ONLY_TOOL_NAMES = new Set(['ai_usage', 'ai_usage_result', 'read_cv',
|
|
|
65
65
|
|
|
66
66
|
// `localOnly` builds the scan-only server for the Claude .mcpb bundle, reusing the SAME
|
|
67
67
|
// tool registry (just filtered) — no duplicate registration.
|
|
68
|
-
function buildServer({ localOnly = false } = {}) {
|
|
68
|
+
function buildServer({ localOnly = false, funnel = createFunnel({ surface: 'mcp' }) } = {}) {
|
|
69
69
|
const lang = detectFlowLang();
|
|
70
|
-
const allTools = [makeAiUsageTool(), makeAiUsageResultTool(), ...makeOnboardingTools(), ...makeRoleTools({ lang }), ...makeDeviceLoginTools(), ...makeSignupTools({ lang }), ...makeRegisterTools({ lang }), ...makeReportTools(), ...makeCertifyDimensionTools(), ...makeFindProjectsTools(), ...makeShowProjectTools(), ...makeSavedPositionsTools(), ...makeInvitationsTools(), ...makeApplicationsTools(), ...makeAvailabilityTools(), ...makeCertificationsTools(), ...makeProfileTools(), ...makeRateTools(), ...makeLangTools(), ...makeSocialsTools(), ...makePortfolioTools(), ...
|
|
70
|
+
const allTools = [makeAiUsageTool(), makeAiUsageResultTool(), ...makeOnboardingTools(), ...makeRoleTools({ lang, funnel }), ...makeDeviceLoginTools(), ...makeSignupTools({ lang, funnel }), ...makeRegisterTools({ lang, funnel }), ...makeReportTools(), ...makeCertifyDimensionTools(), ...makeFindProjectsTools(), ...makeShowProjectTools(), ...makeSavedPositionsTools(), ...makeInvitationsTools(), ...makeApplicationsTools(), ...makeAvailabilityTools(), ...makeCertificationsTools(), ...makeProfileTools(), ...makeRateTools(), ...makeLangTools(), ...makeSocialsTools(), ...makePortfolioTools(), ...makeAlmaTools()];
|
|
71
71
|
const tools = localOnly ? allTools.filter((t) => LOCAL_ONLY_TOOL_NAMES.has(t.name)) : allTools;
|
|
72
72
|
// Slash-command prompts, kept to the tools actually registered in this mode.
|
|
73
73
|
const prompts = makePrompts(new Set(tools.map((t) => t.name)), lang);
|
|
@@ -79,9 +79,15 @@ function buildServer({ localOnly = false } = {}) {
|
|
|
79
79
|
// Long-lived server: refresh the email session's hub JWT from the stored
|
|
80
80
|
// cookie before each tool call so a stale (~15-min) JWT never reaches a tool.
|
|
81
81
|
beforeCall: () => ensureFreshSession(process.env),
|
|
82
|
+
onInitialize: ({ clientInfo }) => funnel.setMcpClient(clientInfo.name),
|
|
82
83
|
});
|
|
83
84
|
}
|
|
84
85
|
|
|
86
|
+
function prepareProcess(env = process.env) {
|
|
87
|
+
require('../src/config').sanitizeStaleBakedEndpoints(env);
|
|
88
|
+
require('../src/backend-request').setClientIdentity('mcp');
|
|
89
|
+
}
|
|
90
|
+
|
|
85
91
|
async function run(argv = process.argv.slice(2)) {
|
|
86
92
|
// The installer's last step; not a command the Talent needs to know.
|
|
87
93
|
if (argv[0] === 'install') {
|
|
@@ -93,7 +99,7 @@ async function run(argv = process.argv.slice(2)) {
|
|
|
93
99
|
return;
|
|
94
100
|
}
|
|
95
101
|
|
|
96
|
-
|
|
102
|
+
prepareProcess();
|
|
97
103
|
|
|
98
104
|
// `--local` = the scan-only server shipped in the Claude .mcpb bundle.
|
|
99
105
|
const server = buildServer({ localOnly: argv.includes('--local') });
|
|
@@ -111,7 +117,7 @@ async function run(argv = process.argv.slice(2)) {
|
|
|
111
117
|
});
|
|
112
118
|
}
|
|
113
119
|
|
|
114
|
-
module.exports = { run, buildServer, buildServerInstructions };
|
|
120
|
+
module.exports = { run, buildServer, buildServerInstructions, prepareProcess };
|
|
115
121
|
|
|
116
122
|
if (require.main === module) {
|
|
117
123
|
run();
|
package/bin/register.js
CHANGED
|
@@ -20,6 +20,7 @@ const { typeOut } = require('../src/typewriter');
|
|
|
20
20
|
const { withSpinner } = require('../src/terminal-progress');
|
|
21
21
|
const { openPath } = require('../src/open-file');
|
|
22
22
|
const { c: fmt } = require('../src/terminal-format');
|
|
23
|
+
const { createFunnel } = require('../src/onboarding-funnel');
|
|
23
24
|
const red = (s) => `${fmt.danger}${s}${fmt.reset}`;
|
|
24
25
|
|
|
25
26
|
const VALID_LANGS = new Set(['es', 'en']);
|
|
@@ -285,7 +286,7 @@ function makeIo({ ask, lang, out, acceptDisclaimer, stdinIsTTY = false }) {
|
|
|
285
286
|
|
|
286
287
|
async function run(
|
|
287
288
|
argv = process.argv.slice(2),
|
|
288
|
-
{ ask: injectedAsk = null, stdinIsTTY: ttyOverride = undefined, deps = null, out = null, openBrowser = undefined, input = undefined, output = undefined } = {},
|
|
289
|
+
{ ask: injectedAsk = null, stdinIsTTY: ttyOverride = undefined, deps = null, out = null, openBrowser = undefined, input = undefined, output = undefined, funnel = undefined } = {},
|
|
289
290
|
) {
|
|
290
291
|
const o = parseArgs(argv);
|
|
291
292
|
const lang = o.lang || detectFlowLang();
|
|
@@ -316,7 +317,9 @@ async function run(
|
|
|
316
317
|
const io = makeIo({ ask, lang, out: write, acceptDisclaimer: o.acceptDisclaimer, stdinIsTTY });
|
|
317
318
|
// At a terminal the profile opens in the browser at the end; piped runs, tests and --no-open only print it.
|
|
318
319
|
const openProfile = stdinIsTTY && !o.noOpen ? (openBrowser || openPath) : undefined;
|
|
319
|
-
|
|
320
|
+
// Injected deps (tests) bring their own funnel or none.
|
|
321
|
+
const registrationFunnel = funnel || (deps ? undefined : createFunnel({ surface: 'cli' }));
|
|
322
|
+
const result = await runOnboarding(io, deps || makeDeps(), { method, openBrowser, onAuthUrl: io.onGoogleAuthUrl, openProfile, funnel: registrationFunnel });
|
|
320
323
|
if (!result.ok) {
|
|
321
324
|
process.stderr.write(`\n ${red(`register: ${result.reason}${result.step ? ` (${result.step})` : ''}`)}\n\n`);
|
|
322
325
|
process.exitCode = 1;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "shakers-cli-beta",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.14.1",
|
|
4
4
|
"description": "Shakers AI usage & skills evaluator: a local-first CLI (`shakers` binary) plus a local MCP server. Commands: ai-usage (scan + classify your AI-tool setup, T0-T7), certify (certify Skills from your code), share (branded LinkedIn card).",
|
|
5
5
|
"type": "commonjs",
|
|
6
6
|
"bin": {
|
package/src/backend-request.js
CHANGED
|
@@ -8,6 +8,14 @@ const crypto = require('crypto');
|
|
|
8
8
|
|
|
9
9
|
const MAX_RESPONSE_BYTES = 8 * 1024 * 1024;
|
|
10
10
|
|
|
11
|
+
const CLIENT_IDENTITIES = ['cli', 'mcp'];
|
|
12
|
+
let clientIdentity = 'cli';
|
|
13
|
+
|
|
14
|
+
function setClientIdentity(identity) {
|
|
15
|
+
if (!CLIENT_IDENTITIES.includes(identity)) throw new Error(`unknown client identity: ${identity}`);
|
|
16
|
+
clientIdentity = identity;
|
|
17
|
+
}
|
|
18
|
+
|
|
11
19
|
// Extra request headers, sanitized (talents-ai-score, issue 038 — the agent certification interview needs `Authorization: Bearer <sessionToken>`).
|
|
12
20
|
const UNSAFE_HEADER_VALUE_RE = /[\r\n\0]/;
|
|
13
21
|
|
|
@@ -39,13 +47,8 @@ function postJsonWithTimeout(url, body, timeoutMs, method = 'POST', idempotencyK
|
|
|
39
47
|
method,
|
|
40
48
|
headers: {
|
|
41
49
|
Accept: 'application/json',
|
|
42
|
-
//
|
|
43
|
-
|
|
44
|
-
// bin/ entrypoint is running, since none of them plumb through here
|
|
45
|
-
// separately today. Set unconditionally so no caller can drop it via
|
|
46
|
-
// extraHeaders (sanitizeExtraHeaders runs after this, so it can only
|
|
47
|
-
// add headers, not remove this one).
|
|
48
|
-
'X-Shakers-Client': 'cli',
|
|
50
|
+
// Tells CLI and MCP traffic apart from the browser.
|
|
51
|
+
'X-Shakers-Client': clientIdentity,
|
|
49
52
|
// Only declared when there IS a body — see the bodyless note above.
|
|
50
53
|
...(data ? { 'Content-Type': 'application/json', 'Content-Length': data.length } : {}),
|
|
51
54
|
...(idempotencyKey ? { 'Idempotency-Key': idempotencyKey } : {}),
|
|
@@ -125,7 +128,7 @@ function postMultipartWithTimeout(url, fields, timeoutMs, method = 'POST', extra
|
|
|
125
128
|
headers: {
|
|
126
129
|
Accept: 'application/json',
|
|
127
130
|
// See postJsonWithTimeout's identical header for why.
|
|
128
|
-
'X-Shakers-Client':
|
|
131
|
+
'X-Shakers-Client': clientIdentity,
|
|
129
132
|
'Content-Type': `multipart/form-data; boundary=${boundary}`,
|
|
130
133
|
'Content-Length': data.length,
|
|
131
134
|
...sanitizeExtraHeaders(extraHeaders),
|
|
@@ -184,4 +187,5 @@ module.exports = {
|
|
|
184
187
|
postMultipartWithTimeout,
|
|
185
188
|
extractSetCookie,
|
|
186
189
|
requestBackend,
|
|
190
|
+
setClientIdentity,
|
|
187
191
|
};
|
package/src/config.js
CHANGED
|
@@ -124,10 +124,17 @@ function resolveIngestEndpoint(env = process.env) {
|
|
|
124
124
|
// Only the DEFAULT bases; per-endpoint SHAKERS_CLI_* env and config.json overrides
|
|
125
125
|
// still win at each getter (env > config.json > baked profile).
|
|
126
126
|
const ENV_PROFILES = {
|
|
127
|
+
prod: {
|
|
128
|
+
certsBase: 'https://api.certifications.shakersworks.com/api/v1',
|
|
129
|
+
hubBase: 'https://api.hub.shakersworks.com/api/v1',
|
|
130
|
+
profileUrl: 'https://app.shakersworks.com/login',
|
|
131
|
+
almaBase: 'https://app.shakersworks.com',
|
|
132
|
+
},
|
|
127
133
|
staging: {
|
|
128
134
|
certsBase: 'https://api.staging.certifications.shakersworks.com/api/v1',
|
|
129
135
|
hubBase: 'https://api.hub.staging.shakersworks.com/api/v1',
|
|
130
136
|
profileUrl: 'https://works.staging.shakersworks.com/login',
|
|
137
|
+
almaBase: 'https://works.staging.shakersworks.com',
|
|
131
138
|
},
|
|
132
139
|
dev: {
|
|
133
140
|
certsBase: 'https://api.dev.certifications.shakersworks.com/api/v1',
|
|
@@ -142,8 +149,8 @@ const ENV_PROFILES = {
|
|
|
142
149
|
};
|
|
143
150
|
|
|
144
151
|
// Which flavor is baked: SHAKERS_CLI_ENV override wins, else derived from the
|
|
145
|
-
// published package name (beta -> staging, private -> dev), else dev (safe default).
|
|
146
|
-
const PACKAGE_NAME_ENV = { 'shakers-cli-beta': 'staging', '@shakers/shakers-cli': 'dev' };
|
|
152
|
+
// published package name (public -> prod, beta -> staging, private -> dev), else dev (safe default).
|
|
153
|
+
const PACKAGE_NAME_ENV = { 'shakers-cli': 'prod', 'shakers-cli-beta': 'staging', '@shakers/shakers-cli': 'dev' };
|
|
147
154
|
const DEFAULT_ENV_PROFILE = 'dev';
|
|
148
155
|
|
|
149
156
|
function bakedEnvProfileName() {
|
|
@@ -395,13 +402,13 @@ function getTalentMeProfileEndpoint(env = process.env) {
|
|
|
395
402
|
}
|
|
396
403
|
|
|
397
404
|
// Alma (the AI assistant) is a SEPARATE service — NOT derived from hubBase.
|
|
398
|
-
//
|
|
405
|
+
// SHAKERS_CLI_ALMA_BASE > the baked profile's almaBase > the local/dev container.
|
|
399
406
|
const DEFAULT_ALMA_BASE = 'http://127.0.0.1:5030';
|
|
400
407
|
|
|
401
408
|
function getAlmaBase(env = process.env) {
|
|
402
409
|
const explicit = readEnv(env, 'ALMA_BASE');
|
|
403
410
|
if (explicit && explicit.trim()) return explicit.trim().replace(/\/+$/, '');
|
|
404
|
-
return DEFAULT_ALMA_BASE;
|
|
411
|
+
return getEnvProfile(env).almaBase || DEFAULT_ALMA_BASE;
|
|
405
412
|
}
|
|
406
413
|
|
|
407
414
|
function getAlmaChatEndpoint(env = process.env) {
|
|
@@ -553,6 +560,10 @@ function getMyImportStatusEndpoint(env = process.env) {
|
|
|
553
560
|
return deriveFromHub(env, 'works/talent/me/import-profile/status');
|
|
554
561
|
}
|
|
555
562
|
|
|
563
|
+
function getOnboardingEventsEndpoint(env = process.env) {
|
|
564
|
+
return deriveFromHub(env, 'works/talent/me/onboarding-events');
|
|
565
|
+
}
|
|
566
|
+
|
|
556
567
|
function getOneTimeTokenEndpoint(env = process.env) {
|
|
557
568
|
return deriveFromHub(env, 'auth/one-time-token/generate');
|
|
558
569
|
}
|
|
@@ -786,7 +797,7 @@ module.exports = {
|
|
|
786
797
|
getMeAvailabilityEndpoint,
|
|
787
798
|
getMeProfileEndpoint,
|
|
788
799
|
getTalentMeProfileEndpoint,
|
|
789
|
-
// Alma assistant (`ask` command / `
|
|
800
|
+
// Alma assistant (`ask` command / `alma_chat` MCP tool) — separate service.
|
|
790
801
|
getAlmaBase,
|
|
791
802
|
getAlmaChatEndpoint,
|
|
792
803
|
getAlmaDecisionEndpoint,
|
|
@@ -816,6 +827,7 @@ module.exports = {
|
|
|
816
827
|
getImportProfileEndpoint,
|
|
817
828
|
getMcpSignupEndpoint,
|
|
818
829
|
getMyImportStatusEndpoint,
|
|
830
|
+
getOnboardingEventsEndpoint,
|
|
819
831
|
getOneTimeTokenEndpoint,
|
|
820
832
|
getOneTimeLoginUrl,
|
|
821
833
|
getProfessionalDetailsEndpoint,
|
package/src/consent-flow.js
CHANGED
|
@@ -15,9 +15,10 @@ const MAX_ATTEMPTS = 5;
|
|
|
15
15
|
const YES_RE = /^(y|yes|s|si|sí)$/i;
|
|
16
16
|
const NO_RE = /^(n|no)$/i;
|
|
17
17
|
|
|
18
|
-
async function askYesNo(ask, question, catalog) {
|
|
18
|
+
async function askYesNo(ask, question, catalog, { defaultNo = false } = {}) {
|
|
19
19
|
for (let attempt = 0; attempt < MAX_ATTEMPTS; attempt++) {
|
|
20
20
|
const raw = String(await ask(question)).trim();
|
|
21
|
+
if (defaultNo && raw === '') return false;
|
|
21
22
|
if (YES_RE.test(raw)) return true;
|
|
22
23
|
if (NO_RE.test(raw)) return false;
|
|
23
24
|
process.stdout.write(` ${catalog.consent.invalidAnswer}\n`);
|
|
@@ -62,7 +63,12 @@ async function runConsentPrompt({
|
|
|
62
63
|
|
|
63
64
|
if (drainBeforeAsk && typeof ask.drain === 'function') ask.drain();
|
|
64
65
|
|
|
65
|
-
|
|
66
|
+
if (loggedIn && sessionEmail && isValidEmail(sessionEmail)) {
|
|
67
|
+
process.stdout.write(` ${c.savedUnderEmail(sessionEmail)}\n`);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// defaultNo: a blank Enter means "do not save" (legal requirement).
|
|
71
|
+
const accepted = await askYesNo(ask, c.persistQuestion, catalog, { defaultNo: true });
|
|
66
72
|
if (accepted === null) {
|
|
67
73
|
process.stdout.write(` ${c.notObtained}\n\n`);
|
|
68
74
|
return null;
|
|
@@ -92,13 +98,9 @@ async function runConsentPrompt({
|
|
|
92
98
|
return null;
|
|
93
99
|
}
|
|
94
100
|
|
|
95
|
-
|
|
96
|
-
recordConsent('granted', email, { verified: false });
|
|
97
|
-
process.stdout.write(` ${c.grantedSavedExternal(email)}\n\n`);
|
|
98
|
-
return 'granted';
|
|
99
|
-
}
|
|
101
|
+
process.stdout.write(` ${c.savedUnderEmail(email)}\n`);
|
|
100
102
|
|
|
101
|
-
// Prove email ownership BEFORE persisting
|
|
103
|
+
// Prove email ownership BEFORE persisting (ADR-006) — every profile, external included.
|
|
102
104
|
const verification = await verifyEmail({ email, ask, catalog, env });
|
|
103
105
|
if (verification && verification.verified) {
|
|
104
106
|
recordConsent('granted', email, { verified: true });
|
|
@@ -106,8 +108,6 @@ async function runConsentPrompt({
|
|
|
106
108
|
return 'granted';
|
|
107
109
|
}
|
|
108
110
|
|
|
109
|
-
// Not verified: persist nothing, re-ask next run. runEmailVerification
|
|
110
|
-
// already printed the specific reason (cancelled / technical / exhausted).
|
|
111
111
|
process.stdout.write(` ${c.notObtained}\n\n`);
|
|
112
112
|
return null;
|
|
113
113
|
}
|
package/src/i18n.js
CHANGED
|
@@ -637,17 +637,17 @@ const catalogs = {
|
|
|
637
637
|
+ 'puede acarrear penalizaciones en tu cuenta de Shakers, incluida la '
|
|
638
638
|
+ 'posible suspensión. Consulta el README de este repositorio para '
|
|
639
639
|
+ 'más detalle.',
|
|
640
|
-
persistQuestion: '¿Guardar este informe en Shakers?
|
|
640
|
+
persistQuestion: '¿Guardar este informe en Shakers? [s/N]:',
|
|
641
641
|
invalidAnswer: 'Respuesta no reconocida. Responde "s" (sí) o "n" (no).',
|
|
642
642
|
emailPrompt: 'Introduce tu correo:',
|
|
643
|
-
emailPromptExternal: 'Déjanos tu email para que el equipo de Shakers pueda contactarte
|
|
643
|
+
emailPromptExternal: 'Déjanos tu email para que el equipo de Shakers pueda contactarte:',
|
|
644
|
+
savedUnderEmail: (email) => `El informe se guardará a nombre de: ${email}`,
|
|
644
645
|
invalidEmail: 'Correo no válido, inténtalo de nuevo.',
|
|
645
646
|
notObtained: 'No se ha podido registrar tu respuesta; se te volverá a preguntar la próxima vez.',
|
|
646
647
|
// talents-ai-score, ADR-051: esta pregunta se pide ahora ANTES de cualquier llamada de IA/egress (agent-synthesis, agent-evaluation, personalización de roadmap), no después.
|
|
647
648
|
noReportWithoutConsent: 'Sin tu consentimiento, este informe no incluirá síntesis, evaluación ni roadmap generados por IA esta vez (el resto del informe se muestra igual). Vuelve a ejecutar `usage` y acepta para incluirlos, o usa --consent-reset si quieres que se te pregunte de nuevo.',
|
|
648
649
|
deniedSaved: 'Entendido, no se guardará nada. Puedes cambiar de opinión más adelante volviendo a ejecutar el comando.',
|
|
649
650
|
grantedSaved: (email) => `Gracias. A partir de ahora este informe se guardará automáticamente en Shakers (correo: ${email}, máx. 1 vez por hora).`,
|
|
650
|
-
grantedSavedExternal: (email) => `Gracias. Hemos guardado tu email (${email}) para que el equipo de Shakers pueda contactarte — no lo hemos verificado.`,
|
|
651
651
|
// talents-ai-score, issue 130: un talento logueado NUNCA ve el prompt de email (su cuenta ya prueba su identidad).
|
|
652
652
|
noSessionEmail: 'No hay un correo asociado a tu sesión todavía, así que no se puede guardar el informe esta vez. Se te preguntará de nuevo la próxima ejecución.',
|
|
653
653
|
skipAlreadyDecided: (decision, path) =>
|
|
@@ -2277,17 +2277,17 @@ const catalogs = {
|
|
|
2277
2277
|
+ 'yours or that you are not authorized to analyze— may result in penalties '
|
|
2278
2278
|
+ "on your Shakers account, up to and including suspension. See this "
|
|
2279
2279
|
+ "repository's README for more detail.",
|
|
2280
|
-
persistQuestion: 'Save this report in Shakers?
|
|
2280
|
+
persistQuestion: 'Save this report in Shakers? [y/N]:',
|
|
2281
2281
|
invalidAnswer: 'Answer not recognized. Reply "y" (yes) or "n" (no).',
|
|
2282
2282
|
emailPrompt: 'Enter your email:',
|
|
2283
|
-
emailPromptExternal:
|
|
2283
|
+
emailPromptExternal: 'Leave us your email so the Shakers team can reach out:',
|
|
2284
|
+
savedUnderEmail: (email) => `The report will be saved under: ${email}`,
|
|
2284
2285
|
invalidEmail: 'Invalid email, try again.',
|
|
2285
2286
|
notObtained: "Couldn't record your answer; you'll be asked again next time.",
|
|
2286
2287
|
// talents-ai-score, ADR-051: this question is now asked BEFORE any AI/egress call (agent-synthesis, agent-evaluation, roadmap personalization), not after.
|
|
2287
2288
|
noReportWithoutConsent: 'Without your consent, this report will not include AI-generated synthesis, evaluation, or roadmap this time (the rest of the report is shown as usual). Run `usage` again and accept to include them, or use --consent-reset if you want to be asked again.',
|
|
2288
2289
|
deniedSaved: 'Understood, nothing will be saved. You can change your mind later by running the command again.',
|
|
2289
2290
|
grantedSaved: (email) => `Thanks. From now on this report will be saved in Shakers automatically (email: ${email}, max. once per hour).`,
|
|
2290
|
-
grantedSavedExternal: (email) => `Thanks. We've saved your email (${email}) so the Shakers team can reach out — we haven't verified it.`,
|
|
2291
2291
|
// talents-ai-score, issue 130: a logged-in Talent is NEVER asked for an email (their account already proves identity).
|
|
2292
2292
|
noSessionEmail: "There's no email on file for your session yet, so this report can't be saved this time. You'll be asked again next run.",
|
|
2293
2293
|
skipAlreadyDecided: (decision, path) =>
|
|
@@ -70,7 +70,7 @@ function makeAiUsageResultTool(deps = {}) {
|
|
|
70
70
|
async function handler() {
|
|
71
71
|
const session = loadAuthSession();
|
|
72
72
|
if (sessionStatus(session) !== 'active') {
|
|
73
|
-
throw new Error('no active Shakers session —
|
|
73
|
+
throw new Error('no active Shakers session — sign in with social_signin_start (or run `shakers login`) first.');
|
|
74
74
|
}
|
|
75
75
|
const endpoint = getUsageDiscoveredInventoryEndpoint();
|
|
76
76
|
if (!endpoint) {
|
|
@@ -94,7 +94,9 @@ function makeAiUsageResultTool(deps = {}) {
|
|
|
94
94
|
|
|
95
95
|
return {
|
|
96
96
|
name: 'ai_usage_result',
|
|
97
|
-
|
|
97
|
+
title: 'Results of your AI analysis',
|
|
98
|
+
description: 'Fetches from Shakers the description of each AI agent found in the analysis you approved. It reads nothing on this computer.',
|
|
99
|
+
guidance:
|
|
98
100
|
"Retrieve the enriched Shakers classification for the talent's agents after ai_usage. Shakers classifies agents and writes their descriptions server-side (no client LLM), so this reads the result: per agent { name, tools, model, category, role, level, code, whatItDoes }. Session-keyed, no arguments. Server-side classification can take a few minutes, so this does a bounded internal poll (~90s max, never a long-held call): returns { status: 'ready', agents } once every agent has been classified, or { status: 'pending', agents } while Shakers is still processing (call it again to keep waiting — 'pending' is normal progress, not an error). Present agents to the talent by NAME and category/role only; the `code` field is an internal handle for add_agent, never show it. Requires an active session.",
|
|
99
101
|
inputSchema: RESULT_INPUT_SCHEMA,
|
|
100
102
|
handler,
|
package/src/mcp-ai-usage-tool.js
CHANGED
|
@@ -385,7 +385,9 @@ function makeAiUsageTool(deps = {}) {
|
|
|
385
385
|
|
|
386
386
|
return {
|
|
387
387
|
name: 'ai_usage',
|
|
388
|
-
|
|
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:
|
|
389
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.",
|
|
390
392
|
inputSchema: AI_USAGE_INPUT_SCHEMA,
|
|
391
393
|
handler,
|
package/src/mcp-alma-tools.js
CHANGED
|
@@ -27,7 +27,7 @@ function makeAlmaTools(deps = {}) {
|
|
|
27
27
|
const requireSession = () => {
|
|
28
28
|
const session = loadAuthSession();
|
|
29
29
|
if (sessionStatus(session) !== 'active') {
|
|
30
|
-
throw new Error('no active Shakers session —
|
|
30
|
+
throw new Error('no active Shakers session — sign in with social_signin_start (or run `shakers login`) first (alma is session-gated).');
|
|
31
31
|
}
|
|
32
32
|
return session;
|
|
33
33
|
};
|
|
@@ -75,13 +75,17 @@ function makeAlmaTools(deps = {}) {
|
|
|
75
75
|
return [
|
|
76
76
|
{
|
|
77
77
|
name: 'alma_chat',
|
|
78
|
-
|
|
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.",
|
|
79
81
|
inputSchema: ALMA_CHAT_SCHEMA,
|
|
80
82
|
handler: almaChat,
|
|
81
83
|
},
|
|
82
84
|
{
|
|
83
85
|
name: 'alma_decision',
|
|
84
|
-
|
|
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.",
|
|
85
89
|
inputSchema: ALMA_DECISION_SCHEMA,
|
|
86
90
|
handler: almaDecision,
|
|
87
91
|
},
|
|
@@ -14,7 +14,7 @@ function makeApplicationsTools(deps = {}) {
|
|
|
14
14
|
const requireLoginSession = () => {
|
|
15
15
|
const session = loadAuthSession();
|
|
16
16
|
if (sessionStatus(session) !== 'active') {
|
|
17
|
-
throw new Error('no active Shakers session —
|
|
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
18
|
}
|
|
19
19
|
return session;
|
|
20
20
|
};
|
|
@@ -29,7 +29,9 @@ function makeApplicationsTools(deps = {}) {
|
|
|
29
29
|
return [
|
|
30
30
|
{
|
|
31
31
|
name: 'list_applications',
|
|
32
|
-
|
|
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.",
|
|
33
35
|
inputSchema: LIST_APPLICATIONS_SCHEMA,
|
|
34
36
|
handler: listApplications,
|
|
35
37
|
},
|
|
@@ -15,7 +15,7 @@ function makeAvailabilityTools(deps = {}) {
|
|
|
15
15
|
const requireLoginSession = () => {
|
|
16
16
|
const session = loadAuthSession();
|
|
17
17
|
if (sessionStatus(session) !== 'active') {
|
|
18
|
-
throw new Error('no active Shakers session —
|
|
18
|
+
throw new Error('no active Shakers session — sign in with social_signin_start (or run `shakers login`) first (get_availability is session-gated).');
|
|
19
19
|
}
|
|
20
20
|
return session;
|
|
21
21
|
};
|
|
@@ -30,7 +30,9 @@ function makeAvailabilityTools(deps = {}) {
|
|
|
30
30
|
return [
|
|
31
31
|
{
|
|
32
32
|
name: 'get_availability',
|
|
33
|
-
|
|
33
|
+
title: 'Your availability',
|
|
34
|
+
description: 'Shows your availability on Shakers: whether you are open to work, hours per month, work mode and location.',
|
|
35
|
+
guidance: "View the talent's availability: open-to-work, monthly hours, work modes (a combinable array of REMOTE/HYBRID/IN_PERSON), and location. Read-only. Requires an active session. Any internal ids in the result (e.g. numeric city id) are handles for tools only — never show or mention them to the talent.",
|
|
34
36
|
inputSchema: GET_AVAILABILITY_SCHEMA,
|
|
35
37
|
handler: getAvailability,
|
|
36
38
|
},
|
|
@@ -14,7 +14,7 @@ function makeCertificationsTools(deps = {}) {
|
|
|
14
14
|
const requireLoginSession = () => {
|
|
15
15
|
const session = loadAuthSession();
|
|
16
16
|
if (sessionStatus(session) !== 'active') {
|
|
17
|
-
throw new Error('no active Shakers session —
|
|
17
|
+
throw new Error('no active Shakers session — sign in with social_signin_start (or run `shakers login`) first (list_certifications is session-gated).');
|
|
18
18
|
}
|
|
19
19
|
return session;
|
|
20
20
|
};
|
|
@@ -29,7 +29,9 @@ function makeCertificationsTools(deps = {}) {
|
|
|
29
29
|
return [
|
|
30
30
|
{
|
|
31
31
|
name: 'list_certifications',
|
|
32
|
-
|
|
32
|
+
title: 'Your certifications',
|
|
33
|
+
description: 'Shows which areas of your roles are certified on Shakers.',
|
|
34
|
+
guidance: "List the talent's role dimensions with their certification state (CERTIFIED/UNCERTIFIED/EXPIRED), band and main role. Requires an active session. Any dimension/cluster ids in the result are INTERNAL handles for calling tools — never show or mention them to the talent; refer to a dimension by its name.",
|
|
33
35
|
inputSchema: LIST_CERTIFICATIONS_SCHEMA,
|
|
34
36
|
handler: listCertifications,
|
|
35
37
|
},
|
|
@@ -17,7 +17,7 @@ function makeCertifyDimensionTools(deps = {}) {
|
|
|
17
17
|
const requireLoginSession = () => {
|
|
18
18
|
const session = loadAuthSession();
|
|
19
19
|
if (sessionStatus(session) !== 'active') {
|
|
20
|
-
throw new Error('no active Shakers session —
|
|
20
|
+
throw new Error('no active Shakers session — sign in with social_signin_start (or run `shakers login`) first (certification is session-gated).');
|
|
21
21
|
}
|
|
22
22
|
return session;
|
|
23
23
|
};
|
|
@@ -51,7 +51,9 @@ function makeCertifyDimensionTools(deps = {}) {
|
|
|
51
51
|
return [
|
|
52
52
|
{
|
|
53
53
|
name: 'list_certifiable_dimensions',
|
|
54
|
-
|
|
54
|
+
title: 'What you can certify',
|
|
55
|
+
description: 'Shows which areas of your main role you can certify now and when you can retry the others.',
|
|
56
|
+
guidance: "List the dimensions the talent can certify right now: their main role's hub dimensions whose interview runs by text (terminal or spoken-case templates) and not already CERTIFIED (UNCERTIFIED/EXPIRED). Dimensions the talent cannot retake yet are NOT in `dimensions`; they come in `onCooldown` with the ISO date they free up, so tell the talent when they can retry instead of offering them. Requires an active session. The certification interview itself is done on the web; running `shakers certify` in the terminal hands the talent the web link to take it. The dimensionKey/clusterId in each item are INTERNAL handles for tools only — never show or mention them to the talent; refer to a dimension by its name.",
|
|
55
57
|
inputSchema: LIST_CERTIFIABLE_DIMENSIONS_SCHEMA,
|
|
56
58
|
handler: listCertifiableDimensions,
|
|
57
59
|
},
|
package/src/mcp-choice.js
CHANGED
|
@@ -66,4 +66,30 @@ async function askAfterTexts(ctx, { tool, texts, question, options, answer }) {
|
|
|
66
66
|
};
|
|
67
67
|
}
|
|
68
68
|
|
|
69
|
-
|
|
69
|
+
// Questions this process put to the talent in the chat, keyed by exactly what they would allow:
|
|
70
|
+
// an answer counts only for a question the tool asked, so a flag the AI sets by itself never does.
|
|
71
|
+
const pendingConsent = new Set();
|
|
72
|
+
|
|
73
|
+
// The talent's yes or no before a tool reads this computer or sends data: { granted } or { reply } to return.
|
|
74
|
+
async function askConsent(ctx, { tool, key, question, yes, no, answer }) {
|
|
75
|
+
const options = [yes, no];
|
|
76
|
+
const picked = pendingConsent.has(key) ? optionFor(answer, options) : null;
|
|
77
|
+
if (picked) {
|
|
78
|
+
pendingConsent.delete(key);
|
|
79
|
+
return { granted: picked === yes };
|
|
80
|
+
}
|
|
81
|
+
const asked = ctx && ctx.elicitation ? await askChoice(ctx, { question, options }) : { chat: chatChoice(question, options) };
|
|
82
|
+
if (asked.answer) return { granted: asked.answer === yes };
|
|
83
|
+
pendingConsent.add(key);
|
|
84
|
+
return {
|
|
85
|
+
reply: {
|
|
86
|
+
ok: false,
|
|
87
|
+
reason: 'consent-required',
|
|
88
|
+
...(asked.dismissed ? { dismissed: asked.dismissed } : {}),
|
|
89
|
+
...asked.chat,
|
|
90
|
+
message: `Nothing was read or sent. ${asked.chat.choiceMessage} Then call ${tool} again with the same arguments and answer set to the option they pick.`,
|
|
91
|
+
},
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
module.exports = { askConsent, askChoice, askAfterTexts, chatChoice, optionFor, queueNotice, takeNotices, SAY_MESSAGE };
|
|
@@ -157,13 +157,17 @@ function makeDeviceLoginTools(deps = {}) {
|
|
|
157
157
|
return [
|
|
158
158
|
{
|
|
159
159
|
name: 'social_signin_start',
|
|
160
|
-
|
|
160
|
+
title: 'Sign in to Shakers',
|
|
161
|
+
description: 'Opens a Shakers page in your browser where you sign in with Google, LinkedIn or your email. Your password never goes through the chat.',
|
|
162
|
+
guidance: "Begin sign-in (or registration) via the device flow — provider 'google' (default), 'linkedin' or 'email'. Email goes through the SAME web flow: the talent reuses their open Shakers session or signs in with their email on the page, never a password in the chat. Returns a verificationUrl and a userCode: show BOTH to the talent verbatim — they open the URL and enter the code in their own browser (they never type anything in the chat, and you never invent or fill the code). Pass mode 'register' to create the account if it is new. After showing them, call social_signin_poll. Returns { ok, status, provider, verificationUrl, userCode, pollIntervalSeconds, ... }.",
|
|
161
163
|
inputSchema: START_SCHEMA,
|
|
162
164
|
handler: start,
|
|
163
165
|
},
|
|
164
166
|
{
|
|
165
167
|
name: 'social_signin_poll',
|
|
166
|
-
|
|
168
|
+
title: 'Finish signing in',
|
|
169
|
+
description: 'Checks whether you finished signing in on that page and, if so, keeps you signed in on this computer.',
|
|
170
|
+
guidance: "Check whether the talent has finished the social sign-in started by social_signin_start, and persist the session when they have. Call it after showing the code; if it returns status 'pending', wait pollIntervalSeconds and call it again — the 'pending' response repeats the verificationUrl and userCode so you can keep the clickable link in front of the talent. Returns status 'authorized' (session saved, with isNewUser + method), 'pending', 'denied', or 'expired'.",
|
|
167
171
|
inputSchema: POLL_SCHEMA,
|
|
168
172
|
handler: poll,
|
|
169
173
|
},
|
|
@@ -28,7 +28,7 @@ function makeFindProjectsTools(deps = {}) {
|
|
|
28
28
|
const requireLoginSession = () => {
|
|
29
29
|
const session = loadAuthSession();
|
|
30
30
|
if (sessionStatus(session) !== 'active') {
|
|
31
|
-
throw new Error('no active Shakers session —
|
|
31
|
+
throw new Error('no active Shakers session — sign in with social_signin_start (or run `shakers login`) first (find_projects is session-gated).');
|
|
32
32
|
}
|
|
33
33
|
return session;
|
|
34
34
|
};
|
|
@@ -54,7 +54,9 @@ function makeFindProjectsTools(deps = {}) {
|
|
|
54
54
|
return [
|
|
55
55
|
{
|
|
56
56
|
name: 'find_projects',
|
|
57
|
-
|
|
57
|
+
title: 'Find projects',
|
|
58
|
+
description: 'Shows the open Shakers projects that match you best.',
|
|
59
|
+
guidance: "List the Positions/Projects available to the talent right now (Shakers Find Positions), paginated and ordered by match desc. Each item carries its parent Project id/name, title, company, budget, work mode and match. Supports server-side pagination (page/limit) and filters (tab all|saved, attendance, country ISO2). `meta` carries {page,pageSize,total,totalPages}. Requires an active session. The positionId/projectId in each item are INTERNAL handles for calling other tools (e.g. show_project, save_project) — NEVER show or mention them to the talent; refer to a project by its title/company only.",
|
|
58
60
|
inputSchema: FIND_PROJECTS_SCHEMA,
|
|
59
61
|
handler: findProjects,
|
|
60
62
|
},
|