shakers-cli 0.16.0 → 0.18.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.
Files changed (69) hide show
  1. package/README.md +21 -6
  2. package/bin/ai-usage.js +31 -12
  3. package/bin/command-audit-worker.js +19 -0
  4. package/bin/login.js +4 -8
  5. package/bin/mcp.js +32 -21
  6. package/bin/register.js +12 -30
  7. package/bin/shakers-cli.js +15 -10
  8. package/package.json +1 -1
  9. package/src/account-marker.js +40 -0
  10. package/src/account-trace.js +23 -0
  11. package/src/agents-client.js +1 -0
  12. package/src/ai-usage-review.js +103 -0
  13. package/src/auth-client.js +7 -107
  14. package/src/auth-session-store.js +12 -11
  15. package/src/backend-request.js +3 -24
  16. package/src/cli-skill.js +77 -0
  17. package/src/command-audit.js +47 -4
  18. package/src/config.js +2 -16
  19. package/src/consent-flow.js +9 -3
  20. package/src/detect-repos.js +4 -8
  21. package/src/detectors.js +23 -0
  22. package/src/device-auth-store.js +2 -2
  23. package/src/device-login.js +11 -9
  24. package/src/env-paths.js +9 -1
  25. package/src/i18n.js +186 -102
  26. package/src/mcp-ai-usage-result-tool.js +38 -14
  27. package/src/mcp-ai-usage-tool.js +317 -172
  28. package/src/mcp-choice.js +171 -34
  29. package/src/mcp-detector.js +26 -2
  30. package/src/mcp-device-login-tools.js +40 -8
  31. package/src/mcp-install.js +292 -53
  32. package/src/mcp-instructions.js +43 -17
  33. package/src/mcp-onboarding-tools.js +1 -3
  34. package/src/mcp-prompts.js +8 -6
  35. package/src/mcp-register-tools.js +25 -14
  36. package/src/mcp-role-tools.js +59 -18
  37. package/src/mcp-server-name.js +7 -0
  38. package/src/mcp-server.js +35 -7
  39. package/src/mcp-signup-client.js +3 -6
  40. package/src/mcp-signup-flow.js +389 -0
  41. package/src/mcp-signup-tools.js +250 -133
  42. package/src/onboarding-flow.js +72 -146
  43. package/src/onboarding-funnel.js +46 -6
  44. package/src/opencode-sessions.js +92 -0
  45. package/src/profile-agents.js +26 -0
  46. package/src/render-report-text.js +1 -1
  47. package/src/report-gating.js +4 -2
  48. package/src/report-store.js +7 -2
  49. package/src/roles-flow.js +8 -5
  50. package/src/saved-positions-flow.js +3 -3
  51. package/src/scanner.js +104 -12
  52. package/src/session-gate.js +1 -4
  53. package/src/session-normalizers.js +114 -8
  54. package/src/session-refresh.js +5 -16
  55. package/src/session-scan.js +167 -22
  56. package/src/share-consent-state.js +2 -2
  57. package/src/share-payload.js +4 -1
  58. package/src/share.js +2 -2
  59. package/src/signup-client.js +12 -39
  60. package/src/signup-copy.js +545 -58
  61. package/src/skill-install.js +102 -0
  62. package/src/stdin-ask.js +13 -1
  63. package/src/tier-engine.js +14 -2
  64. package/src/trust-narration.js +32 -0
  65. package/src/usage-run-steps.js +68 -6
  66. package/src/usage-scan.js +11 -8
  67. package/src/brand-svg.js +0 -9
  68. package/src/inter-fonts.js +0 -6
  69. package/src/mcp-auth-form.js +0 -480
@@ -2,15 +2,19 @@
2
2
 
3
3
  const fs = require('fs');
4
4
  const path = require('path');
5
- const { getHomeDir } = require('./env-paths');
5
+ const { getHomeDir, codexHome, piAgentDir, expandHome } = require('./env-paths');
6
6
  const { scrubSecrets } = require('./agent-synthesis');
7
7
  const {
8
8
  normalizeCodexRecord,
9
9
  normalizeGeminiRecord,
10
10
  normalizeContinueRecord,
11
11
  normalizeClineRecord,
12
+ normalizeClineSessionRecord,
13
+ normalizeCursorRecord,
14
+ normalizePiRecord,
12
15
  parseAiderHistory,
13
16
  } = require('./session-normalizers');
