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.
- package/README.md +21 -6
- package/bin/ai-usage.js +31 -12
- package/bin/command-audit-worker.js +19 -0
- package/bin/login.js +4 -8
- package/bin/mcp.js +32 -21
- package/bin/register.js +12 -30
- package/bin/shakers-cli.js +15 -10
- package/package.json +1 -1
- package/src/account-marker.js +40 -0
- package/src/account-trace.js +23 -0
- package/src/agents-client.js +1 -0
- package/src/ai-usage-review.js +103 -0
- package/src/auth-client.js +7 -107
- package/src/auth-session-store.js +12 -11
- package/src/backend-request.js +3 -24
- package/src/cli-skill.js +77 -0
- package/src/command-audit.js +47 -4
- package/src/config.js +2 -16
- package/src/consent-flow.js +9 -3
- package/src/detect-repos.js +4 -8
- package/src/detectors.js +23 -0
- package/src/device-auth-store.js +2 -2
- package/src/device-login.js +11 -9
- package/src/env-paths.js +9 -1
- package/src/i18n.js +186 -102
- package/src/mcp-ai-usage-result-tool.js +38 -14
- package/src/mcp-ai-usage-tool.js +317 -172
- package/src/mcp-choice.js +171 -34
- package/src/mcp-detector.js +26 -2
- package/src/mcp-device-login-tools.js +40 -8
- package/src/mcp-install.js +292 -53
- package/src/mcp-instructions.js +43 -17
- package/src/mcp-onboarding-tools.js +1 -3
- package/src/mcp-prompts.js +8 -6
- package/src/mcp-register-tools.js +25 -14
- package/src/mcp-role-tools.js +59 -18
- package/src/mcp-server-name.js +7 -0
- package/src/mcp-server.js +35 -7
- package/src/mcp-signup-client.js +3 -6
- package/src/mcp-signup-flow.js +389 -0
- package/src/mcp-signup-tools.js +250 -133
- package/src/onboarding-flow.js +72 -146
- package/src/onboarding-funnel.js +46 -6
- package/src/opencode-sessions.js +92 -0
- package/src/profile-agents.js +26 -0
- package/src/render-report-text.js +1 -1
- package/src/report-gating.js +4 -2
- package/src/report-store.js +7 -2
- package/src/roles-flow.js +8 -5
- package/src/saved-positions-flow.js +3 -3
- package/src/scanner.js +104 -12
- package/src/session-gate.js +1 -4
- package/src/session-normalizers.js +114 -8
- package/src/session-refresh.js +5 -16
- package/src/session-scan.js +167 -22
- package/src/share-consent-state.js +2 -2
- package/src/share-payload.js +4 -1
- package/src/share.js +2 -2
- package/src/signup-client.js +12 -39
- package/src/signup-copy.js +545 -58
- package/src/skill-install.js +102 -0
- package/src/stdin-ask.js +13 -1
- package/src/tier-engine.js +14 -2
- package/src/trust-narration.js +32 -0
- package/src/usage-run-steps.js +68 -6
- package/src/usage-scan.js +11 -8
- package/src/brand-svg.js +0 -9
- package/src/inter-fonts.js +0 -6
- package/src/mcp-auth-form.js +0 -480
package/src/session-scan.js
CHANGED
|
@@ -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([
|
|
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',
|
|
104
|
-
{ tool: 'codex',
|
|
105
|
-
{ tool: 'gemini',
|
|
106
|
-
|
|
107
|
-
{ tool: '
|
|
108
|
-
{ tool: '
|
|
109
|
-
{ tool: '
|
|
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
|
|
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
|
|
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 (
|
|
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
|
|
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(
|
|
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
|
}
|
package/src/share-payload.js
CHANGED
|
@@ -48,7 +48,9 @@ function agentDefinitionsByName(report) {
|
|
|
48
48
|
return byName;
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
-
|
|
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;
|
package/src/signup-client.js
CHANGED
|
@@ -1,11 +1,8 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
const { postJsonWithTimeout
|
|
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,
|
|
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 =
|
|
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
|
-
|
|
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 };
|