shakers-cli-beta 0.8.0-beta.5 → 0.10.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 +39 -9
- package/bin/ai-usage.js +37 -19
- package/bin/login.js +1 -1
- package/bin/mcp.js +25 -21
- package/bin/mcpb.js +5 -0
- package/package.json +3 -2
- package/src/auth-session-store.js +1 -0
- package/src/authorship.js +7 -6
- package/src/backend-request.js +2 -2
- package/src/certify-dimension-client.js +0 -54
- package/src/certify-dimension-flow.js +20 -81
- package/src/config.js +29 -8
- package/src/cv-file.js +62 -0
- package/src/device-login.js +5 -3
- package/src/find-projects-client.js +16 -6
- package/src/git-activity.js +8 -7
- package/src/git-log-prefetch.js +53 -0
- package/src/i18n.js +33 -58
- package/src/mcp-ai-usage-tool.js +150 -65
- package/src/mcp-auth-form.js +213 -44
- package/src/mcp-certify-dimension-tools.js +2 -2
- package/src/mcp-choice.js +47 -0
- package/src/mcp-install.js +32 -3
- package/src/mcp-instructions.js +27 -0
- package/src/mcp-onboarding-tools.js +3 -3
- package/src/mcp-prompts.js +73 -0
- package/src/mcp-rate-tools.js +2 -2
- package/src/mcp-register-tools.js +89 -313
- package/src/mcp-role-tools.js +177 -16
- package/src/mcp-server.js +108 -4
- package/src/mcp-signup-client.js +118 -0
- package/src/mcp-signup-tools.js +818 -0
- package/src/onboarding-client.js +28 -52
- package/src/onboarding-flow.js +3 -147
- package/src/onboarding-status-client.js +16 -1
- package/src/register-context.js +1 -17
- package/src/roles-client.js +3 -1
- package/src/scanner.js +13 -1
- package/src/session-normalizers.js +174 -0
- package/src/session-scan.js +149 -10
- package/src/signup-client.js +33 -21
- package/src/signup-copy.js +338 -0
- package/src/signup-language.js +21 -0
- package/src/usage-run-steps.js +3 -2
- package/src/work-streams.js +7 -2
|
@@ -1,163 +1,9 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
const {
|
|
4
|
-
FREELANCE_TYPES, WORK_SITUATIONS, EMPLOYMENT_PARTICIPATIONS, FREELANCE_OPINIONS,
|
|
5
|
-
CHANGE_MOTIVATORS, WORK_MODES, MONTHLY_HOURS, LANGUAGE_CODES, LANGUAGE_LEVELS,
|
|
6
|
-
} = require('./onboarding-flow');
|
|
7
|
-
const { getCatalog } = require('./i18n');
|
|
8
3
|
const { TranscriptBuffer } = require('./interview-transcript');
|
|
4
|
+
const { askChoice } = require('./mcp-choice');
|
|
9
5
|
|
|
10
|
-
const
|
|
11
|
-
const MONTHLY_HOURS_SLASHED = MONTHLY_HOURS.join('/');
|
|
12
|
-
|
|
13
|
-
// "Friendly label (CODE)" list from an i18n label map: the talent sees the label, the model passes the canonical code.
|
|
14
|
-
function labelledOptions(codes, labels) {
|
|
15
|
-
return codes.map((code) => `${labels[code] || code} (${code})`).join(', ');
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
function buildSchemas(lang) {
|
|
19
|
-
const L = getCatalog(lang).onboarding;
|
|
20
|
-
const showLabel = 'Show the talent the label, pass the code';
|
|
21
|
-
|
|
22
|
-
const ACCOUNT_FIELDS = {
|
|
23
|
-
name: { type: 'string', description: "The talent's first name, as they give it in the chat." },
|
|
24
|
-
lastName: { type: 'string', description: "The talent's last name, as they give it in the chat." },
|
|
25
|
-
email: { type: 'string', description: "The talent's own email address, used as their account login. They give it in the chat." },
|
|
26
|
-
preferredLanguage: { type: 'string', enum: ['ES', 'EN', 'IT', 'PT', 'FR'], description: 'Preferred language as an UPPERCASE ISO code. Optional — defaults to the session language.' },
|
|
27
|
-
newsletterConsent: { type: 'boolean', description: 'Whether the talent agrees to receive the newsletter. Optional, defaults to false.' },
|
|
28
|
-
freelanceType: { type: 'string', enum: FREELANCE_TYPES, description: `How the talent is set up to work. ${showLabel}: ${labelledOptions(FREELANCE_TYPES, L.freelanceTypeLabels)}. Distinct from workSituation below (that is the CURRENT freelance/employed/etc situation).` },
|
|
29
|
-
};
|
|
30
|
-
|
|
31
|
-
const WORK_SITUATION_SCHEMA = {
|
|
32
|
-
type: 'object',
|
|
33
|
-
description: "The talent's current work situation (ADR-036), asked once after account creation — the same question the web's registration flow asks. NOT the same thing as freelanceType above.",
|
|
34
|
-
properties: {
|
|
35
|
-
situation: { type: 'string', enum: WORK_SITUATIONS, description: `Current work situation. ${showLabel}: ${labelledOptions(WORK_SITUATIONS, L.workSituationLabels)}. 'EMPLOYED' requires participation below.` },
|
|
36
|
-
participation: { type: 'string', enum: EMPLOYMENT_PARTICIPATIONS, description: `Only when situation is 'EMPLOYED'. ${showLabel}: ${labelledOptions(EMPLOYMENT_PARTICIPATIONS, L.employmentParticipationLabels)}.` },
|
|
37
|
-
opinion: { type: 'string', enum: FREELANCE_OPINIONS, description: `The talent's opinion on freelancing. ${showLabel}: ${labelledOptions(FREELANCE_OPINIONS, L.freelanceOpinionLabels)}. Required unless situation is 'FREELANCE' (implied ALREADY_FREELANCE).` },
|
|
38
|
-
changeMotivators: { type: 'string', enum: CHANGE_MOTIVATORS, description: `What motivates the talent about freelancing/Shakers. ${showLabel}: ${labelledOptions(CHANGE_MOTIVATORS, L.changeMotivatorLabels)}. Required only when the talent is or is considering freelance (situation 'FREELANCE', or opinion 'WAS_FREELANCE_BEFORE'/'OPEN_TO_FREELANCE'); omit when opinion is 'NOT_INTERESTED'.` },
|
|
39
|
-
},
|
|
40
|
-
required: ['situation'],
|
|
41
|
-
};
|
|
42
|
-
|
|
43
|
-
const LANGUAGES_SCHEMA = {
|
|
44
|
-
type: 'array',
|
|
45
|
-
description: `Languages the talent speaks (ADR-036). ALWAYS ask this — it must not be skipped. ${showLabel}. Language options: ${labelledOptions(LANGUAGE_CODES, L.languageNames)}; and for each language a level: ${labelledOptions(LANGUAGE_LEVELS, L.languageLevelLabels)}. Bulk upsert — send the talent’s FULL language list. Omit only if the talent explicitly declines.`,
|
|
46
|
-
items: {
|
|
47
|
-
type: 'object',
|
|
48
|
-
properties: {
|
|
49
|
-
language: { type: 'string', enum: LANGUAGE_CODES, description: `Language code. ${showLabel}: ${labelledOptions(LANGUAGE_CODES, L.languageNames)}.` },
|
|
50
|
-
level: { type: 'string', enum: LANGUAGE_LEVELS, description: `Language level. ${showLabel}: ${labelledOptions(LANGUAGE_LEVELS, L.languageLevelLabels)}.` },
|
|
51
|
-
},
|
|
52
|
-
required: ['language', 'level'],
|
|
53
|
-
},
|
|
54
|
-
};
|
|
55
|
-
|
|
56
|
-
const AVAILABILITY_SCHEMA = {
|
|
57
|
-
type: 'object',
|
|
58
|
-
description: `Availability AND location (ADR-028). ALWAYS ask this — it must not be skipped. There is NO separate location field: the talent's LOCATION (country, timezone, region) lives here. Offer the options: available (yes/no); monthlyHours must be one of ${MONTHLY_HOURS_QUOTED}; workModes is a MULTI-VALUE array, any combination of ${WORK_MODES.join('/')} (the talent can pick several); longFullTimeProjects (yes/no — open to long full-time projects). For location, ask the talent their country and timezone (region is optional). All fields optional/nullable, but collect what you can. monthlyHours is a STRICT enum string (not an int). city is not collected here.`,
|
|
59
|
-
properties: {
|
|
60
|
-
available: { type: 'boolean', description: 'Whether the talent is currently available (yes/no).' },
|
|
61
|
-
monthlyHours: { type: 'string', enum: MONTHLY_HOURS, description: `Monthly hours the talent can work, exactly one of: ${MONTHLY_HOURS_QUOTED} (send '> 160' verbatim, with the space).` },
|
|
62
|
-
workModes: { type: 'array', items: { type: 'string', enum: WORK_MODES }, description: `Work modes the talent accepts — a combinable array, any subset of ${WORK_MODES.join('/')} (e.g. ['REMOTE','HYBRID']). Only REMOTE means the talent will not see hybrid or on-site projects.` },
|
|
63
|
-
country: { type: 'string', description: 'LOCATION — ISO-2 country code, 2 letters (e.g. ES, US, GB).' },
|
|
64
|
-
timezone: { type: 'string', description: 'LOCATION — IANA timezone (e.g. Europe/Madrid, America/New_York). Max 100 chars.' },
|
|
65
|
-
subdivision: { type: 'string', description: 'LOCATION — optional region/state code (e.g. ES-CT). Max 10 chars.' },
|
|
66
|
-
longFullTimeProjects: { type: 'boolean', description: 'Whether the talent is open to long full-time projects (yes/no).' },
|
|
67
|
-
},
|
|
68
|
-
};
|
|
69
|
-
|
|
70
|
-
const REGISTER_SCHEMA = {
|
|
71
|
-
type: 'object',
|
|
72
|
-
properties: {
|
|
73
|
-
...ACCOUNT_FIELDS,
|
|
74
|
-
profile: PROFILE_SCHEMA,
|
|
75
|
-
workSituation: WORK_SITUATION_SCHEMA,
|
|
76
|
-
pricing: PRICING_SCHEMA,
|
|
77
|
-
availability: AVAILABILITY_SCHEMA,
|
|
78
|
-
languages: LANGUAGES_SCHEMA,
|
|
79
|
-
phone: PHONE_SCHEMA,
|
|
80
|
-
},
|
|
81
|
-
required: ['name', 'lastName', 'email', 'freelanceType', 'profile', 'workSituation', 'pricing'],
|
|
82
|
-
};
|
|
83
|
-
|
|
84
|
-
// Same shape as REGISTER_SCHEMA but NOTHING is required: the preview accepts a
|
|
85
|
-
// partial, pre-filled payload and reports what is still missing.
|
|
86
|
-
const PREVIEW_SCHEMA = {
|
|
87
|
-
type: 'object',
|
|
88
|
-
description: 'A partial, pre-filled register payload to REVIEW before writing. Every field is optional — fill only what the talent has said or what is verifiable from context, omit the rest. Returns what WOULD be written, what is still missing to ask, and any invalid values. Writes nothing.',
|
|
89
|
-
properties: { ...REGISTER_SCHEMA.properties },
|
|
90
|
-
};
|
|
91
|
-
|
|
92
|
-
return { ACCOUNT_FIELDS, WORK_SITUATION_SCHEMA, LANGUAGES_SCHEMA, AVAILABILITY_SCHEMA, REGISTER_SCHEMA, PREVIEW_SCHEMA };
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
// The register tool's own long description, per language: the batched
|
|
96
|
-
// collection guide with friendly labels inline for every choice field.
|
|
97
|
-
function buildRegisterDescription(lang) {
|
|
98
|
-
const L = getCatalog(lang).onboarding;
|
|
99
|
-
return "The talent registers THEMSELVES on Shakers (self-service; no prior session needed). Your job is to relay the non-secret profile details the talent gives you in the chat as arguments. Credentials never touch the chat or any tool argument: the tool opens a sign-up window on the talent's own machine (127.0.0.1) where they either Continue with Google (no password at all) or type an email and password directly into that local window.\n\nGO STRAIGHT TO A FILLED DRAFT — never open with a questionnaire or a list of what you do not know. (1) Your FIRST reply is one short question: may you search for and read their CV on this machine to fill everything in? (skip it if they already said yes). (2) With their yes: call suggest_register_context with talentName = the name you know, then read_cv the candidates (nameMatch first, then generic ones like cv.pdf) with talentConsent:true and talentName, until one returns mentionsTalent:true; a CV with mentionsTalent:false belongs to someone else: drop it and never show or use anything from it. From their CV take LinkedIn/GitHub (links), experience, education, languages and location; pass paths exactly as given (the short ~/ form is fine). If they say no, there is no CV, or none of the CVs is theirs, do NOT fall back to questions: build the same draft from everything else you know about them — your memory of past conversations, a search of those conversations if you have that tool, this conversation and any context or memory files you can see; the CV adds facts, it is never a precondition. (3) Pre-fill EVERY field: identity from git/your memory/the CV; workSituation from what you know (a company email or employer in memory or CV means employed there — propose it); `pricing` ALWAYS as a concrete per-project estimate from their role, seniority, stack and location, labelled as your estimate (ask only if you know nothing about their work); `workSituation.changeMotivators` as your best match; availability, languages and country from context. What you know about the person outranks git: if the git identity is someone else, ignore it. (4) Call register_preview and show ONE summary that says where each value comes from, for the talent to confirm or correct in one reply. Required fields you truly have no basis for go in one short question at the end; optional unknowns (phone, website, newsletter) in a single 'si quieres' line, never as missing items. (5) After their confirmation, call register. The sign-up goes to suggest_register_context's `environment` (local/development/staging/production): name it correctly and never call a test environment production. All of this is self-declared profile data.\n\nFor whatever you still have to ASK (only what you could not infer), ask in ONE grouped message, not one field at a time, and for every CHOICE field PRESENT THE VALID OPTIONS so the talent just picks one. For every choice field SHOW THE TALENT THE FRIENDLY LABEL, never the raw code (e.g. show 'Nativo', not 'NATIVE'), and pass the code as the value. Fields, grouped:\n1. Identity — name, lastName, email (optional: preferredLanguage, newsletterConsent).\n2. Profile — `profile.linkedinUrl` (required source; optional GitHub/website/CV path).\n3. Work — freelanceType (offer: "
|
|
100
|
-
+ labelledOptions(FREELANCE_TYPES, L.freelanceTypeLabels)
|
|
101
|
-
+ ") and `workSituation` (offer situation: "
|
|
102
|
-
+ labelledOptions(WORK_SITUATIONS, L.workSituationLabels)
|
|
103
|
-
+ "; participation "
|
|
104
|
-
+ labelledOptions(EMPLOYMENT_PARTICIPATIONS, L.employmentParticipationLabels)
|
|
105
|
-
+ " only when EMPLOYED; opinion "
|
|
106
|
-
+ labelledOptions(FREELANCE_OPINIONS, L.freelanceOpinionLabels)
|
|
107
|
-
+ " unless FREELANCE; changeMotivators "
|
|
108
|
-
+ labelledOptions(CHANGE_MOTIVATORS, L.changeMotivatorLabels)
|
|
109
|
-
+ " when the talent is or is considering freelance).\n4. Pricing — per-PROJECT price; currency USD, EUR or GBP.\n5. Availability + languages — `availability` (offer monthlyHours "
|
|
110
|
-
+ MONTHLY_HOURS_SLASHED
|
|
111
|
-
+ ", workModes as a combinable array of " + WORK_MODES.join('/') + ", longFullTimeProjects yes/no, and LOCATION: country as ISO-2 e.g. ES, timezone as IANA e.g. Europe/Madrid) and `languages` (offer "
|
|
112
|
-
+ labelledOptions(LANGUAGE_CODES, L.languageNames)
|
|
113
|
-
+ ", each with a level "
|
|
114
|
-
+ labelledOptions(LANGUAGE_LEVELS, L.languageLevelLabels)
|
|
115
|
-
+ ").\n6. Contact — `phone` (telephoneCode e.g. +34 and telephoneNumber).\n\nALWAYS ask for availability (including location), languages and phone — they are expected, not optional extras; only omit one if the talent explicitly declines. There is NO separate location field: location (country/timezone/region) is part of `availability`.\n\nOnce the window completes the account exists and the session is set; the tool then starts the background profile import, saves the work situation, per-PROJECT pricing (ADR-024), and any availability/languages/phone provided. If the account already exists, the tool returns { accountAlreadyExists: true } — switch to the login tool (method 'google') instead. Returns { importJobId, next, disclaimers, profileUrl }. `next` is 'ai_usage' when the talent has no prior AI-usage evidence (offer step 4, the ai_usage tool) or 'onboarding_interview_start' when they already do (step 4 is skipped). When you run ai_usage as this step, pass the returned `usageRepoScope` ({ mode: 'all' }) — registration always measures the whole machine, never a single repo. Before running ai_usage OR onboarding_interview_start, show the talent the matching `disclaimers` block, its two parts (info accessed, goal+duration) separately. Poll register_status with the importJobId to track the import. Always finish any path by calling onboarding_finish (whether or not the talent takes steps 4 and 5) — that is what marks the onboarding complete and moves the talent's registration level.";
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
const PHONE_SCHEMA = {
|
|
119
|
-
type: 'object',
|
|
120
|
-
description: "Phone (ADR-036). ALWAYS ask this — it must not be skipped. Two parts: telephoneCode = the international dialling code (e.g. +34, +1, +44), telephoneNumber = the local number. Omit only if the talent explicitly declines to share it.",
|
|
121
|
-
properties: {
|
|
122
|
-
telephoneCode: { type: 'string', description: "International dialling code, e.g. '+34', '+1', '+44'. Max 10 chars." },
|
|
123
|
-
telephoneNumber: { type: 'string', description: 'Local phone number. Max 20 chars.' },
|
|
124
|
-
},
|
|
125
|
-
required: ['telephoneCode', 'telephoneNumber'],
|
|
126
|
-
};
|
|
127
|
-
|
|
128
|
-
const PROFILE_SCHEMA = {
|
|
129
|
-
type: 'object',
|
|
130
|
-
properties: {
|
|
131
|
-
linkedinUrl: { type: 'string', description: "The talent's LinkedIn URL. Required — it is the mandatory profile source." },
|
|
132
|
-
cvPath: { type: 'string', description: 'Path to the CV (PDF/DOC/DOCX, max 10 MB) on this machine, absolute or ~/... (optional). Only a CV the talent confirmed is theirs. The FILE is uploaded to Shakers with the profile import.' },
|
|
133
|
-
githubUrl: { type: 'string', description: 'GitHub URL (optional).' },
|
|
134
|
-
websiteUrl: { type: 'string', description: 'Personal website URL (optional).' },
|
|
135
|
-
},
|
|
136
|
-
required: ['linkedinUrl'],
|
|
137
|
-
};
|
|
138
|
-
|
|
139
|
-
const PRICING_SCHEMA = {
|
|
140
|
-
type: 'object',
|
|
141
|
-
description: 'Per-PROJECT pricing (ADR-024). NOT hourly or annual. Amounts are the whole project price as a plain number, no thousands separators, at most 2 decimals, 0..999999.99. Currency is USD, EUR or GBP. The full-time (standard) price is the primary one and should always be captured, exactly as the CLI asks it; part-time is an additional modality. At least one variant must be selected.',
|
|
142
|
-
properties: {
|
|
143
|
-
fullTimeSelected: { type: 'boolean', description: 'Whether the talent offers full-time projects. This standard/full-time price is the primary one — capture it by default.' },
|
|
144
|
-
fullTimeAmount: { type: 'number', description: 'Full-time (standard) per-project price, plain number without thousands separators (e.g. 60000).' },
|
|
145
|
-
fullTimeCurrency: { type: 'string', enum: ['USD', 'EUR', 'GBP'], description: 'Currency for the full-time price.' },
|
|
146
|
-
partTimeSelected: { type: 'boolean', description: 'Whether the talent also offers part-time projects (additional modality).' },
|
|
147
|
-
partTimeAmount: { type: 'number', description: 'Part-time per-project price, plain number without thousands separators (e.g. 50).' },
|
|
148
|
-
partTimeCurrency: { type: 'string', enum: ['USD', 'EUR', 'GBP'], description: 'Currency for the part-time price.' },
|
|
149
|
-
},
|
|
150
|
-
};
|
|
151
|
-
|
|
152
|
-
// Default (English) static exports for callers/tests that read the module-level
|
|
153
|
-
// schemas directly. The live tools rebuild these per session language.
|
|
154
|
-
const { ACCOUNT_FIELDS, WORK_SITUATION_SCHEMA, LANGUAGES_SCHEMA, AVAILABILITY_SCHEMA, REGISTER_SCHEMA, PREVIEW_SCHEMA } = buildSchemas('en');
|
|
155
|
-
|
|
156
|
-
const STATUS_SCHEMA = {
|
|
157
|
-
type: 'object',
|
|
158
|
-
properties: { jobId: { type: 'string', description: 'The importJobId returned by register — an internal handle for polling; never shown or mentioned to the talent.' } },
|
|
159
|
-
required: ['jobId'],
|
|
160
|
-
};
|
|
6
|
+
const ANSWER_MODES = ['drafted', 'own', 'full'];
|
|
161
7
|
|
|
162
8
|
const INTERVIEW_START_SCHEMA = {
|
|
163
9
|
type: 'object',
|
|
@@ -166,7 +12,17 @@ const INTERVIEW_START_SCHEMA = {
|
|
|
166
12
|
language: { type: 'string', enum: ['es', 'en'] },
|
|
167
13
|
disclaimerAcknowledged: {
|
|
168
14
|
type: 'boolean',
|
|
169
|
-
description: 'Must be true. Set it only after showing the talent
|
|
15
|
+
description: 'Must be true. Set it only after showing the talent both interview disclaimers word for word, each as its own block (from signup_status or from this tool called without it), and getting their agreement.',
|
|
16
|
+
},
|
|
17
|
+
where: {
|
|
18
|
+
type: 'string',
|
|
19
|
+
enum: ['here', 'later'],
|
|
20
|
+
description: "The talent's pick to 'here now or later on the web?'. Omit it to have the tool ask: in a dialog when the client has one, otherwise it returns the question with its options.",
|
|
21
|
+
},
|
|
22
|
+
answerMode: {
|
|
23
|
+
type: 'string',
|
|
24
|
+
enum: ANSWER_MODES,
|
|
25
|
+
description: "The talent's pick to the answer-mode question: drafted = you propose each answer and they approve or edit it, own = they answer themselves, full = you answer every question yourself. Required with disclaimerAcknowledged:true; omit it to have the tool ask, in a dialog or as options it returns.",
|
|
170
26
|
},
|
|
171
27
|
confirmRepeat: {
|
|
172
28
|
type: 'boolean',
|
|
@@ -180,7 +36,7 @@ const INTERVIEW_TURN_SCHEMA = {
|
|
|
180
36
|
type: 'object',
|
|
181
37
|
properties: {
|
|
182
38
|
interviewId: { type: 'string', description: 'Internal handle for this interview — for calling the interview tools ONLY; never shown or mentioned to the talent.' },
|
|
183
|
-
message: { type: 'string', description: "The talent's answer to the current question: in
|
|
39
|
+
message: { type: 'string', description: "The talent's answer to the current question: in 'own' mode their REAL, VERBATIM words; in 'drafted' mode the answer they APPROVED or edited after seeing your proposal; in 'full' mode (or once they explicitly asked you to answer the rest yourself) your own answer built only from what they confirmed. Outside 'full' mode never a value they have not seen and confirmed: if they have not answered or approved yet, do NOT call this." },
|
|
184
40
|
},
|
|
185
41
|
required: ['interviewId', 'message'],
|
|
186
42
|
};
|
|
@@ -193,26 +49,37 @@ const INTERVIEW_COMPLETE_SCHEMA = {
|
|
|
193
49
|
|
|
194
50
|
const FINISH_SCHEMA = { type: 'object', properties: {} };
|
|
195
51
|
|
|
52
|
+
const LIVEKIT_LOAD_FAILURES = new Set(['livekit-not-installed', 'livekit-no-text-streams']);
|
|
53
|
+
|
|
54
|
+
// No telemetry ships in this package: stderr reaches the MCP client's server log, with what is needed to size the gap.
|
|
55
|
+
function defaultReportLivekitFailure(kind, error) {
|
|
56
|
+
const cause = error && error.cause && error.cause.message ? error.cause.message : (error && error.message) || '';
|
|
57
|
+
process.stderr.write(` [mcp] livekit-unavailable kind=${kind} os=${process.platform} arch=${process.arch} node=${process.version} cause=${JSON.stringify(cause.slice(0, 300))}\n`);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// The last interview this process completed: the main-role step waits for its evaluation.
|
|
61
|
+
let lastCompletedInterview = null;
|
|
62
|
+
|
|
63
|
+
function completedOnboardingInterview() {
|
|
64
|
+
return lastCompletedInterview;
|
|
65
|
+
}
|
|
66
|
+
|
|
196
67
|
function makeRegisterTools(deps = {}) {
|
|
197
68
|
const flow = require('./onboarding-flow');
|
|
198
69
|
const resolved = flow.makeDeps(deps.flowDeps || {});
|
|
199
70
|
const lang = deps.lang || 'en';
|
|
200
|
-
const schemas = buildSchemas(lang);
|
|
201
|
-
const registerDescription = buildRegisterDescription(lang);
|
|
202
71
|
// Same confirmation copy the CLI prints ("Price saved.", …), reused verbatim.
|
|
203
72
|
const catalog = require('./i18n').getCatalog(lang).onboarding;
|
|
204
|
-
|
|
73
|
+
// The talent's sign-up language, for what this tool asks them in the chat.
|
|
74
|
+
const textLang = () => require('./signup-language').signupLanguage(lang);
|
|
75
|
+
const copy = () => require('./signup-copy').signupCopy(textLang());
|
|
205
76
|
const checkCompleted = deps.checkOnboardingCompleted || require('./onboarding-availability').checkOnboardingCompleted;
|
|
206
77
|
|
|
207
78
|
// The LiveKit room is a live WebRTC connection that must survive between the
|
|
208
79
|
// separate start/turn/complete JSON-RPC calls of this long-lived stdio server.
|
|
209
80
|
const interviewSessions = new Map();
|
|
210
81
|
|
|
211
|
-
|
|
212
|
-
if (kind === 'livekit-not-installed') return catalog.livekitMissing;
|
|
213
|
-
if (kind === 'livekit-no-text-streams') return catalog.livekitOld;
|
|
214
|
-
return catalog.livekitConnectError;
|
|
215
|
-
}
|
|
82
|
+
const reportLivekitFailure = deps.reportLivekitFailure || defaultReportLivekitFailure;
|
|
216
83
|
|
|
217
84
|
async function teardownInterview(interviewId, { persist = false } = {}) {
|
|
218
85
|
const sess = interviewSessions.get(interviewId);
|
|
@@ -231,112 +98,29 @@ function makeRegisterTools(deps = {}) {
|
|
|
231
98
|
return saved;
|
|
232
99
|
}
|
|
233
100
|
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
lastName: args.lastName,
|
|
238
|
-
email: args.email,
|
|
239
|
-
preferredLanguage: args.preferredLanguage,
|
|
240
|
-
newsletterConsent: args.newsletterConsent === true,
|
|
241
|
-
freelanceType: args.freelanceType,
|
|
242
|
-
};
|
|
243
|
-
const check = flow.validateRegisterFields(registerFields);
|
|
244
|
-
if (!check.ok) return { ok: false, step: 'signup', reason: check.reason };
|
|
245
|
-
|
|
246
|
-
const created = await runLoopbackAuth({ mode: 'register', deps: resolved, lang, registerFields });
|
|
247
|
-
if (created.viaGoogle) {
|
|
248
|
-
// Social registration is the device flow (start/poll tools), not this single blocking call.
|
|
249
|
-
return {
|
|
250
|
-
ok: false,
|
|
251
|
-
step: 'signup',
|
|
252
|
-
reason: 'use-device-flow',
|
|
253
|
-
next: 'social_signin_start',
|
|
254
|
-
mode: 'register',
|
|
255
|
-
message: `To register with a social provider, call social_signin_start (provider "google" or "linkedin", mode "register", freelanceType "${registerFields.freelanceType || ''}") to get a URL and code to show the talent, then social_signin_poll. Once it returns authorized, run the profile steps (import, work situation, pricing).`,
|
|
256
|
-
};
|
|
257
|
-
}
|
|
258
|
-
if (!created.ok) {
|
|
259
|
-
return { ok: false, step: 'signup', reason: created.reason };
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
const profile = args.profile || {};
|
|
263
|
-
const importRes = await flow.startProfileImport(resolved, {
|
|
264
|
-
linkedinUrl: profile.linkedinUrl,
|
|
265
|
-
cvPath: profile.cvPath,
|
|
266
|
-
githubUrl: profile.githubUrl,
|
|
267
|
-
websiteUrl: profile.websiteUrl,
|
|
268
|
-
language: lang,
|
|
269
|
-
});
|
|
270
|
-
const importOutcome = importRes.ok
|
|
271
|
-
? { ok: true, jobId: importRes.jobId, state: importRes.state }
|
|
272
|
-
: { ok: false, reason: importRes.reason, warning: 'Profile import is unavailable right now; registration continues and the import can be retried later.' };
|
|
273
|
-
|
|
274
|
-
const workSituationRes = await flow.saveWorkSituation(resolved, args.workSituation || {});
|
|
275
|
-
if (!workSituationRes.ok) return { ok: false, step: 'work-situation', reason: workSituationRes.reason };
|
|
276
|
-
|
|
277
|
-
const priceRes = await flow.savePricing(resolved, args.pricing || {});
|
|
278
|
-
if (!priceRes.ok) return { ok: false, step: 'pricing', reason: priceRes.reason };
|
|
279
|
-
|
|
280
|
-
let availabilityOutcome = { skipped: true };
|
|
281
|
-
if (args.availability && typeof args.availability === 'object') {
|
|
282
|
-
const availRes = await flow.saveAvailability(resolved, args.availability);
|
|
283
|
-
availabilityOutcome = availRes.ok
|
|
284
|
-
? { ok: true }
|
|
285
|
-
: { ok: false, reason: availRes.reason, warning: 'Availability could not be saved; registration continues and it can be set later.' };
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
let languagesOutcome = { skipped: true };
|
|
289
|
-
if (Array.isArray(args.languages) && args.languages.length > 0) {
|
|
290
|
-
const langRes = await flow.saveLanguages(resolved, args.languages);
|
|
291
|
-
languagesOutcome = langRes.ok
|
|
292
|
-
? { ok: true }
|
|
293
|
-
: { ok: false, reason: langRes.reason, unresolved: langRes.unresolved, warning: 'Languages could not be saved; registration continues and they can be added later.' };
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
let phoneOutcome = { skipped: true };
|
|
297
|
-
if (args.phone && typeof args.phone === 'object') {
|
|
298
|
-
const phoneRes = await flow.savePhone(resolved, args.phone);
|
|
299
|
-
phoneOutcome = phoneRes.ok
|
|
300
|
-
? { ok: true }
|
|
301
|
-
: { ok: false, reason: phoneRes.reason, warning: 'Phone could not be saved; registration continues and it can be set later.' };
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
const prior = await flow.detectPriorUsage(resolved);
|
|
305
|
-
const hasEvidence = prior.ok ? prior.hasEvidence : null;
|
|
306
|
-
|
|
307
|
-
// Human-readable confirmations, identical to the CLI step notices.
|
|
308
|
-
const confirmations = [catalog.accountReady, catalog.professionalSaved, catalog.pricingSaved];
|
|
309
|
-
if (importOutcome.ok) confirmations.push(catalog.importDone);
|
|
310
|
-
if (availabilityOutcome.ok) confirmations.push(catalog.availabilitySaved);
|
|
311
|
-
if (languagesOutcome.ok) confirmations.push(catalog.languagesSaved);
|
|
312
|
-
if (phoneOutcome.ok) confirmations.push(catalog.phoneSaved);
|
|
101
|
+
function interviewLater() {
|
|
102
|
+
return { ok: false, reason: 'interview-later', message: 'The talent does the interview later on the web: skip it here and go on with the main role, then open_web.' };
|
|
103
|
+
}
|
|
313
104
|
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
languages: languagesOutcome,
|
|
322
|
-
phone: phoneOutcome,
|
|
323
|
-
priorUsage: { hasEvidence },
|
|
324
|
-
next: hasEvidence === true ? 'onboarding_interview_start' : 'ai_usage',
|
|
325
|
-
usageRepoScope: { mode: 'all' },
|
|
326
|
-
disclaimers: {
|
|
327
|
-
usage: flow.usageDisclaimers(lang),
|
|
328
|
-
interview: flow.interviewDisclaimers(lang),
|
|
329
|
-
},
|
|
330
|
-
profileUrl: flow.profileUrl(resolved),
|
|
331
|
-
};
|
|
105
|
+
async function askWhere(args, ctx) {
|
|
106
|
+
if (args.where === 'here') return { here: true };
|
|
107
|
+
if (args.where === 'later') return { here: false };
|
|
108
|
+
const c = copy();
|
|
109
|
+
const asked = await askChoice(ctx, { question: c.interviewWhereQuestion, options: [c.interviewHere, c.interviewLater] });
|
|
110
|
+
if (asked.answer) return { here: asked.answer === c.interviewHere };
|
|
111
|
+
return { chat: asked.chat, dismissed: asked.dismissed };
|
|
332
112
|
}
|
|
333
113
|
|
|
334
|
-
async function
|
|
335
|
-
|
|
336
|
-
|
|
114
|
+
async function askAnswerMode(args, ctx) {
|
|
115
|
+
if (ANSWER_MODES.includes(args.answerMode)) return { mode: args.answerMode };
|
|
116
|
+
const c = copy();
|
|
117
|
+
const labels = { drafted: c.answerModeDrafted, own: c.answerModeOwn, full: c.answerModeFull };
|
|
118
|
+
const asked = await askChoice(ctx, { question: c.answerModeQuestion, options: ANSWER_MODES.map((m) => labels[m]) });
|
|
119
|
+
if (asked.answer) return { mode: ANSWER_MODES.find((m) => labels[m] === asked.answer) };
|
|
120
|
+
return { chat: asked.chat, dismissed: asked.dismissed };
|
|
337
121
|
}
|
|
338
122
|
|
|
339
|
-
async function interviewStart(args = {}) {
|
|
123
|
+
async function interviewStart(args = {}, ctx = {}) {
|
|
340
124
|
let alreadyCompleted = false;
|
|
341
125
|
try {
|
|
342
126
|
alreadyCompleted = await checkCompleted({});
|
|
@@ -353,18 +137,39 @@ function makeRegisterTools(deps = {}) {
|
|
|
353
137
|
};
|
|
354
138
|
}
|
|
355
139
|
if (args.disclaimerAcknowledged !== true) {
|
|
140
|
+
const where = await askWhere(args, ctx);
|
|
141
|
+
if (where.here === false) return interviewLater();
|
|
142
|
+
const disclaimers = flow.interviewDisclaimers(textLang());
|
|
143
|
+
const show = 'show the talent each relayVerbatim text word for word, each as its own block, with no titles, labels or bold of your own, then call again with disclaimerAcknowledged:true.';
|
|
356
144
|
return {
|
|
357
145
|
ok: false,
|
|
358
146
|
reason: 'disclaimers-not-acknowledged',
|
|
359
|
-
|
|
360
|
-
|
|
147
|
+
relayVerbatim: [disclaimers.infoAccessed, disclaimers.goalDuration],
|
|
148
|
+
...(where.chat ? { where: where.chat } : {}),
|
|
149
|
+
...(where.dismissed ? { dismissed: where.dismissed } : {}),
|
|
150
|
+
message: where.chat
|
|
151
|
+
? `First ask \`where.question\` with its \`where.options\` (your native choice buttons if you have them, otherwise a short numbered list; accept the number or the text). If they pick the web, call again with where:"later". If they pick here, ${show}`
|
|
152
|
+
: `The talent does it here: ${show}`,
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
const answer = await askAnswerMode(args, ctx);
|
|
156
|
+
if (answer.chat) {
|
|
157
|
+
return {
|
|
158
|
+
ok: false,
|
|
159
|
+
reason: 'answer-mode-required',
|
|
160
|
+
...(answer.dismissed ? { dismissed: answer.dismissed } : {}),
|
|
161
|
+
...answer.chat,
|
|
162
|
+
message: `${answer.chat.choiceMessage} Then call again with disclaimerAcknowledged:true and answerMode: drafted for the first option, own for the second, full for the third.`,
|
|
361
163
|
};
|
|
362
164
|
}
|
|
363
165
|
const start = await flow.createAndStartLivekitOnboarding(resolved, {
|
|
364
166
|
candidateId: args.candidateId,
|
|
365
167
|
language: args.language || lang,
|
|
366
168
|
});
|
|
367
|
-
if (!start.ok)
|
|
169
|
+
if (!start.ok) {
|
|
170
|
+
const out = { ok: false, step: start.step || 'start', reason: start.reason };
|
|
171
|
+
return start.reason === 'interview-already-started' ? { ...out, next: 'open_web' } : out;
|
|
172
|
+
}
|
|
368
173
|
|
|
369
174
|
const closing = start.closingMessage;
|
|
370
175
|
const isClosing = closing ? (t) => typeof t === 'string' && t.includes(closing) : null;
|
|
@@ -376,7 +181,9 @@ function makeRegisterTools(deps = {}) {
|
|
|
376
181
|
} catch (e) {
|
|
377
182
|
try { await client.disconnect(); } catch { /* nothing joined */ }
|
|
378
183
|
const kind = (e && e.kind) || 'connect-failed';
|
|
379
|
-
|
|
184
|
+
if (LIVEKIT_LOAD_FAILURES.has(kind)) reportLivekitFailure(kind, e);
|
|
185
|
+
// Whatever kept the room from opening here, the same interview runs on the web.
|
|
186
|
+
return { ok: false, step: 'connect', reason: kind, relayVerbatim: copy().livekitUnavailable, message: 'Show relayVerbatim word for word, then open_web.', next: 'open_web' };
|
|
380
187
|
}
|
|
381
188
|
|
|
382
189
|
let opening;
|
|
@@ -388,7 +195,7 @@ function makeRegisterTools(deps = {}) {
|
|
|
388
195
|
}
|
|
389
196
|
buffer.recordAgent(opening.text);
|
|
390
197
|
interviewSessions.set(start.interviewId, { client, buffer, connectAt: Date.now(), ended: opening.ended === true });
|
|
391
|
-
return { ok: true, interviewId: start.interviewId, greeting: opening.text, ended: opening.ended === true };
|
|
198
|
+
return { ok: true, interviewId: start.interviewId, answerMode: answer.mode, greeting: opening.text, ended: opening.ended === true };
|
|
392
199
|
}
|
|
393
200
|
|
|
394
201
|
async function interviewTurn(args = {}) {
|
|
@@ -418,6 +225,7 @@ function makeRegisterTools(deps = {}) {
|
|
|
418
225
|
}
|
|
419
226
|
const saved = await teardownInterview(args.interviewId, { persist: true });
|
|
420
227
|
if (!saved.ok) return { ok: false, reason: saved.reason || 'complete-failed' };
|
|
228
|
+
lastCompletedInterview = { interviewId: args.interviewId, completedAt: Date.now() };
|
|
421
229
|
return { ok: true, state: 'taken', message: catalog.interviewComplete };
|
|
422
230
|
}
|
|
423
231
|
|
|
@@ -426,7 +234,7 @@ function makeRegisterTools(deps = {}) {
|
|
|
426
234
|
const reset = await flow.restartOnboarding(resolved);
|
|
427
235
|
if (!reset.ok) {
|
|
428
236
|
if (reset.reason === 'onboarding-not-found') {
|
|
429
|
-
return { ok: false, reason: 'onboarding-not-found', message: 'The talent has no onboarding interview yet
|
|
237
|
+
return { ok: false, reason: 'onboarding-not-found', message: 'The talent has no onboarding interview yet: nothing to repeat. Offer onboarding_interview_start instead.' };
|
|
430
238
|
}
|
|
431
239
|
return { ok: false, reason: reset.reason };
|
|
432
240
|
}
|
|
@@ -454,11 +262,6 @@ function makeRegisterTools(deps = {}) {
|
|
|
454
262
|
};
|
|
455
263
|
}
|
|
456
264
|
|
|
457
|
-
// PURE preview: reuses the write-path validators, touches no network, no session.
|
|
458
|
-
async function registerPreview(args = {}) {
|
|
459
|
-
return flow.buildRegisterPreview(args || {});
|
|
460
|
-
}
|
|
461
|
-
|
|
462
265
|
// Content-free context the model may PROPOSE to the talent (never auto-write).
|
|
463
266
|
async function suggestContext(args = {}) {
|
|
464
267
|
const read = deps.readRegisterContextSuggestions || require('./register-context').readRegisterContextSuggestions;
|
|
@@ -477,27 +280,15 @@ function makeRegisterTools(deps = {}) {
|
|
|
477
280
|
}
|
|
478
281
|
|
|
479
282
|
return [
|
|
480
|
-
{
|
|
481
|
-
name: 'register',
|
|
482
|
-
description: registerDescription,
|
|
483
|
-
inputSchema: schemas.REGISTER_SCHEMA,
|
|
484
|
-
handler: register,
|
|
485
|
-
},
|
|
486
|
-
{
|
|
487
|
-
name: 'register_preview',
|
|
488
|
-
description: "REVIEW a (partial) register payload BEFORE writing — the preferred way to register with the least typing. Fill every field you can reasonably infer, then call this. It writes NOTHING; it returns { wouldWrite: the normalized data per hub endpoint, missingFields: what is still needed (required:true) or just suggested (required:false) to ASK the talent, warnings: any value that is invalid, mustConfirm }. Show the talent a short human summary of wouldWrite and the missing items, let them confirm or correct, then call `register` with the final payload. Pre-fill every field you can reasonably infer (memory, chat, context), including pricing and changeMotivators; omit only what you have no basis for. The talent confirms the whole summary once; fields in `mustConfirm` just need to be visible in that summary, not asked separately. All register data is self-declared profile data.",
|
|
489
|
-
inputSchema: schemas.PREVIEW_SCHEMA,
|
|
490
|
-
handler: registerPreview,
|
|
491
|
-
},
|
|
492
283
|
{
|
|
493
284
|
name: 'suggest_register_context',
|
|
494
|
-
description: "
|
|
285
|
+
description: "SUGGESTIONS for the sign-up read on this machine (no network, no file contents): timezone, git name/email, the gh CLI GitHub login, and cvCandidates: PDF/DOC/DOCX files in Desktop, Documents, Downloads and iCloud Drive whose NAME looks like a CV or carries the talent's name, each with nameMatch and generic flags. A candidate is only a lead (people who hire keep other people's CVs): confirm it with read_cv. Pass talentName to sharpen the match. Use it when the talent said you may look for their CV on this machine.",
|
|
495
286
|
inputSchema: { type: 'object', properties: { talentName: { type: 'string', description: "The talent's full name as you know it (memory, chat); improves CV matching." } } },
|
|
496
287
|
handler: suggestContext,
|
|
497
288
|
},
|
|
498
289
|
{
|
|
499
290
|
name: 'read_cv',
|
|
500
|
-
description: "Read the TEXT of a CV file (PDF, DOC, DOCX) on this machine
|
|
291
|
+
description: "Read the TEXT of a CV file (PDF, DOC, DOCX) on this machine, with the talent's permission (talentConsent:true) and talentName set. Returns { mentionsTalent, links: { linkedin, github, emails, phones }, text } for the talent's own CV; a CV that does not name them returns only mentionsTalent:false: it is someone else's, drop it, never describe it, try the next candidate. Nothing is uploaded by this tool: pass the confirmed path to signup_start (or import_profile) as cvPath, use the text for userQuery and the email, and links.linkedin as their LinkedIn URL.",
|
|
501
292
|
inputSchema: {
|
|
502
293
|
type: 'object',
|
|
503
294
|
properties: {
|
|
@@ -509,15 +300,9 @@ function makeRegisterTools(deps = {}) {
|
|
|
509
300
|
},
|
|
510
301
|
handler: readCv,
|
|
511
302
|
},
|
|
512
|
-
{
|
|
513
|
-
name: 'register_status',
|
|
514
|
-
description: 'Check the state of the background profile import started by register. Pass the importJobId (an internal handle — never shown to the talent). Returns { state } (e.g. running | done | failed).',
|
|
515
|
-
inputSchema: STATUS_SCHEMA,
|
|
516
|
-
handler: registerStatus,
|
|
517
|
-
},
|
|
518
303
|
{
|
|
519
304
|
name: 'onboarding_interview_start',
|
|
520
|
-
description: "Start the onboarding interview
|
|
305
|
+
description: "Start the onboarding interview, here in the chat. Offer it only when signup_status says onboardingInterview.completed is not true. Call it first without disclaimerAcknowledged: it asks 'here now or later on the web?' (in a dialog when the client has one, otherwise it returns `where` with the question and its options to ask; default here) and returns the two interview disclaimers. reason 'interview-later' means they chose the web: skip the interview. If it returns reason 'interview-already-started' (begun elsewhere and not finished), call open_web so they continue on the web. It runs over LiveKit and the server agent conducts it; the client does not decide its branch. You MUST show the talent both interview disclaimers word for word, each as its own block, never paraphrased or relabelled, and only then call with disclaimerAcknowledged:true and answerMode (without answerMode it asks the answer-mode question in a dialog, or returns it with its options: ask it and call again). Returns { interviewId, answerMode, greeting, ended }: relay the greeting VERBATIM, then drive it with onboarding_interview_turn. ANSWER MODES. For drafted and full, first gather everything you know (memory, a search of past conversations if you have that tool, their CV, the ai_usage report, the sign-up data) and show ONE short sheet 'esto es lo que se de ti' (roles and projects with concrete facts, stack, languages, what they want next); let them correct it once; those corrections override your memory. Your answers are first person, one or two short sentences in their plain voice, built only from facts on the confirmed sheet: no embellishment, no adjectives, metrics or claims they did not give, no repeating a fact already said unless asked; for wishes, preferences or opinions give your best guess and say it is a guess. (1) drafted: show each question VERBATIM with your proposed answer and send it only after the talent approves or edits it; if the sheet does not cover a question, ask them briefly instead of guessing. Only if the talent explicitly asks you to answer everything yourself (for example 'hazla entera' or 'contéstalas tú todas'), answer the remaining questions without asking for each approval, as in full. (2) full (they chose it, or asked for it as above): answer every question yourself without per-answer approval, still showing each question and your answer, and when the interview ends tell them in one line that you answered those questions yourself. (3) own: you are a pure CONDUIT: show every question VERBATIM and pass back only their REAL, VERBATIM words. In every mode never reword, summarize or translate the questions. `interviewId` is internal: never show it. If it fails with next:'open_web', relay `message` and call open_web so they do the interview on the web.",
|
|
521
306
|
inputSchema: INTERVIEW_START_SCHEMA,
|
|
522
307
|
handler: interviewStart,
|
|
523
308
|
available: async () => {
|
|
@@ -530,25 +315,25 @@ function makeRegisterTools(deps = {}) {
|
|
|
530
315
|
},
|
|
531
316
|
{
|
|
532
317
|
name: 'onboarding_interview_turn',
|
|
533
|
-
description: "Send the talent's answer to the current onboarding-interview question and get the next one. Follow the
|
|
318
|
+
description: "Send the talent's answer to the current onboarding-interview question and get the next one. Follow the answerMode the talent chose at onboarding_interview_start. drafted: `message` is the answer the talent APPROVED or edited after seeing your proposal; never send one they have not confirmed, unless they explicitly asked you to answer the rest yourself ('hazla entera', 'contéstalas tú todas'): from then on, as in full, send your own answers without per-answer approval. full: send your own answer to each question, built only from what they confirmed, and when it ends tell them you answered those questions yourself. own: `message` is ONLY the talent's REAL, VERBATIM answer — never invent, infer or complete it. Outside full, if the talent has not answered or approved yet, do NOT call this, wait for them. Relay the returned `response` (the next question) to the talent VERBATIM — do not reword, summarize or translate it. Keep calling with their next `message` until `ended` is true, then call onboarding_interview_complete. Do NOT show or mention `interviewId` to the talent (internal handle). If it returns reason 'unknown-interview' (the server was restarted and the live room is gone), start again with onboarding_interview_start.",
|
|
534
319
|
inputSchema: INTERVIEW_TURN_SCHEMA,
|
|
535
320
|
handler: interviewTurn,
|
|
536
321
|
},
|
|
537
322
|
{
|
|
538
323
|
name: 'onboarding_interview_complete',
|
|
539
|
-
description: "Close the onboarding interview once the turns report ended:true: the buffered transcript is saved and the live room is disconnected. `interviewId` is
|
|
324
|
+
description: "Close the onboarding interview once the turns report ended:true: the buffered transcript is saved and the live room is disconnected. `interviewId` is internal: never show it. Returns { state:'taken' }. If it returns reason 'unknown-interview' the room was already gone (server restart): nothing to complete. In the sign-up flow, go on to the main role (list_my_roles with waitSeconds, then set_main_role) and finish with open_web; do NOT call onboarding_finish there.",
|
|
540
325
|
inputSchema: INTERVIEW_COMPLETE_SCHEMA,
|
|
541
326
|
handler: interviewComplete,
|
|
542
327
|
},
|
|
543
328
|
{
|
|
544
329
|
name: 'repeat_onboarding_interview',
|
|
545
|
-
description: "OVERWRITE-repeat the onboarding interview: resets the talent's completed onboarding interview IN PLACE
|
|
330
|
+
description: "OVERWRITE-repeat the onboarding interview: resets the talent's completed onboarding interview IN PLACE; the previous evaluation is DISCARDED and CANNOT be recovered. Clearly warn the talent that this is irreversible and get their explicit go-ahead BEFORE calling this. On success run the interview again (onboarding_interview_start, onboarding_interview_turn until ended, onboarding_interview_complete), then onboarding_finish. Returns { interviewId, language } or reason 'onboarding-not-found' (no onboarding yet), 'http-401'/'http-403'.",
|
|
546
331
|
inputSchema: { type: 'object', properties: {} },
|
|
547
332
|
handler: repeatOnboarding,
|
|
548
333
|
},
|
|
549
334
|
{
|
|
550
335
|
name: 'onboarding_finish',
|
|
551
|
-
description: "Mark the onboarding as
|
|
336
|
+
description: "Mark the onboarding as COMPLETED on Shakers. NOT part of the sign-up flow (signup_start): there, never call it. Use it only to close a repeat_onboarding_interview run. Idempotent. Returns { finalized, onboardingStatus, registrationLevel, completedProfilePercentage, profileUrl }.",
|
|
552
337
|
inputSchema: FINISH_SCHEMA,
|
|
553
338
|
handler: onboardingFinish,
|
|
554
339
|
},
|
|
@@ -557,18 +342,9 @@ function makeRegisterTools(deps = {}) {
|
|
|
557
342
|
|
|
558
343
|
module.exports = {
|
|
559
344
|
makeRegisterTools,
|
|
560
|
-
|
|
561
|
-
REGISTER_SCHEMA,
|
|
562
|
-
PREVIEW_SCHEMA,
|
|
563
|
-
STATUS_SCHEMA,
|
|
345
|
+
completedOnboardingInterview,
|
|
564
346
|
INTERVIEW_START_SCHEMA,
|
|
565
347
|
INTERVIEW_TURN_SCHEMA,
|
|
566
348
|
INTERVIEW_COMPLETE_SCHEMA,
|
|
567
349
|
FINISH_SCHEMA,
|
|
568
|
-
PROFILE_SCHEMA,
|
|
569
|
-
WORK_SITUATION_SCHEMA,
|
|
570
|
-
PRICING_SCHEMA,
|
|
571
|
-
AVAILABILITY_SCHEMA,
|
|
572
|
-
LANGUAGES_SCHEMA,
|
|
573
|
-
PHONE_SCHEMA,
|
|
574
350
|
};
|