17
+ const { readOpencodeSessions } = require('./opencode-sessions');
14
18
 
15
19
  // GLOBAL, machine-wide inventory of AI coding-tool SESSIONS.
16
20
 
@@ -78,11 +82,130 @@ const FILE_TOOL_NAMES = new Set([
78
82
  'Write',
79
83
  'NotebookEdit',
80
84
  ]);
81
- const PLANNING_TOOL_NAMES = new Set(['ExitPlanMode', 'TodoWrite']);
85
+ const PLANNING_TOOL_NAMES = new Set([
86
+ 'ExitPlanMode',
87
+ 'TodoWrite',
88
+ 'update_plan',
89
+ 'write_todos',
90
+ 'enter_plan_mode',
91
+ 'exit_plan_mode',
92
+ 'tracker_create_task',
93
+ 'tracker_update_task',
94
+ 'update_todo_list',
95
+ 'plan_mode_respond',
96
+ 'todowrite',
97
+ 'todo',
98
+ ]);
82
99
 
83
100
  // Only logs.json + session-*.jsonl under ~/.gemini/tmp are transcripts (skip checkpoints/config).
84
101
  function isGeminiTranscriptFile(name) {
85
- return name === 'logs.json' || (name.startsWith('session-') && name.endsWith('.jsonl'));
102
+ return name === 'logs.json' || (name.startsWith('session-') && (name.endsWith('.jsonl') || name.endsWith('.json')));
103
+ }
104
+
105
+ function isCursorTranscriptFile(name, full) {
106
+ return name.endsWith('.jsonl') && full.split(path.sep).includes('agent-transcripts');
107
+ }
108
+
109
+ function isClineSessionFile(name) {
110
+ return name.endsWith('.messages.json');
111
+ }
112
+
113
+ function readJsonFile(file) {
114
+ try {
115
+ return JSON.parse(fs.readFileSync(file, 'utf8'));
116
+ } catch {
117
+ return null;
118
+ }
119
+ }
120
+
121
+ function geminiContext(geminiDir) {
122
+ let slugs = null;
123
+ return (file) => {
124
+ if (slugs === null) {
125
+ slugs = new Map();
126
+ const registry = readJsonFile(path.join(geminiDir, 'projects.json'));
127
+ const projects = registry && registry.projects && typeof registry.projects === 'object' ? registry.projects : {};
128
+ for (const [dir, slug] of Object.entries(projects)) {
129
+ if (typeof slug === 'string') slugs.set(slug, dir);
130
+ }
131
+ }
132
+ const slug = path.relative(path.join(geminiDir, 'tmp'), file).split(path.sep)[0];
133
+ const cwd = slugs.get(slug);
134
+ return cwd ? { cwd } : null;
135
+ };
136
+ }
137
+
138
+ function slugOf(name) {
139
+ return name.replace(/[^A-Za-z0-9]/g, '-');
140
+ }
141
+
142
+ function resolveSlugPath(slug, dir = path.parse(process.cwd()).root, depth = 0) {
143
+ if (!slug) return dir;
144
+ if (depth > 24) return null;
145
+ let entries;
146
+ try {
147
+ entries = fs.readdirSync(dir, { withFileTypes: true });
148
+ } catch {
149
+ return null;
150
+ }
151
+ for (const e of entries) {
152
+ if (!e.isDirectory()) continue;
153
+ const part = slugOf(e.name);
154
+ if (slug === part) return path.join(dir, e.name);
155
+ if (part && slug.startsWith(`${part}-`)) {
156
+ const hit = resolveSlugPath(slug.slice(part.length + 1), path.join(dir, e.name), depth + 1);
157
+ if (hit) return hit;
158
+ }
159
+ }
160
+ return null;
161
+ }
162
+
163
+ function cursorContext(projectsDir) {
164
+ const cwds = new Map();
165
+ return (file) => {
166
+ const slug = path.relative(projectsDir, file).split(path.sep)[0];
167
+ if (!cwds.has(slug)) cwds.set(slug, resolveSlugPath(slug));
168
+ const out = {};
169
+ const cwd = cwds.get(slug);
170
+ if (cwd) out.cwd = cwd;
171
+ try {
172
+ out.timestamp = fs.statSync(file).mtime.toISOString();
173
+ } catch {
174
+ return cwd ? out : null;
175
+ }
176
+ return out;
177
+ };
178
+ }
179
+
180
+ function clineSessionStem(file) {
181
+ return path.basename(file).slice(0, -'.messages.json'.length);
182
+ }
183
+
184
+ function isClineSubagentFile(file) {
185
+ return clineSessionStem(file) !== path.basename(path.dirname(file));
186
+ }
187
+
188
+ function clineContext(file) {
189
+ if (isClineSubagentFile(file)) return null;
190
+ const manifest = readJsonFile(path.join(path.dirname(file), `${clineSessionStem(file)}.json`));
191
+ if (!manifest || typeof manifest !== 'object') return null;
192
+ const out = {};
193
+ const cwd = manifest.cwd || manifest.workspace_root;
194
+ if (typeof cwd === 'string' && cwd) out.cwd = cwd;
195
+ if (typeof manifest.started_at === 'string') out.timestamp = manifest.started_at;
196
+ if (manifest.metadata && manifest.metadata.mode === 'plan') out.planMode = true;
197
+ return Object.keys(out).length ? out : null;
198
+ }
199
+
200
+ function clineSessionsDir(env, home) {
201
+ if (env.CLINE_SESSION_DATA_DIR) return env.CLINE_SESSION_DATA_DIR;
202
+ const dataDir = env.CLINE_DATA_DIR || path.join(env.CLINE_DIR || path.join(home, '.cline'), 'data');
203
+ return path.join(dataDir, 'sessions');
204
+ }
205
+
206
+ function piSessionsDir(env) {
207
+ if (env.PI_CODING_AGENT_SESSION_DIR) return expandHome(env.PI_CODING_AGENT_SESSION_DIR, env);
208
+ return path.join(piAgentDir(env), 'sessions');
86
209
  }
