shakers-cli-beta 0.9.0 → 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 +30 -5
- package/bin/ai-usage.js +32 -19
- package/bin/mcp.js +11 -20
- package/bin/mcpb.js +5 -0
- package/package.json +2 -2
- package/src/auth-session-store.js +1 -0
- package/src/backend-request.js +2 -2
- package/src/config.js +29 -0
- package/src/cv-file.js +62 -0
- package/src/device-login.js +5 -3
- package/src/find-projects-client.js +16 -6
- package/src/i18n.js +23 -22
- package/src/mcp-ai-usage-tool.js +150 -65
- package/src/mcp-auth-form.js +186 -31
- 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 +2 -2
- package/src/mcp-prompts.js +8 -6
- package/src/mcp-rate-tools.js +2 -2
- package/src/mcp-register-tools.js +89 -315
- package/src/mcp-role-tools.js +177 -16
- package/src/mcp-server.js +71 -3
- 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/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/onboarding-client.js
CHANGED
|
@@ -1,11 +1,9 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
const fs = require('fs');
|
|
4
|
-
const path = require('path');
|
|
5
3
|
const { postJsonWithTimeout, postMultipartWithTimeout } = require('./backend-request');
|
|
6
4
|
const { WORKS_CODE_TO_REASON } = require('./skills-client');
|
|
7
5
|
const { toLowerLang, toUpperLang } = require('./lang-codes');
|
|
8
|
-
const {
|
|
6
|
+
const { readLocalCv } = require('./cv-file');
|
|
9
7
|
|
|
10
8
|
const DEFAULT_TIMEOUT_MS = 20000;
|
|
11
9
|
const IMPORT_KICKOFF_TIMEOUT_MS = 90000;
|
|
@@ -42,46 +40,48 @@ function envelopeData(raw) {
|
|
|
42
40
|
return json.data;
|
|
43
41
|
}
|
|
44
42
|
|
|
45
|
-
function buildImportSources({ linkedinUrl, cvPath, githubUrl, websiteUrl, language } = {}) {
|
|
43
|
+
function buildImportSources({ linkedinUrl, cvPath, githubUrl, websiteUrl, language, userQuery, fillEmptyOnly } = {}) {
|
|
46
44
|
const sources = { language: toLowerLang(language) };
|
|
47
45
|
if (typeof linkedinUrl === 'string' && linkedinUrl) sources.linkedinUrl = linkedinUrl;
|
|
48
46
|
if (typeof cvPath === 'string' && cvPath) sources.cvPath = cvPath;
|
|
49
47
|
if (typeof githubUrl === 'string' && githubUrl) sources.githubUrl = githubUrl;
|
|
50
48
|
if (typeof websiteUrl === 'string' && websiteUrl) sources.websiteUrl = websiteUrl;
|
|
49
|
+
if (typeof userQuery === 'string' && userQuery) sources.userQuery = userQuery;
|
|
50
|
+
if (fillEmptyOnly === true) sources.fillEmptyOnly = true;
|
|
51
51
|
return sources;
|
|
52
52
|
}
|
|
53
53
|
|
|
54
|
-
//
|
|
55
|
-
|
|
56
|
-
'.
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
54
|
+
// Hub answers import-profile synchronously with a per-source report; null when the AI omitted it.
|
|
55
|
+
function importReportFrom(data) {
|
|
56
|
+
const report = data && typeof data === 'object' ? data.importReport : null;
|
|
57
|
+
if (!report || !Array.isArray(report.sources)) return null;
|
|
58
|
+
return {
|
|
59
|
+
outcome: typeof report.outcome === 'string' ? report.outcome : null,
|
|
60
|
+
sources: report.sources
|
|
61
|
+
.filter((src) => src && typeof src.source === 'string')
|
|
62
|
+
.map((src) => ({
|
|
63
|
+
source: src.source,
|
|
64
|
+
status: typeof src.status === 'string' ? src.status : null,
|
|
65
|
+
...(typeof src.code === 'string' ? { code: src.code } : {}),
|
|
66
|
+
...(typeof src.retryable === 'boolean' ? { retryable: src.retryable } : {}),
|
|
67
|
+
})),
|
|
68
|
+
};
|
|
69
|
+
}
|
|
61
70
|
|
|
71
|
+
// The CV formats hub accepts on import-profile.
|
|
62
72
|
function readCvUpload(input) {
|
|
63
|
-
|
|
64
|
-
const contentType = CV_CONTENT_TYPES[path.extname(cvPath).toLowerCase()];
|
|
65
|
-
if (!contentType) return { ok: false, reason: 'cv-bad-format' };
|
|
66
|
-
let data;
|
|
67
|
-
try {
|
|
68
|
-
data = fs.readFileSync(cvPath);
|
|
69
|
-
} catch {
|
|
70
|
-
return { ok: false, reason: 'cv-not-found' };
|
|
71
|
-
}
|
|
72
|
-
if (data.length > CV_MAX_BYTES) return { ok: false, reason: 'cv-too-large' };
|
|
73
|
-
return { ok: true, file: { filename: path.basename(cvPath), contentType, data } };
|
|
73
|
+
return readLocalCv(input, { extensions: ['.pdf', '.doc', '.docx'], badFormat: 'cv-bad-format' });
|
|
74
74
|
}
|
|
75
75
|
|
|
76
76
|
async function requestImportProfile(
|
|
77
|
-
{ linkedinUrl, cvPath, githubUrl, websiteUrl, language, accessToken, hubAccessToken } = {},
|
|
77
|
+
{ linkedinUrl, cvPath, githubUrl, websiteUrl, language, userQuery, fillEmptyOnly, accessToken, hubAccessToken } = {},
|
|
78
78
|
{ endpoint, timeoutMs = IMPORT_KICKOFF_TIMEOUT_MS } = {},
|
|
79
79
|
) {
|
|
80
80
|
if (!endpoint) return { ok: false, reason: 'no-endpoint' };
|
|
81
|
-
if (!linkedinUrl) return { ok: false, reason: 'no-
|
|
81
|
+
if (!linkedinUrl && !cvPath && !githubUrl && !websiteUrl) return { ok: false, reason: 'no-source' };
|
|
82
82
|
if (!hubAccessToken) return { ok: false, reason: 'no-hub-token' };
|
|
83
83
|
|
|
84
|
-
const body = buildImportSources({ linkedinUrl, cvPath, githubUrl, websiteUrl, language });
|
|
84
|
+
const body = buildImportSources({ linkedinUrl, cvPath, githubUrl, websiteUrl, language, userQuery, fillEmptyOnly });
|
|
85
85
|
let res;
|
|
86
86
|
try {
|
|
87
87
|
if (body.cvPath) {
|
|
@@ -90,7 +90,8 @@ async function requestImportProfile(
|
|
|
90
90
|
const cv = readCvUpload(body.cvPath);
|
|
91
91
|
if (!cv.ok) return cv;
|
|
92
92
|
const { cvPath: _localPath, ...fields } = body;
|
|
93
|
-
|
|
93
|
+
const form = { ...fields, fillEmptyOnly: fields.fillEmptyOnly ? 'true' : undefined };
|
|
94
|
+
res = await postMultipartWithTimeout(endpoint, { ...form, cv: cv.file }, timeoutMs, 'POST', hubHeaders({ hubAccessToken }));
|
|
94
95
|
} else {
|
|
95
96
|
res = await postJsonWithTimeout(endpoint, body, timeoutMs, 'POST', null, hubHeaders({ hubAccessToken }));
|
|
96
97
|
}
|
|
@@ -101,31 +102,7 @@ async function requestImportProfile(
|
|
|
101
102
|
|
|
102
103
|
const data = envelopeData(res.raw);
|
|
103
104
|
if (data === undefined) return { ok: false, reason: 'bad-response' };
|
|
104
|
-
|
|
105
|
-
const state = data && typeof data === 'object' && typeof data.state === 'string' ? data.state : 'running';
|
|
106
|
-
return { ok: true, jobId, state };
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
async function requestImportStatus(
|
|
110
|
-
{ jobId, accessToken, hubAccessToken } = {},
|
|
111
|
-
{ endpoint, timeoutMs = DEFAULT_TIMEOUT_MS } = {},
|
|
112
|
-
) {
|
|
113
|
-
if (!endpoint) return { ok: false, reason: 'no-endpoint' };
|
|
114
|
-
if (!jobId) return { ok: false, reason: 'no-job' };
|
|
115
|
-
|
|
116
|
-
const url = `${endpoint.replace(/\/+$/, '')}/${encodeURIComponent(jobId)}`;
|
|
117
|
-
let res;
|
|
118
|
-
try {
|
|
119
|
-
res = await postJsonWithTimeout(url, null, timeoutMs, 'GET', null, hubHeaders({ hubAccessToken }));
|
|
120
|
-
} catch (e) {
|
|
121
|
-
return { ok: false, reason: (e && e.kind) || 'network-error' };
|
|
122
|
-
}
|
|
123
|
-
if (res.status < 200 || res.status >= 300) return { ok: false, reason: reasonForError(res.status, res.raw) };
|
|
124
|
-
|
|
125
|
-
const data = envelopeData(res.raw);
|
|
126
|
-
if (data === undefined || !data || typeof data !== 'object') return { ok: false, reason: 'bad-response' };
|
|
127
|
-
const state = typeof data.state === 'string' ? data.state : 'running';
|
|
128
|
-
return { ok: true, state };
|
|
105
|
+
return { ok: true, report: importReportFrom(data) };
|
|
129
106
|
}
|
|
130
107
|
|
|
131
108
|
async function requestSetProfessionalDetails(
|
|
@@ -380,7 +357,6 @@ async function requestCompleteTextSession(
|
|
|
380
357
|
|
|
381
358
|
module.exports = {
|
|
382
359
|
requestImportProfile,
|
|
383
|
-
requestImportStatus,
|
|
384
360
|
requestSetProfessionalDetails,
|
|
385
361
|
requestSetPricingRate,
|
|
386
362
|
requestSetAvailability,
|
package/src/onboarding-flow.js
CHANGED
|
@@ -241,120 +241,6 @@ function validateAvailability(input = {}) {
|
|
|
241
241
|
return { ok: true, availability: body };
|
|
242
242
|
}
|
|
243
243
|
|
|
244
|
-
// Fields the talent must ALWAYS confirm explicitly — never inferred/invented by
|
|
245
|
-
// the model (a wrong price or motivation is worse than a gap).
|
|
246
|
-
const MUST_CONFIRM_FIELDS = ['pricing', 'workSituation.changeMotivators'];
|
|
247
|
-
|
|
248
|
-
// PURE preview of a (possibly partial) register payload: what WOULD be written,
|
|
249
|
-
// what is still missing, and what is invalid — reusing the same validators the
|
|
250
|
-
// write path uses. Touches no network and needs no session, so the model can show
|
|
251
|
-
// the talent a review before the single `register` write.
|
|
252
|
-
function buildRegisterPreview(args = {}) {
|
|
253
|
-
const wouldWrite = [];
|
|
254
|
-
const missingFields = [];
|
|
255
|
-
const warnings = [];
|
|
256
|
-
const addMissing = (field, required) => missingFields.push({ field, required });
|
|
257
|
-
|
|
258
|
-
// --- account (always written at registration; password never in the payload) ---
|
|
259
|
-
const account = {
|
|
260
|
-
name: args.name,
|
|
261
|
-
lastName: args.lastName,
|
|
262
|
-
email: args.email,
|
|
263
|
-
freelanceType: args.freelanceType,
|
|
264
|
-
};
|
|
265
|
-
if (!isNonEmptyString(account.name)) addMissing('name', true);
|
|
266
|
-
if (!isNonEmptyString(account.lastName)) addMissing('lastName', true);
|
|
267
|
-
if (!isNonEmptyString(account.email)) addMissing('email', true);
|
|
268
|
-
const ftype = normalizeFreelanceType(account.freelanceType);
|
|
269
|
-
if (!ftype) {
|
|
270
|
-
if (isNonEmptyString(account.freelanceType)) warnings.push({ field: 'freelanceType', reason: 'bad-freelance-type' });
|
|
271
|
-
else addMissing('freelanceType', true);
|
|
272
|
-
}
|
|
273
|
-
wouldWrite.push({
|
|
274
|
-
section: 'account',
|
|
275
|
-
endpoint: 'hub: auth/sign-up/email + works/auth/complete-registration',
|
|
276
|
-
data: {
|
|
277
|
-
name: isNonEmptyString(account.name) ? account.name : null,
|
|
278
|
-
lastName: isNonEmptyString(account.lastName) ? account.lastName : null,
|
|
279
|
-
email: isNonEmptyString(account.email) ? account.email : null,
|
|
280
|
-
freelanceType: ftype || null,
|
|
281
|
-
preferredLanguage: isNonEmptyString(args.preferredLanguage) ? args.preferredLanguage : null,
|
|
282
|
-
newsletterConsent: args.newsletterConsent === true,
|
|
283
|
-
},
|
|
284
|
-
note: 'password is never part of this payload — the talent types it in the local sign-up window',
|
|
285
|
-
});
|
|
286
|
-
|
|
287
|
-
// --- profile import (linkedinUrl is the required source) ---
|
|
288
|
-
const profile = args.profile && typeof args.profile === 'object' ? args.profile : {};
|
|
289
|
-
if (!isNonEmptyString(profile.linkedinUrl)) addMissing('profile.linkedinUrl', true);
|
|
290
|
-
else {
|
|
291
|
-
const pdata = { linkedinUrl: profile.linkedinUrl };
|
|
292
|
-
for (const k of ['githubUrl', 'websiteUrl', 'cvPath']) if (isNonEmptyString(profile[k])) pdata[k] = profile[k];
|
|
293
|
-
wouldWrite.push({ section: 'profile', endpoint: 'hub: works/me/import-profile', data: pdata });
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
// --- work situation (situation required) ---
|
|
297
|
-
const ws = args.workSituation && typeof args.workSituation === 'object' ? args.workSituation : {};
|
|
298
|
-
if (!isNonEmptyString(ws.situation)) addMissing('workSituation.situation', true);
|
|
299
|
-
else {
|
|
300
|
-
const vws = validateWorkSituation(ws);
|
|
301
|
-
if (!vws.ok) warnings.push({ field: 'workSituation', reason: vws.reason });
|
|
302
|
-
else wouldWrite.push({ section: 'workSituation', endpoint: 'hub: works/me/professional-details', data: vws.professionalDetails });
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
// --- pricing (at least one variant; ALWAYS confirm) ---
|
|
306
|
-
const pricing = args.pricing && typeof args.pricing === 'object' ? args.pricing : {};
|
|
307
|
-
const pricingEmpty = pricing.fullTimeSelected !== true && pricing.partTimeSelected !== true
|
|
308
|
-
&& pricing.fullTimeAmount == null && pricing.partTimeAmount == null;
|
|
309
|
-
if (pricingEmpty) addMissing('pricing', true);
|
|
310
|
-
else {
|
|
311
|
-
const vp = validatePricing(pricing);
|
|
312
|
-
if (!vp.ok) warnings.push({ field: 'pricing', reason: vp.reason });
|
|
313
|
-
else wouldWrite.push({ section: 'pricing', endpoint: 'hub: works/talents/me/work-details/pricing-rate', data: vp.pricing });
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
// --- availability (optional; suggest the sub-fields the talent did not give) ---
|
|
317
|
-
if (args.availability && typeof args.availability === 'object') {
|
|
318
|
-
const va = validateAvailability(args.availability);
|
|
319
|
-
if (!va.ok) warnings.push({ field: 'availability', reason: va.reason });
|
|
320
|
-
else {
|
|
321
|
-
wouldWrite.push({ section: 'availability', endpoint: 'hub: works/talents/me/work-details/availability', data: va.availability });
|
|
322
|
-
for (const k of ['available', 'monthlyHours', 'workModes', 'country', 'timezone']) {
|
|
323
|
-
if (!(k in va.availability)) addMissing(`availability.${k}`, false);
|
|
324
|
-
}
|
|
325
|
-
}
|
|
326
|
-
} else {
|
|
327
|
-
addMissing('availability', false);
|
|
328
|
-
}
|
|
329
|
-
|
|
330
|
-
// --- languages (optional; ids resolve at write-time, so the preview shows codes) ---
|
|
331
|
-
if (Array.isArray(args.languages) && args.languages.length > 0) {
|
|
332
|
-
const vl = validateLanguageRows(args.languages);
|
|
333
|
-
if (!vl.ok) warnings.push({ field: 'languages', reason: vl.reason });
|
|
334
|
-
else wouldWrite.push({ section: 'languages', endpoint: 'hub: works/talents/me/work-details/languages', data: vl.rows, note: 'language codes resolve to ids at write time' });
|
|
335
|
-
} else {
|
|
336
|
-
addMissing('languages', false);
|
|
337
|
-
}
|
|
338
|
-
|
|
339
|
-
// --- phone (optional) ---
|
|
340
|
-
if (args.phone && typeof args.phone === 'object') {
|
|
341
|
-
const vph = validatePhone(args.phone);
|
|
342
|
-
if (!vph.ok) warnings.push({ field: 'phone', reason: vph.reason });
|
|
343
|
-
else wouldWrite.push({ section: 'phone', endpoint: 'hub: works/me', data: { telephoneCode: vph.telephoneCode, telephoneNumber: vph.telephoneNumber } });
|
|
344
|
-
} else {
|
|
345
|
-
addMissing('phone', false);
|
|
346
|
-
}
|
|
347
|
-
|
|
348
|
-
return {
|
|
349
|
-
ok: true,
|
|
350
|
-
provenance: 'self-declared',
|
|
351
|
-
wouldWrite,
|
|
352
|
-
missingFields,
|
|
353
|
-
warnings,
|
|
354
|
-
mustConfirm: MUST_CONFIRM_FIELDS,
|
|
355
|
-
};
|
|
356
|
-
}
|
|
357
|
-
|
|
358
244
|
function makeDeps(overrides = {}) {
|
|
359
245
|
const config = require('./config');
|
|
360
246
|
const client = require('./onboarding-client');
|
|
@@ -392,7 +278,6 @@ function makeDeps(overrides = {}) {
|
|
|
392
278
|
getTalentProfileUrl: config.getTalentProfileUrl,
|
|
393
279
|
getUsageDiscoveredInventoryEndpoint: config.getUsageDiscoveredInventoryEndpoint,
|
|
394
280
|
requestImportProfile: client.requestImportProfile,
|
|
395
|
-
requestImportStatus: client.requestImportStatus,
|
|
396
281
|
requestSetProfessionalDetails: client.requestSetProfessionalDetails,
|
|
397
282
|
requestSetPricingRate: client.requestSetPricingRate,
|
|
398
283
|
requestSetAvailability: client.requestSetAvailability,
|
|
@@ -434,19 +319,6 @@ function isAcceptablePassword(password) {
|
|
|
434
319
|
return typeof password === 'string' && password.length >= 8;
|
|
435
320
|
}
|
|
436
321
|
|
|
437
|
-
function isNonEmptyString(value) {
|
|
438
|
-
return typeof value === 'string' && value.trim().length > 0;
|
|
439
|
-
}
|
|
440
|
-
|
|
441
|
-
function validateRegisterFields(fields = {}) {
|
|
442
|
-
if (!isNonEmptyString(fields.name)) return { ok: false, reason: 'missing-name' };
|
|
443
|
-
if (!isNonEmptyString(fields.lastName)) return { ok: false, reason: 'missing-last-name' };
|
|
444
|
-
if (!isNonEmptyString(fields.email)) return { ok: false, reason: 'missing-email' };
|
|
445
|
-
const type = normalizeFreelanceType(fields.freelanceType);
|
|
446
|
-
if (!type) return { ok: false, reason: 'bad-freelance-type' };
|
|
447
|
-
return { ok: true };
|
|
448
|
-
}
|
|
449
|
-
|
|
450
322
|
async function signUp(deps, fields = {}) {
|
|
451
323
|
// The CLI no longer asks freelanceType at signup → transient default (corrected later from the work situation via professional-details).
|
|
452
324
|
if (!isAcceptablePassword(fields.password)) return { ok: false, reason: 'weak-password' };
|
|
@@ -465,6 +337,7 @@ async function signUp(deps, fields = {}) {
|
|
|
465
337
|
newsletterConsent: fields.newsletterConsent === true,
|
|
466
338
|
freelanceType: normalizeFreelanceType(fields.freelanceType) || DEFAULT_SIGNUP_FREELANCE_TYPE,
|
|
467
339
|
freelanceIntent: fields.freelanceIntent,
|
|
340
|
+
claimCode: fields.claimCode || null,
|
|
468
341
|
},
|
|
469
342
|
{ signUpEndpoint, completeRegistrationEndpoint },
|
|
470
343
|
);
|
|
@@ -548,17 +421,6 @@ async function startProfileImport(deps, sources = {}) {
|
|
|
548
421
|
);
|
|
549
422
|
}
|
|
550
423
|
|
|
551
|
-
async function pollProfileImport(deps, { jobId } = {}) {
|
|
552
|
-
const gate = await requireActiveSession(deps);
|
|
553
|
-
if (!gate.ok) return gate;
|
|
554
|
-
const endpoint = deps.getImportProfileEndpoint();
|
|
555
|
-
if (!endpoint) return { ok: false, reason: 'no-endpoint' };
|
|
556
|
-
return deps.requestImportStatus(
|
|
557
|
-
{ jobId, accessToken: gate.session.accessToken, hubAccessToken: gate.session.hubAccessToken },
|
|
558
|
-
{ endpoint },
|
|
559
|
-
);
|
|
560
|
-
}
|
|
561
|
-
|
|
562
424
|
// A: anchor the ingest identity to THIS talent's session email so a stale
|
|
563
425
|
// consent.json from a prior alta doesn't misattribute the footprint. Never throws.
|
|
564
426
|
function reattachConsentIdentity(deps) {
|
|
@@ -798,14 +660,12 @@ function profileUrl(deps) {
|
|
|
798
660
|
}
|
|
799
661
|
|
|
800
662
|
function usageDisclaimers(lang) {
|
|
801
|
-
const
|
|
802
|
-
const c = getCatalog(lang).onboarding;
|
|
663
|
+
const c = require('./signup-copy').legalCopy(lang);
|
|
803
664
|
return { infoAccessed: c.usageInfoAccessed, goalDuration: c.usageGoalDuration };
|
|
804
665
|
}
|
|
805
666
|
|
|
806
667
|
function interviewDisclaimers(lang) {
|
|
807
|
-
const
|
|
808
|
-
const c = getCatalog(lang).onboarding;
|
|
668
|
+
const c = require('./signup-copy').legalCopy(lang);
|
|
809
669
|
return { infoAccessed: c.interviewInfoAccessed, goalDuration: c.interviewGoalDuration };
|
|
810
670
|
}
|
|
811
671
|
|
|
@@ -1165,9 +1025,6 @@ module.exports = {
|
|
|
1165
1025
|
validateAvailability,
|
|
1166
1026
|
buildPricingUpsert,
|
|
1167
1027
|
isAcceptablePassword,
|
|
1168
|
-
validateRegisterFields,
|
|
1169
|
-
buildRegisterPreview,
|
|
1170
|
-
MUST_CONFIRM_FIELDS,
|
|
1171
1028
|
deriveCurrentEmploymentStatus,
|
|
1172
1029
|
showsFreelanceOpinion,
|
|
1173
1030
|
deriveFreelanceIntent,
|
|
@@ -1180,7 +1037,6 @@ module.exports = {
|
|
|
1180
1037
|
establishSessionFromCredentials,
|
|
1181
1038
|
registerWithSocialAccount,
|
|
1182
1039
|
startProfileImport,
|
|
1183
|
-
pollProfileImport,
|
|
1184
1040
|
reattachConsentIdentity,
|
|
1185
1041
|
saveWorkSituation,
|
|
1186
1042
|
savePricing,
|
|
@@ -39,4 +39,19 @@ async function requestOnboardingStatus({ accessToken } = {}, { endpoint, timeout
|
|
|
39
39
|
return { ok: true, completed: data.completed === true };
|
|
40
40
|
}
|
|
41
41
|
|
|
42
|
-
|
|
42
|
+
// GET <certs>/interviews/:id/finalization -> COLLECTING | PROCESSING | READY | FAILED for a completed interview's evaluation.
|
|
43
|
+
async function requestInterviewFinalization({ interviewId, accessToken } = {}, { base, timeoutMs = DEFAULT_TIMEOUT_MS } = {}) {
|
|
44
|
+
if (!base || !interviewId) return { ok: false, reason: 'no-endpoint' };
|
|
45
|
+
let res;
|
|
46
|
+
try {
|
|
47
|
+
res = await postJsonWithTimeout(`${base}/${encodeURIComponent(interviewId)}/finalization`, null, timeoutMs, 'GET', null, authHeaders({ accessToken }));
|
|
48
|
+
} catch (e) {
|
|
49
|
+
return { ok: false, reason: (e && e.kind) || 'network-error' };
|
|
50
|
+
}
|
|
51
|
+
if (res.status < 200 || res.status >= 300) return { ok: false, reason: `http-${res.status}` };
|
|
52
|
+
const json = parseJson(res.raw);
|
|
53
|
+
const status = json && json.data && typeof json.data.status === 'string' ? json.data.status : null;
|
|
54
|
+
return status ? { ok: true, status } : { ok: false, reason: 'bad-response' };
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
module.exports = { requestOnboardingStatus, requestInterviewFinalization };
|
package/src/register-context.js
CHANGED
|
@@ -123,21 +123,6 @@ function findCvCandidates(home, personName) {
|
|
|
123
123
|
.map(({ path: cvPath, nameMatch, generic }) => ({ path: tildify(cvPath, { SHAKERS_CLI_HOME_DIR: home }), nameMatch, generic }));
|
|
124
124
|
}
|
|
125
125
|
|
|
126
|
-
// Which backend this install talks to, so the AI never calls a test sign-up
|
|
127
|
-
// "production".
|
|
128
|
-
function detectEnvironment(env) {
|
|
129
|
-
let base = '';
|
|
130
|
-
try {
|
|
131
|
-
base = String(require('./config').getHubBase(env) || '');
|
|
132
|
-
} catch {
|
|
133
|
-
return 'unknown';
|
|
134
|
-
}
|
|
135
|
-
if (/localhost|127\.0\.0\.1/.test(base)) return 'local';
|
|
136
|
-
if (/staging/.test(base)) return 'staging';
|
|
137
|
-
if (/\.dev\.|new-works/.test(base)) return 'development';
|
|
138
|
-
return base ? 'production' : 'unknown';
|
|
139
|
-
}
|
|
140
|
-
|
|
141
126
|
// Returns the signals found, each clearly a SUGGESTION to confirm. `talentName`
|
|
142
127
|
// (what the AI already knows) sharpens the CV match beyond the git name.
|
|
143
128
|
function readRegisterContextSuggestions({ cwd, env = process.env, talentName } = {}) {
|
|
@@ -146,7 +131,6 @@ function readRegisterContextSuggestions({ cwd, env = process.env, talentName } =
|
|
|
146
131
|
const gitName = gitConfig('user.name', cwd);
|
|
147
132
|
const personName = [talentName, gitName].filter((n) => typeof n === 'string' && n.trim()).join(' ');
|
|
148
133
|
return {
|
|
149
|
-
environment: detectEnvironment(env),
|
|
150
134
|
timezone: detectTimezone(),
|
|
151
135
|
gitName,
|
|
152
136
|
gitEmail: gitConfig('user.email', cwd),
|
|
@@ -156,4 +140,4 @@ function readRegisterContextSuggestions({ cwd, env = process.env, talentName } =
|
|
|
156
140
|
};
|
|
157
141
|
}
|
|
158
142
|
|
|
159
|
-
module.exports = { readRegisterContextSuggestions, detectTimezone, findCvCandidates, detectGithubUser, isGenericCvName,
|
|
143
|
+
module.exports = { readRegisterContextSuggestions, detectTimezone, findCvCandidates, detectGithubUser, isGenericCvName, nameTokens, fold };
|
package/src/roles-client.js
CHANGED
|
@@ -34,10 +34,12 @@ function normalizeRole(r) {
|
|
|
34
34
|
if (!clusterId) return null;
|
|
35
35
|
return {
|
|
36
36
|
clusterId,
|
|
37
|
-
|
|
37
|
+
// Hub's assigned-clusters rows name the role in clusterName.
|
|
38
|
+
name: typeof r.name === 'string' ? r.name : (typeof r.clusterName === 'string' ? r.clusterName : null),
|
|
38
39
|
category: typeof r.category === 'string' ? r.category : null,
|
|
39
40
|
proposalKind: typeof r.proposalKind === 'string' ? r.proposalKind : null,
|
|
40
41
|
type: typeof r.type === 'string' ? r.type : null,
|
|
42
|
+
source: typeof r.source === 'string' ? r.source : null,
|
|
41
43
|
};
|
|
42
44
|
}
|
|
43
45
|
|
package/src/signup-client.js
CHANGED
|
@@ -29,36 +29,48 @@ function buildRegistrationContext({ preferredLanguage, newsletterConsent, freela
|
|
|
29
29
|
return context;
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
-
//
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
)
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
32
|
+
// Hub refuses these claims with a coded error and creates nothing, so registering without the code is safe.
|
|
33
|
+
const UNCLAIMABLE_CODES = new Set(['auth-engine.claim_code_invalid', 'users.unregistered_talent_not_claimable']);
|
|
34
|
+
|
|
35
|
+
function codeFrom(raw) {
|
|
36
|
+
const body = parseJson(raw);
|
|
37
|
+
return body && typeof body.code === 'string' ? body.code : null;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
async function postCompleteRegistration(body, { endpoint, headers, timeoutMs }) {
|
|
41
41
|
let complete;
|
|
42
42
|
try {
|
|
43
|
-
complete = await postJsonWithTimeout(
|
|
44
|
-
endpoint,
|
|
45
|
-
{ userType: 'WORKS_TALENT', context },
|
|
46
|
-
timeoutMs,
|
|
47
|
-
'POST',
|
|
48
|
-
null,
|
|
49
|
-
Object.keys(headers).length ? headers : null,
|
|
50
|
-
);
|
|
43
|
+
complete = await postJsonWithTimeout(endpoint, body, timeoutMs, 'POST', null, Object.keys(headers).length ? headers : null);
|
|
51
44
|
} catch (e) {
|
|
52
45
|
return { ok: false, reason: (e && e.kind) || 'network-error' };
|
|
53
46
|
}
|
|
54
47
|
if (complete.status < 200 || complete.status >= 300) {
|
|
55
|
-
return { ok: false, reason: `complete-registration-http-${complete.status}`, status: complete.status, message: messageFrom(complete.raw) };
|
|
48
|
+
return { ok: false, reason: `complete-registration-http-${complete.status}`, status: complete.status, code: codeFrom(complete.raw), message: messageFrom(complete.raw) };
|
|
56
49
|
}
|
|
57
50
|
return { ok: true };
|
|
58
51
|
}
|
|
59
52
|
|
|
53
|
+
// Shared complete-registration call. With a claimCode it claims the MCP sign-up's UNREGISTERED profile; an unclaimable code still registers the account, with claimed:false.
|
|
54
|
+
async function requestCompleteRegistration(
|
|
55
|
+
context = {},
|
|
56
|
+
{ endpoint, cookie = null, bearerToken = null, claimCode = null, timeoutMs = DEFAULT_TIMEOUT_MS } = {},
|
|
57
|
+
) {
|
|
58
|
+
if (!endpoint) return { ok: false, reason: 'no-endpoint' };
|
|
59
|
+
const headers = {};
|
|
60
|
+
if (cookie) headers.Cookie = cookie;
|
|
61
|
+
if (bearerToken) headers.Authorization = `Bearer ${bearerToken}`;
|
|
62
|
+
const base = { userType: 'WORKS_TALENT', context };
|
|
63
|
+
if (typeof claimCode !== 'string' || !claimCode) return postCompleteRegistration(base, { endpoint, headers, timeoutMs });
|
|
64
|
+
|
|
65
|
+
const claimed = await postCompleteRegistration({ ...base, claimCode }, { endpoint, headers, timeoutMs });
|
|
66
|
+
if (claimed.ok) return { ok: true, claimed: true };
|
|
67
|
+
if (!UNCLAIMABLE_CODES.has(claimed.code)) return claimed;
|
|
68
|
+
const plain = await postCompleteRegistration(base, { endpoint, headers, timeoutMs });
|
|
69
|
+
return plain.ok ? { ok: true, claimed: false, claimError: claimed.code } : plain;
|
|
70
|
+
}
|
|
71
|
+
|
|
60
72
|
async function requestSignUp(
|
|
61
|
-
{ name, lastName, email, password, preferredLanguage, newsletterConsent, freelanceType, freelanceIntent } = {},
|
|
73
|
+
{ name, lastName, email, password, preferredLanguage, newsletterConsent, freelanceType, freelanceIntent, claimCode = null } = {},
|
|
62
74
|
{ signUpEndpoint, completeRegistrationEndpoint, timeoutMs = DEFAULT_TIMEOUT_MS } = {},
|
|
63
75
|
) {
|
|
64
76
|
if (!signUpEndpoint) return { ok: false, reason: 'no-endpoint' };
|
|
@@ -83,9 +95,9 @@ async function requestSignUp(
|
|
|
83
95
|
if (!completeRegistrationEndpoint) return { ok: false, reason: 'no-endpoint' };
|
|
84
96
|
|
|
85
97
|
const context = buildRegistrationContext({ preferredLanguage, newsletterConsent, freelanceType, freelanceIntent });
|
|
86
|
-
const complete = await requestCompleteRegistration(context, { endpoint: completeRegistrationEndpoint, cookie, timeoutMs });
|
|
98
|
+
const complete = await requestCompleteRegistration(context, { endpoint: completeRegistrationEndpoint, cookie, claimCode, timeoutMs });
|
|
87
99
|
if (!complete.ok) return complete;
|
|
88
|
-
return { ok: true, accountExists: false };
|
|
100
|
+
return claimCode ? { ok: true, accountExists: false, claimed: complete.claimed === true } : { ok: true, accountExists: false };
|
|
89
101
|
}
|
|
90
102
|
|
|
91
103
|
module.exports = { requestSignUp, requestCompleteRegistration, buildRegistrationContext };
|