shakers-cli-beta 0.10.4 → 0.12.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.
@@ -4,7 +4,6 @@
4
4
 
5
5
  const { postJsonWithTimeout } = require('./backend-request');
6
6
  const { reasonForError } = require('./onboarding-client');
7
- const { toUpperLang } = require('./lang-codes');
8
7
 
9
8
  const DEFAULT_TIMEOUT_MS = 20000;
10
9
 
@@ -154,87 +153,9 @@ async function discoverOfferableDimensions(deps = {}, { accessToken, hubAccessTo
154
153
  return { ok: true, mainRole: mine.mainRole, offerable, unmatched };
155
154
  }
156
155
 
157
- async function requestCreateCertificationInterview(
158
- { dimensionKey, languageCode, accessToken } = {},
159
- { endpoint, timeoutMs = DEFAULT_TIMEOUT_MS } = {},
160
- ) {
161
- if (!endpoint) return { ok: false, reason: 'no-endpoint' };
162
- if (!dimensionKey) return { ok: false, reason: 'no-dimension' };
163
- const body = { dimensionKey };
164
- if (languageCode) body.languageCode = toUpperLang(languageCode);
165
- let res;
166
- try {
167
- res = await postJsonWithTimeout(endpoint, body, timeoutMs, 'POST', null, bearerHeaders(accessToken));
168
- } catch (e) {
169
- return { ok: false, reason: (e && e.kind) || 'network-error' };
170
- }
171
- if (res.status < 200 || res.status >= 300) {
172
- const reason = reasonForError(res.status, res.raw);
173
- if (reason !== 'dimension-on-cooldown') return { ok: false, reason };
174
- // certs puts the date in the message only: "... available on 2026-10-28T14:50:38.265Z".
175
- const available = String(res.raw || '').match(/available on (\d{4}-\d{2}-\d{2}T[\d:.]+Z)/);
176
- return { ok: false, reason, availableOn: available ? available[1] : null };
177
- }
178
- const data = payloadOf(res.raw);
179
- const interviewId = pick(data || {}, ['interviewId', 'id']);
180
- if (!interviewId) return { ok: false, reason: 'bad-response' };
181
- return { ok: true, interviewId, language: pick(data || {}, ['language', 'languageCode']) || null };
182
- }
183
-
184
- // Poll the verdict; a 2xx/404/425/202 with no band yet resolves `ready:false` (retriable).
185
- async function requestCertificationReport(
186
- { interviewId, accessToken } = {},
187
- { base, timeoutMs = DEFAULT_TIMEOUT_MS } = {},
188
- ) {
189
- if (!base) return { ok: false, reason: 'no-endpoint' };
190
- if (!interviewId) return { ok: false, reason: 'no-interview' };
191
- const url = `${base.replace(/\/+$/, '')}/${encodeURIComponent(interviewId)}/report`;
192
- let res;
193
- try {
194
- res = await postJsonWithTimeout(url, null, timeoutMs, 'GET', null, bearerHeaders(accessToken));
195
- } catch (e) {
196
- return { ok: false, reason: (e && e.kind) || 'network-error' };
197
- }
198
- if (res.status === 404 || res.status === 425 || res.status === 202) return { ok: true, ready: false, report: null };
199
- if (res.status < 200 || res.status >= 300) return { ok: false, reason: reasonForError(res.status, res.raw) };
200
- const data = payloadOf(res.raw);
201
- if (data === undefined || data === null || typeof data !== 'object') return { ok: true, ready: false, report: null };
202
- const band = pick(data, ['band', 'level', 'combinedLevel']);
203
- // A report with too little evidence for a level has band null; it is still finished.
204
- if (!band && !pick(data, ['evaluationId'])) return { ok: true, ready: false, report: null };
205
- return { ok: true, ready: true, report: data };
206
- }
207
-
208
- // The case the web preview shows before starting (works `get-dimension-case`): the exercise statement, else the
209
- // version's brief. `case: null` for a template with neither.
210
- async function requestDimensionCase(
211
- { dimensionKey, accessToken } = {},
212
- { base, timeoutMs = DEFAULT_TIMEOUT_MS } = {},
213
- ) {
214
- if (!base) return { ok: false, reason: 'no-endpoint' };
215
- if (!dimensionKey) return { ok: false, reason: 'no-dimension' };
216
- const path = String(dimensionKey).split('/').map(encodeURIComponent).join('/');
217
- const url = `${base.replace(/\/+$/, '')}/dimension/${path}/case`;
218
- let res;
219
- try {
220
- res = await postJsonWithTimeout(url, null, timeoutMs, 'GET', null, bearerHeaders(accessToken));
221
- } catch (e) {
222
- return { ok: false, reason: (e && e.kind) || 'network-error' };
223
- }
224
- if (res.status < 200 || res.status >= 300) return { ok: false, reason: reasonForError(res.status, res.raw) };
225
- const data = payloadOf(res.raw);
226
- if (!data || typeof data !== 'object') return { ok: false, reason: 'bad-response' };
227
- const text = (value) => (typeof value === 'string' && value.trim() ? value.trim() : null);
228
- const brief = text(data.statement) || text(data.candidateBrief);
229
- return { ok: true, case: brief ? { brief, testType: text(data.testType) } : null };
230
- }
231
-
232
156
  module.exports = {
233
157
  requestMyCertifications,
234
158
  requestTemplatesByDimension,
235
- requestCreateCertificationInterview,
236
- requestCertificationReport,
237
- requestDimensionCase,
238
159
  composeDimensionKey,
239
160
  composeOfferableDimensions,
240
161
  discoverOfferableDimensions,
@@ -1,28 +1,33 @@
1
1
  'use strict';
2
2
 
3
- // `certify` dimension flow: discover -> pick -> create -> LiveKit interview -> poll verdict.
3
+ // `certify` dimension flow: discover -> pick -> hand over the web deep link. The interview runs on the web.
4
4
 
5
5
  const { runInteractiveMultiSelect } = require('./interactive-select');
6
6
 
7
7
  function makeCertifyDimensionDeps(overrides = {}) {
8
8
  const config = require('./config');
9
9
  const client = require('./certify-dimension-client');
10
- const onboardingFlow = require('./onboarding-flow');
11
10
  return {
12
- getCertificationInterviewsEndpoint: config.getCertificationInterviewsEndpoint,
13
- // `{certsBase}/interviews` — shared base for LiveKit start/complete + the report poll.
14
- getInterviewsBase: config.getOnboardingInterviewsEndpoint,
11
+ getTalentProfileUrl: config.getTalentProfileUrl,
15
12
  discoverOfferableDimensions: (opts) => client.discoverOfferableDimensions({}, opts),
16
- requestCreateCertificationInterview: client.requestCreateCertificationInterview,
17
- requestCertificationReport: client.requestCertificationReport,
18
- requestDimensionCase: client.requestDimensionCase,
19
- conductLivekitInterview: onboardingFlow.conductLivekitInterview,
20
- onboardingDeps: onboardingFlow.makeDeps(),
21
- sleep: (ms) => new Promise((r) => setTimeout(r, ms)),
13
+ buildWebLink,
22
14
  ...overrides,
23
15
  };
24
16
  }
25
17
 
18
+ // The web route validates the decoded `dimension` param against `^[^/]+\/[^/]+$`,
19
+ // so the separator slash stays literal and each segment is url-encoded.
20
+ function buildWebLink(profileUrl, { clusterId, slug } = {}) {
21
+ if (!profileUrl || !clusterId || !slug) return null;
22
+ let origin;
23
+ try {
24
+ origin = new URL(profileUrl).origin;
25
+ } catch {
26
+ return null;
27
+ }
28
+ return `${origin}/certifications?dimension=${encodeURIComponent(clusterId)}/${encodeURIComponent(slug)}`;
29
+ }
30
+
26
31
  // Numbered fallback when `ask` can't release stdin for raw mode.
27
32
  async function askNumberedChoice(ask, promptText, count) {
28
33
  const raw = (await ask(promptText)).trim();
@@ -62,53 +67,6 @@ function resolveDimensionArg(arg, items) {
62
67
  return items.find((d) => d.dimensionKey === raw || d.slug === raw) || undefined;
63
68
  }
64
69
 
65
- // The evaluation lands one to two minutes after the interview ends, four when the queue is busy.
66
- async function pollReport(deps, { interviewId, accessToken }, { base, attempts = 60, intervalMs = 5000 } = {}) {
67
- for (let i = 0; i < attempts; i++) {
68
- const res = await deps.requestCertificationReport({ interviewId, accessToken }, { base });
69
- if (!res.ok) return { ok: false, reason: res.reason };
70
- if (res.ready) return { ok: true, report: res.report };
71
- if (i < attempts - 1) await deps.sleep(intervalMs);
72
- }
73
- return { ok: false, reason: 'timeout' };
74
- }
75
-
76
- function formatDay(iso, lang) {
77
- const d = iso ? new Date(iso) : null;
78
- if (!d || Number.isNaN(d.getTime())) return null;
79
- return d.toLocaleDateString(lang === 'en' ? 'en-GB' : 'es-ES', { day: 'numeric', month: 'long', year: 'numeric' });
80
- }
81
-
82
- function renderVerdict(report, cd) {
83
- const lines = [];
84
- const band = report.band || report.level || report.combinedLevel;
85
- const dimension = report.dimensionName || report.dimension || report.dimensionKey;
86
- if (dimension) lines.push(cd.verdictDimension(dimension));
87
- if (band) lines.push(cd.verdictBand(band));
88
- if (typeof report.certified === 'boolean') lines.push(report.certified ? cd.verdictCertified : cd.verdictNotCertified);
89
- const summary = report.summary || report.rationale || report.feedback;
90
- if (typeof summary === 'string' && summary.trim()) lines.push(summary.trim());
91
- const areas = Array.isArray(report.areas) ? report.areas : null;
92
- if (areas && areas.length) {
93
- for (const a of areas) {
94
- const name = a && (a.name || a.area || a.tag);
95
- const note = a && (a.note || a.comment);
96
- if (name) lines.push(`- ${name}${note ? `: ${note}` : ''}`);
97
- }
98
- }
99
- return lines.join('\n ');
100
- }
101
-
102
- // The statement is Markdown; the terminal shows it without the emphasis markers.
103
- function renderCase(dimensionCase, cd) {
104
- const brief = dimensionCase.brief.replace(/(\*\*|__)(.+?)\1/g, '$2');
105
- const expectations = cd.caseExpectations[dimensionCase.testType];
106
- const lines = brief.split('\n');
107
- if (expectations) lines.push('', cd.caseExpectationsTitle, ...Object.values(expectations).map((e) => `- ${e}`));
108
- lines.push('', cd.caseBeforeStart);
109
- return lines.join('\n ');
110
- }
111
-
112
70
  async function runCertifyDimension({ io, ask, stdinIsTTY, session, lang, catalog, opts = {}, deps = makeCertifyDimensionDeps() }) {
113
71
  const cd = catalog.certifyDimension;
114
72
  const accessToken = session ? session.accessToken : null;
@@ -157,60 +115,21 @@ async function runCertifyDimension({ io, ask, stdinIsTTY, session, lang, catalog
157
115
  }
158
116
  }
159
117
 
160
- const createEndpoint = deps.getCertificationInterviewsEndpoint();
161
- const created = await io.withProgress(cd.creating, () =>
162
- deps.requestCreateCertificationInterview(
163
- { dimensionKey: chosen.dimensionKey, languageCode: lang, accessToken },
164
- { endpoint: createEndpoint },
165
- ));
166
- if (!created.ok) {
167
- io.error(created.reason === 'dimension-on-cooldown'
168
- ? cd.onCooldown(formatDay(created.availableOn, lang))
169
- : cd.createFailed(created.reason));
170
- return { ok: false, reason: created.reason, step: 'create' };
171
- }
172
-
173
- // A failed read leaves the run as it was before the case existed, like the web preview does.
174
- const caseRead = await deps.requestDimensionCase(
175
- { dimensionKey: chosen.dimensionKey, accessToken },
176
- { base: deps.getInterviewsBase() },
177
- );
178
- if (caseRead.ok && caseRead.case) {
179
- io.section(cd.caseTitle);
180
- io.notify(renderCase(caseRead.case, cd));
181
- if (stdinIsTTY) await ask(` ${cd.caseReadyPrompt}`);
182
- }
183
-
184
- const interview = await deps.conductLivekitInterview(io, deps.onboardingDeps, {
185
- interviewId: created.interviewId,
186
- language: lang,
187
- plain: true,
188
- });
189
- if (!interview.ok) {
190
- io.warn(interview.reason === 'needs-web' ? cd.needsWeb : cd.interviewUnavailable(interview.reason));
191
- return { ok: false, reason: interview.reason, step: 'interview' };
192
- }
193
-
194
- const reportBase = deps.getInterviewsBase();
195
- const polled = await io.withProgress(cd.reportPolling, () =>
196
- pollReport(deps, { interviewId: created.interviewId, accessToken }, { base: reportBase }));
197
- if (!polled.ok) {
198
- io.warn(polled.reason === 'timeout' ? cd.reportTimeout : cd.reportFailed(polled.reason));
199
- return { ok: true, completed: 'interview', verdict: null };
118
+ const link = deps.buildWebLink(deps.getTalentProfileUrl(), { clusterId: chosen.clusterId, slug: chosen.slug });
119
+ if (!link) {
120
+ io.warn(cd.webHandoffNoLink);
121
+ return { ok: true, handoff: null, dimension: chosen.dimensionKey };
200
122
  }
201
123
 
202
- io.section(cd.verdictTitle);
203
- io.notify(renderVerdict(polled.report, cd));
204
- io.success(cd.done);
205
- return { ok: true, completed: 'interview', verdict: polled.report };
124
+ io.notify(cd.webHandoff);
125
+ io.notify(cd.webHandoffLink(link));
126
+ return { ok: true, handoff: link, dimension: chosen.dimensionKey };
206
127
  }
207
128
 
208
129
  module.exports = {
209
130
  makeCertifyDimensionDeps,
210
131
  runCertifyDimension,
132
+ buildWebLink,
211
133
  chooseDimension,
212
134
  resolveDimensionArg,
213
- pollReport,
214
- renderVerdict,
215
- renderCase,
216
135
  };
@@ -0,0 +1,105 @@
1
+ 'use strict';
2
+
3
+ const os = require('os');
4
+ const path = require('path');
5
+ const { scrubSecrets } = require('./agent-synthesis');
6
+ const { loadAuthSession } = require('./auth-session-store');
7
+ const { getCertsBase } = require('./config');
8
+ const { postJsonWithTimeout } = require('./backend-request');
9
+
10
+ // Fire-and-forget: short timeout, every error swallowed, so a logging failure never blocks or fails a command.
11
+ const AUDIT_TIMEOUT_MS = 1500;
12
+
13
+ // Capped at 200 chars AFTER scrubbing (mirrors MAX_COMMAND_LEN in session-scan.js) so a secret straddling the cut cannot leak.
14
+ const MAX_ARGS_LEN = 200;
15
+
16
+ function cliVersion() {
17
+ try {
18
+ return require(path.join(__dirname, '..', 'package.json')).version || null;
19
+ } catch {
20
+ return null;
21
+ }
22
+ }
23
+
24
+ // Scrub THEN cap: scrubSecrets can GROW text, so the cap is measured on the scrubbed string that goes on the wire.
25
+ function redactArgs(argv) {
26
+ if (!Array.isArray(argv) || argv.length === 0) return null;
27
+ const scrubbed = scrubSecrets(argv.join(' ')).trim().slice(0, MAX_ARGS_LEN);
28
+ return scrubbed || null;
29
+ }
30
+
31
+ // Anonymous runs carry only a best-effort machine hint; authenticated runs identify via the Bearer instead.
32
+ function machineHint() {
33
+ let osUser = null;
34
+ try {
35
+ const info = os.userInfo();
36
+ osUser = info && typeof info.username === 'string' ? info.username : null;
37
+ } catch {
38
+ osUser = null;
39
+ }
40
+ let hostname = null;
41
+ try {
42
+ hostname = os.hostname() || null;
43
+ } catch {
44
+ hostname = null;
45
+ }
46
+ return { osUser, hostname };
47
+ }
48
+
49
+ // Builds the record + headers; returns null when there is no endpoint to post to.
50
+ function buildAudit(command, argv, env) {
51
+ let base;
52
+ try {
53
+ base = getCertsBase(env);
54
+ } catch {
55
+ base = null;
56
+ }
57
+ if (!base) return null;
58
+
59
+ const session = loadAuthSession(env);
60
+ const token =
61
+ session && (session.accessToken || session.hubAccessToken)
62
+ ? session.accessToken || session.hubAccessToken
63
+ : null;
64
+
65
+ const body = {
66
+ command: String(command || 'unknown').slice(0, 100),
67
+ args: redactArgs(argv),
68
+ cliVersion: cliVersion(),
69
+ clientTimestamp: new Date().toISOString(),
70
+ };
71
+
72
+ // Machine hint only when anonymous; identity comes from the token otherwise.
73
+ if (!token) {
74
+ const { osUser, hostname } = machineHint();
75
+ body.osUser = osUser;
76
+ body.hostname = hostname;
77
+ }
78
+
79
+ const headers = token ? { Authorization: `Bearer ${token}` } : null;
80
+ const endpoint = `${base.replace(/\/+$/, '')}/cli/command-log`;
81
+ return { endpoint, body, headers };
82
+ }
83
+
84
+ // Emits ONE fire-and-forget audit record; never throws or rejects. Returns the `.catch`-guarded promise so tests can await it (production does not).
85
+ function emitCommandAudit({ command, argv = [], env = process.env } = {}) {
86
+ try {
87
+ const audit = buildAudit(command, argv, env);
88
+ if (!audit) return Promise.resolve();
89
+ return postJsonWithTimeout(
90
+ audit.endpoint,
91
+ audit.body,
92
+ AUDIT_TIMEOUT_MS,
93
+ 'POST',
94
+ null,
95
+ audit.headers,
96
+ ).then(
97
+ () => {},
98
+ () => {},
99
+ );
100
+ } catch {
101
+ return Promise.resolve();
102
+ }
103
+ }
104
+
105
+ module.exports = { emitCommandAudit, redactArgs, buildAudit };
package/src/config.js CHANGED
@@ -264,7 +264,7 @@ function getAuthTokenEndpoint(env = process.env) {
264
264
  function getCompleteRegistrationEndpoint(env = process.env) {
265
265
  const explicit = readEnv(env, 'COMPLETE_REGISTRATION_ENDPOINT');
266
266
  if (explicit && explicit.trim()) return explicit.trim();
267
- return deriveFromHub(env, 'works/auth/complete-registration');
267
+ return deriveFromHub(env, 'works/talent/auth/complete-registration');
268
268
  }
269
269
 
270
270
  // Google via device flow (RFC 8628): the Hub broker owns all Google config; the CLI holds no client_id.
@@ -287,7 +287,7 @@ function getDeviceTokenEndpoint(env = process.env) {
287
287
  function getMyCertificationsEndpoint(env = process.env) {
288
288
  const explicit = readEnv(env, 'MY_CERTIFICATIONS_ENDPOINT');
289
289
  if (explicit && explicit.trim()) return explicit.trim();
290
- return deriveFromHub(env, 'works/certifications/me');
290
+ return deriveFromHub(env, 'works/talent/certifications/me');
291
291
  }
292
292
 
293
293
  // Role/cluster endpoints (hub, talent Bearer). available-roles = the catalog the
@@ -296,19 +296,19 @@ function getMyCertificationsEndpoint(env = process.env) {
296
296
  function getAvailableRolesEndpoint(env = process.env) {
297
297
  const explicit = readEnv(env, 'AVAILABLE_ROLES_ENDPOINT');
298
298
  if (explicit && explicit.trim()) return explicit.trim();
299
- return deriveFromHub(env, 'works/certifications/me/available-roles');
299
+ return deriveFromHub(env, 'works/talent/certifications/me/available-roles');
300
300
  }
301
301
 
302
302
  function getSetMainRoleEndpoint(env = process.env) {
303
303
  const explicit = readEnv(env, 'SET_MAIN_ROLE_ENDPOINT');
304
304
  if (explicit && explicit.trim()) return explicit.trim();
305
- return deriveFromHub(env, 'works/certifications/me/main-role');
305
+ return deriveFromHub(env, 'works/talent/certifications/me/main-role');
306
306
  }
307
307
 
308
308
  function getAssignedClustersEndpoint(env = process.env) {
309
309
  const explicit = readEnv(env, 'ASSIGNED_CLUSTERS_ENDPOINT');
310
310
  if (explicit && explicit.trim()) return explicit.trim();
311
- return deriveFromHub(env, 'works/talents/me/work-details/assigned-clusters');
311
+ return deriveFromHub(env, 'works/talent/me/work-details/assigned-clusters');
312
312
  }
313
313
 
314
314
  function getTemplatesByDimensionEndpoint(env = process.env) {
@@ -326,12 +326,20 @@ function getMyAssessmentsEndpoint(env = process.env) {
326
326
  return base ? `${base}/users/me/assessments` : null;
327
327
  }
328
328
 
329
+ // `certify` creates its dimension interview here (certs, talent Bearer).
330
+ function getCertificationInterviewsEndpoint(env = process.env) {
331
+ const explicit = readEnv(env, 'CERTIFICATION_INTERVIEWS_ENDPOINT');
332
+ if (explicit && explicit.trim()) return explicit.trim();
333
+ const base = getCertsBase(env);
334
+ return base ? `${base}/users/me/certification-interviews` : null;
335
+ }
336
+
329
337
  // Find Positions list (hub `works/positions/find`, talent Bearer). Serves the
330
338
  // All/Recommended/Saved tabs via `criteria.savedByMe`; ordered by match desc.
331
339
  function getFindPositionsEndpoint(env = process.env) {
332
340
  const explicit = readEnv(env, 'FIND_POSITIONS_ENDPOINT');
333
341
  if (explicit && explicit.trim()) return explicit.trim();
334
- return deriveFromHub(env, 'works/positions/find');
342
+ return deriveFromHub(env, 'works/talent/positions/find');
335
343
  }
336
344
 
337
345
  // Marketplace read/write (hub, talent Bearer). The id-taking getters return null
@@ -340,20 +348,20 @@ function getPositionDetailEndpoint(positionId, env = process.env) {
340
348
  const explicit = readEnv(env, 'POSITION_DETAIL_ENDPOINT');
341
349
  if (explicit && explicit.trim()) return explicit.trim();
342
350
  if (positionId == null || typeof positionId === 'object') return null;
343
- return deriveFromHub(env, `works/positions/${encodeURIComponent(positionId)}/detail`);
351
+ return deriveFromHub(env, `works/talent/positions/${encodeURIComponent(positionId)}/detail`);
344
352
  }
345
353
 
346
354
  function getSavePositionEndpoint(env = process.env) {
347
355
  const explicit = readEnv(env, 'SAVE_POSITION_ENDPOINT');
348
356
  if (explicit && explicit.trim()) return explicit.trim();
349
- return deriveFromHub(env, 'works/saved-positions');
357
+ return deriveFromHub(env, 'works/talent/saved-positions');
350
358
  }
351
359
 
352
360
  function getUnsavePositionEndpoint(positionId, env = process.env) {
353
361
  const explicit = readEnv(env, 'UNSAVE_POSITION_ENDPOINT');
354
362
  if (explicit && explicit.trim()) return explicit.trim();
355
363
  if (positionId == null || typeof positionId === 'object') return null;
356
- return deriveFromHub(env, `works/saved-positions/${encodeURIComponent(positionId)}`);
364
+ return deriveFromHub(env, `works/talent/saved-positions/${encodeURIComponent(positionId)}`);
357
365
  }
358
366
 
359
367
  function getReceivedInvitationsEndpoint(env = process.env) {
@@ -365,7 +373,7 @@ function getReceivedInvitationsEndpoint(env = process.env) {
365
373
  function getMyCandidaturesEndpoint(env = process.env) {
366
374
  const explicit = readEnv(env, 'MY_CANDIDATURES_ENDPOINT');
367
375
  if (explicit && explicit.trim()) return explicit.trim();
368
- return deriveFromHub(env, 'works/candidatures/me');
376
+ return deriveFromHub(env, 'works/talent/candidatures/me');
369
377
  }
370
378
 
371
379
  function getMeAvailabilityEndpoint(env = process.env) {
@@ -377,13 +385,13 @@ function getMeAvailabilityEndpoint(env = process.env) {
377
385
  function getMeProfileEndpoint(env = process.env) {
378
386
  const explicit = readEnv(env, 'ME_PROFILE_ENDPOINT');
379
387
  if (explicit && explicit.trim()) return explicit.trim();
380
- return deriveFromHub(env, 'works/me/profile');
388
+ return deriveFromHub(env, 'works/talent/me/profile-summary');
381
389
  }
382
390
 
383
391
  function getTalentMeProfileEndpoint(env = process.env) {
384
392
  const explicit = readEnv(env, 'TALENT_ME_PROFILE_ENDPOINT');
385
393
  if (explicit && explicit.trim()) return explicit.trim();
386
- return deriveFromHub(env, 'works/talents/me/profile');
394
+ return deriveFromHub(env, 'works/talent/me/profile');
387
395
  }
388
396
 
389
397
  // Alma (the AI assistant) is a SEPARATE service — NOT derived from hubBase.
@@ -444,7 +452,7 @@ function getUsageReportEndpoint(env = process.env) {
444
452
  function getAiProfileEndpoint(env = process.env) {
445
453
  const explicit = readEnv(env, 'AI_PROFILE_ENDPOINT');
446
454
  if (explicit && explicit.trim()) return explicit.trim();
447
- return deriveFromHub(env, 'works/me/ai-profile');
455
+ return deriveFromHub(env, 'works/talent/me/ai-profile');
448
456
  }
449
457
 
450
458
  // Evidence emission endpoint for the `cli_scan` instrument (talents-ai-score Phase 2).
@@ -476,7 +484,7 @@ function getPortfolioDraftDescriptionEndpoint(env = process.env) {
476
484
  function getPortfolioUpdateEndpoint(env = process.env) {
477
485
  const explicit = readEnv(env, 'PORTFOLIO_UPDATE_ENDPOINT');
478
486
  if (explicit && explicit.trim()) return explicit.trim();
479
- return deriveFromHub(env, 'works/portfolios');
487
+ return deriveFromHub(env, 'works/talent/portfolios');
480
488
  }
481
489
 
482
490
  function getAgentsDeclareEndpoint(env = process.env) {
@@ -502,7 +510,7 @@ function getAgentPortfoliosEndpoint(agentId, env = process.env) {
502
510
  // Null on missing/non-scalar id: no bogus URL, and keeps config.test.js's `fn(env)`
503
511
  // getter sweep honest when the env object lands in `agentId`.
504
512
  if (agentId == null || typeof agentId === 'object') return null;
505
- return deriveFromHub(env, `works/me/agents/${encodeURIComponent(agentId)}/portfolios`);
513
+ return deriveFromHub(env, `works/talent/me/agents/${encodeURIComponent(agentId)}/portfolios`);
506
514
  }
507
515
 
508
516
  // Onboarding endpoints (evaluation-mcp, ADR-011..015/024 — the register flow).
@@ -533,7 +541,7 @@ function getSignUpEndpoint(env = process.env) {
533
541
  function getImportProfileEndpoint(env = process.env) {
534
542
  const explicit = readEnv(env, 'IMPORT_PROFILE_ENDPOINT');
535
543
  if (explicit && explicit.trim()) return explicit.trim();
536
- return deriveFromHub(env, 'works/me/import-profile');
544
+ return deriveFromHub(env, 'works/talent/me/import-profile');
537
545
  }
538
546
 
539
547
  // MCP sign-up (ticket 39540): public UNREGISTERED + claim code, its import status, and the one-time web login.
@@ -542,7 +550,7 @@ function getMcpSignupEndpoint(env = process.env) {
542
550
  }
543
551
 
544
552
  function getMyImportStatusEndpoint(env = process.env) {
545
- return deriveFromHub(env, 'works/me/import-profile/status');
553
+ return deriveFromHub(env, 'works/talent/me/import-profile/status');
546
554
  }
547
555
 
548
556
  function getOneTimeTokenEndpoint(env = process.env) {
@@ -564,25 +572,25 @@ function getOneTimeLoginUrl(token, env = process.env) {
564
572
  function getProfessionalDetailsEndpoint(env = process.env) {
565
573
  const explicit = readEnv(env, 'PROFESSIONAL_DETAILS_ENDPOINT');
566
574
  if (explicit && explicit.trim()) return explicit.trim();
567
- return deriveFromHub(env, 'works/me/professional-details');
575
+ return deriveFromHub(env, 'works/talent/me/professional-details');
568
576
  }
569
577
 
570
578
  function getPricingRateEndpoint(env = process.env) {
571
579
  const explicit = readEnv(env, 'PRICING_RATE_ENDPOINT');
572
580
  if (explicit && explicit.trim()) return explicit.trim();
573
- return deriveFromHub(env, 'works/talents/me/work-details/pricing-rate');
581
+ return deriveFromHub(env, 'works/talent/me/work-details/pricing-rate');
574
582
  }
575
583
 
576
584
  function getSetAvailabilityEndpoint(env = process.env) {
577
585
  const explicit = readEnv(env, 'AVAILABILITY_ENDPOINT');
578
586
  if (explicit && explicit.trim()) return explicit.trim();
579
- return deriveFromHub(env, 'works/talents/me/work-details/availability');
587
+ return deriveFromHub(env, 'works/talent/me/work-details/availability');
580
588
  }
581
589
 
582
590
  function getLanguagesEndpoint(env = process.env) {
583
591
  const explicit = readEnv(env, 'LANGUAGES_ENDPOINT');
584
592
  if (explicit && explicit.trim()) return explicit.trim();
585
- return deriveFromHub(env, 'works/talents/me/work-details/languages');
593
+ return deriveFromHub(env, 'works/talent/me/work-details/languages');
586
594
  }
587
595
 
588
596
  function getLanguagesCatalogEndpoint(env = process.env) {
@@ -616,7 +624,7 @@ function getTranslationsEndpoint(env = process.env) {
616
624
  function getTalentMeEndpoint(env = process.env) {
617
625
  const explicit = readEnv(env, 'TALENT_ME_ENDPOINT');
618
626
  if (explicit && explicit.trim()) return explicit.trim();
619
- return deriveFromHub(env, 'works/me');
627
+ return deriveFromHub(env, 'works/users/me');
620
628
  }
621
629
 
622
630
  function getOnboardingInterviewsEndpoint(env = process.env) {
@@ -758,6 +766,7 @@ module.exports = {
758
766
  getAssignedClustersEndpoint,
759
767
  getTemplatesByDimensionEndpoint,
760
768
  getMyAssessmentsEndpoint,
769
+ getCertificationInterviewsEndpoint,
761
770
  // Find Positions list (`find-projects` command / `find_projects` MCP tool).
762
771
  getFindPositionsEndpoint,
763
772
  // Marketplace (show-project / save-project / unsave-project / invitations).
@@ -1,6 +1,6 @@
1
1
  'use strict';
2
2
 
3
- // Find Positions list client (hub `works/positions/find`, talent Bearer). Each
3
+ // Find Positions list client. Each
4
4
  // item carries the parent Project's id/name — a "project" here is the Position's
5
5
  // parent, not a separate list. Pagination (limit/offset) and filters map to the
6
6
  // endpoint's `criteria`; the server orders by match desc and returns the total.