87
210
 
88
211
  // Cline / Roo Code conversation file (both forks use this filename under tasks/<id>/).
@@ -99,14 +222,18 @@ function vscodeGlobalStorageDir(home) {
99
222
 
100
223
  function toolSources(env = process.env) {
101
224
  const home = getHomeDir(env);
225
+ const codex = codexHome(env);
226
+ const gemini = path.join(home, '.gemini');
227
+ const cursorProjects = path.join(home, '.cursor', 'projects');
102
228
  return [
103
- { tool: 'claude', dir: path.join(home, '.claude', 'projects') },
104
- { tool: 'codex', dir: path.join(home, '.codex', 'sessions'), normalize: normalizeCodexRecord },
105
- { tool: 'gemini', dir: path.join(home, '.gemini', 'tmp'), fileFilter: isGeminiTranscriptFile, normalize: normalizeGeminiRecord },
106
- // Kept by decision: ~/.cursor/chats is OBSOLETE (real Cursor chat is SQLite in App Support); awaiting a future SQLite extractor, no tool removed.
107
- { tool: 'cursor', dir: path.join(home, '.cursor', 'chats') },
108
- { tool: 'continue', dir: path.join(home, '.continue', 'sessions'), normalize: normalizeContinueRecord },
109
- { tool: 'cline', dir: vscodeGlobalStorageDir(home), fileFilter: isClineHistoryFile, normalize: normalizeClineRecord },
229
+ { tool: 'claude', dirs: [path.join(home, '.claude', 'projects')] },
230
+ { tool: 'codex', dirs: [path.join(codex, 'sessions'), path.join(codex, 'archived_sessions')], normalize: normalizeCodexRecord },
231
+ { tool: 'gemini', dirs: [path.join(gemini, 'tmp')], fileFilter: isGeminiTranscriptFile, normalize: normalizeGeminiRecord, context: geminiContext(gemini) },
232
+ { tool: 'cursor', dirs: [cursorProjects], fileFilter: isCursorTranscriptFile, normalize: normalizeCursorRecord, context: cursorContext(cursorProjects) },
233
+ { tool: 'continue', dirs: [path.join(home, '.continue', 'sessions')], normalize: normalizeContinueRecord },
234
+ { tool: 'cline', dirs: [vscodeGlobalStorageDir(home), clineSessionsDir(env, home)], fileFilter: (name) => isClineHistoryFile(name) || isClineSessionFile(name), normalize: (r) => normalizeClineSessionRecord(r) || normalizeClineRecord(r), context: (file) => (isClineSessionFile(path.basename(file)) ? clineContext(file) : null), subagent: (file) => isSubagentFile(file) || (isClineSessionFile(path.basename(file)) && isClineSubagentFile(file)) },
235
+ { tool: 'pi', dirs: [piSessionsDir(env)], normalize: normalizePiRecord },
236
+ { tool: 'opencode', sessions: () => readOpencodeSessions(env, MAX_FILES_PER_TOOL) },
110
237
  // Repo-level: .aider.chat.history.md lives INSIDE each git repo, so cwd = the repo toplevel.
111
238
  { tool: 'aider', repoLevel: true, file: '.aider.chat.history.md', parseText: parseAiderHistory },
112
239
  ];
@@ -127,7 +254,7 @@ function listSessionFiles(root, fileFilter = isTranscriptFile) {
127
254
  const full = path.join(dir, entry.name);
128
255
  if (entry.isDirectory()) {
129
256
  stack.push(full);
130
- } else if (entry.isFile() && fileFilter(entry.name)) {
257
+ } else if (entry.isFile() && fileFilter(entry.name, full)) {
131
258
  out.push(full);
132
259
  if (out.length >= MAX_FILES_PER_TOOL) return out;
133
260
  }
@@ -169,7 +296,8 @@ function parseRawRecords(file) {
169
296
  function readSourceObjects(source, file) {
170
297
  const raw = parseRawRecords(file);
171
298
  if (typeof source.normalize !== 'function') return raw;
172
- const out = [];
299
+ const context = typeof source.context === 'function' ? source.context(file) : null;
300
+ const out = context ? [context] : [];
173
301
  for (const r of raw) {
174
302
  const o = source.normalize(r);
175
303
  if (Array.isArray(o)) out.push(...o);
@@ -178,6 +306,26 @@ function readSourceObjects(source, file) {
178
306
  return out;
179
307
  }
180
308
 
309
+ function listSourceFiles(source) {
310
+ const out = [];
311
+ for (const dir of source.dirs) {
312
+ out.push(...listSessionFiles(dir, source.fileFilter || isTranscriptFile));
313
+ if (out.length >= MAX_FILES_PER_TOOL) return out.slice(0, MAX_FILES_PER_TOOL);
314
+ }
315
+ return out;
316
+ }
317
+
318
+ function* sourceSessions(source) {
319
+ if (typeof source.sessions === 'function') {
320
+ yield* source.sessions();
321
+ return;
322
+ }
323
+ const subagent = source.subagent || isSubagentFile;
324
+ for (const file of listSourceFiles(source)) {
325
+ yield { file, objects: readSourceObjects(source, file), subagent: subagent(file) };
326
+ }
327
+ }
328
+
181
329
  // Bounded whole-file text read (for repo-level markdown sources, e.g. Aider).
182
330
  function readTextFile(file) {
183
331
  let fd;
@@ -304,9 +452,7 @@ function* iterateSessionObjects(env = process.env) {
304
452
  const seenCwds = new Set();
305
453
  for (const source of sources) {
306
454
  if (source.repoLevel) continue;
307
- const files = listSessionFiles(source.dir, source.fileFilter || isTranscriptFile);
308
- for (const file of files) {
309
- const objects = readSourceObjects(source, file);
455
+ for (const { file, objects } of sourceSessions(source)) {
310
456
  const cwd = resolveFileCwd(objects);
311
457
  if (cwd) seenCwds.add(cwd);
312
458
  for (const obj of objects) {
@@ -360,9 +506,10 @@ function makeSessionsAccumulator() {
360
506
  },
361
507
  };
362
508
  },
363
- addFile(file, objects) {
509
+ addFile(file, objects, subagent = isSubagentFile(file)) {
364
510
  for (const o of objects) harvestObject(o, cur.acc);
365
- if (isSubagentFile(file)) {
511
+ if (objects.some((o) => o && o.planMode === true)) cur.acc.planningSignalCount += 1;
512
+ if (subagent) {
366
513
  cur.subagentRuns += 1;
367
514
  } else {
368
515
  cur.rootSessions += 1;
@@ -424,15 +571,12 @@ function walkSessionSignals(env = process.env, selectedCwds = null, onHumanObjec
424
571
  // Phase 1: home-dir sources; remember the repo toplevels (cwds) they reveal.
425
572
  for (const source of sources) {
426
573
  if (source.repoLevel) continue;
427
- const files = listSessionFiles(source.dir, source.fileFilter || isTranscriptFile);
428
- if (files.length === 0) continue;
429
574
  sessions.startTool(source.tool);
430
- for (const f of files) {
431
- const objects = readSourceObjects(source, f);
575
+ for (const { file, objects, subagent } of sourceSessions(source)) {
432
576
  const cwd = resolveFileCwd(objects);
433
577
  if (cwd) seenCwds.add(cwd);
434
578
  if (!fileInScope(cwd, selectedCwds)) continue;
435
- sessions.addFile(f, objects);
579
+ sessions.addFile(file, objects, subagent);
436
580
  if (onHumanObject) {
437
581
  for (const o of objects) onHumanObject(o);
438
582
  }
@@ -468,6 +612,7 @@ module.exports = {
468
612
  makeSessionsAccumulator,
469
613
  walkSessionSignals,
470
614
  toolSources,
615
+ sourceSessions,
471
616
  listSessionFiles,
472
617
  readSessionObjects,
473
618
  resolveFileCwd,
@@ -19,9 +19,9 @@ const SEND_THROTTLE_MS = 60 * 60 * 1000;
19
19
 
20
20
  // consent state (~/.config/ai-footprint/consent.json) Replaces the old `credentials.json` (token model).
21
21
 
22
- function loadConsentState() {
22
+ function loadConsentState(env = process.env) {
23
23
  try {
24
- return JSON.parse(fs.readFileSync(consentPath(), 'utf8'));
24
+ return JSON.parse(fs.readFileSync(consentPath(env), 'utf8'));
25
25
  } catch {
26
26
  return null;
27
27
  }
@@ -48,7 +48,9 @@ function agentDefinitionsByName(report) {
48
48
  return byName;
49
49
  }
50
50
 
51
- function derivePayload(report, maturity) {
51
+ const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
52
+
53
+ function derivePayload(report, maturity, { cliCommandId = null } = {}) {
52
54
  const agents = Array.isArray(report.agents) ? report.agents : [];
53
55
  const catalogIdsByName = agentCatalogIdsByName(report);
54
56
  const definitionsByName = agentDefinitionsByName(report);
@@ -141,6 +143,7 @@ function derivePayload(report, maturity) {
141
143
  decisions: whitelistDecisions(report.decisions),
142
144
  // PER-CANDIDATE authorship + depth (per detected skill, per detected agent).
143
145
  detectionEvidence: whitelistDetectionEvidence(report.detectionEvidence),
146
+ ...(typeof cliCommandId === 'string' && UUID_RE.test(cliCommandId) ? { cliCommandId } : {}),
144
147
  };
145
148
  }
146
149
 
package/src/share.js CHANGED
@@ -43,7 +43,7 @@ async function requestJsonBackend(endpoint, body, { timeoutMs = 15000, idempoten
43
43
 
44
44
  /* ---------- automatic sending ---------- */
45
45
 
46
- async function autoShare(report, maturity, { root, bypassThrottle = false } = {}) {
46
+ async function autoShare(report, maturity, { root, bypassThrottle = false, cliCommandId = null } = {}) {
47
47
  const state = loadConsentState();
48
48
  const decision = getConsentDecision(state);
49
49
 
@@ -67,7 +67,7 @@ async function autoShare(report, maturity, { root, bypassThrottle = false } = {}
67
67
  return { ok: false, skipped: true, reason: 'no-endpoint-configured' };
68
68
  }
69
69
 
70
- const payload = derivePayload(report, maturity);
70
+ const payload = derivePayload(report, maturity, { cliCommandId });
71
71
 
72
72
  const idempotencyKey = crypto.randomUUID();
73
73
  let res;
@@ -1,11 +1,8 @@
1
1
  'use strict';
2
2
 
3
- const { postJsonWithTimeout, extractSetCookie } = require('./backend-request');
4
- const { SESSION_COOKIE_NAME } = require('./auth-client');
3
+ const { postJsonWithTimeout } = require('./backend-request');
5
4
  const { toUpperLang } = require('./lang-codes');
6
5
 
7
- // Register a talent via the Hub's better-auth email engine, in two steps (WEB-login parity): 1.
8
-
9
6
  const DEFAULT_TIMEOUT_MS = 30000;
10
7
 
11
8
  function parseJson(raw) {
@@ -29,6 +26,12 @@ function buildRegistrationContext({ preferredLanguage, newsletterConsent, freela
29
26
  return context;
30
27
  }
31
28
 
29
+ const TERMS_VERSION = '2026-10-22';
30
+
31
+ function buildRegistrationConsent({ newsletterConsent } = {}) {
32
+ return { termsAccepted: true, termsVersion: TERMS_VERSION, newsletterConsent: newsletterConsent === true };
33
+ }
34
+
32
35
  // Hub refuses these claims with a coded error and creates nothing, so registering without the code is safe.
33
36
  const UNCLAIMABLE_CODES = new Set(['auth-engine.claim_code_invalid', 'users.unregistered_talent_not_claimable']);
34
37
 
@@ -53,13 +56,14 @@ async function postCompleteRegistration(body, { endpoint, headers, timeoutMs })
53
56
  // 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
57
  async function requestCompleteRegistration(
55
58
  context = {},
56
- { endpoint, cookie = null, bearerToken = null, claimCode = null, timeoutMs = DEFAULT_TIMEOUT_MS } = {},
59
+ { endpoint, bearerToken = null, claimCode = null, consent = null, timeoutMs = DEFAULT_TIMEOUT_MS } = {},
57
60
  ) {
58
61
  if (!endpoint) return { ok: false, reason: 'no-endpoint' };
59
62
  const headers = {};
60
- if (cookie) headers.Cookie = cookie;
61
63
  if (bearerToken) headers.Authorization = `Bearer ${bearerToken}`;
62
- const base = { userType: 'WORKS_TALENT', context };
64
+ const base = consent && consent.termsAccepted === true
65
+ ? { userType: 'WORKS_TALENT', context, consent }
66
+ : { userType: 'WORKS_TALENT', context };
63
67
  if (typeof claimCode !== 'string' || !claimCode) return postCompleteRegistration(base, { endpoint, headers, timeoutMs });
64
68
 
65
69
  const claimed = await postCompleteRegistration({ ...base, claimCode }, { endpoint, headers, timeoutMs });
@@ -69,35 +73,4 @@ async function requestCompleteRegistration(
69
73
  return plain.ok ? { ok: true, claimed: false, claimError: claimed.code } : plain;
70
74
  }
71
75
 
72
- async function requestSignUp(
73
- { name, lastName, email, password, preferredLanguage, newsletterConsent, freelanceType, freelanceIntent, claimCode = null } = {},
74
- { signUpEndpoint, completeRegistrationEndpoint, timeoutMs = DEFAULT_TIMEOUT_MS } = {},
75
- ) {
76
- if (!signUpEndpoint) return { ok: false, reason: 'no-endpoint' };
77
- if (!name || !lastName || !email || !password) return { ok: false, reason: 'missing-fields' };
78
-
79
- let signUp;
80
- try {
81
- signUp = await postJsonWithTimeout(signUpEndpoint, { email, password, name, lastName }, timeoutMs);
82
- } catch (e) {
83
- return { ok: false, reason: (e && e.kind) || 'network-error' };
84
- }
85
-
86
- // A duplicate email is a normal outcome the caller handles by switching to login.
87
- if (signUp.status === 409 || signUp.status === 422) return { ok: true, accountExists: true };
88
- if (signUp.status < 200 || signUp.status >= 300) {
89
- return { ok: false, reason: `http-${signUp.status}`, status: signUp.status, message: messageFrom(signUp.raw) };
90
- }
91
-
92
- const cookie = extractSetCookie(signUp.headers, SESSION_COOKIE_NAME);
93
- if (!cookie) return { ok: false, reason: 'bad-response' };
94
-
95
- if (!completeRegistrationEndpoint) return { ok: false, reason: 'no-endpoint' };
96
-
97
- const context = buildRegistrationContext({ preferredLanguage, newsletterConsent, freelanceType, freelanceIntent });
98
- const complete = await requestCompleteRegistration(context, { endpoint: completeRegistrationEndpoint, cookie, claimCode, timeoutMs });
99
- if (!complete.ok) return complete;
100
- return claimCode ? { ok: true, accountExists: false, claimed: complete.claimed === true } : { ok: true, accountExists: false };
101
- }
102
-
103
- module.exports = { requestSignUp, requestCompleteRegistration, buildRegistrationContext };
76
+ module.exports = { requestCompleteRegistration, buildRegistrationContext, buildRegistrationConsent, TERMS_VERSION };