shakers-cli 0.14.2 → 0.16.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 +51 -1203
- package/bin/ai-usage.js +11 -56
- package/bin/login.js +10 -10
- package/bin/mcp.js +13 -9
- package/bin/mcpb.js +1 -1
- package/bin/onboarding.js +3 -2
- package/bin/register.js +17 -4
- package/bin/report-html.js +2 -2
- package/bin/shakers-cli.js +352 -0
- package/bin/shakers.js +20 -344
- package/bin/share.js +1 -1
- package/bin/superadmin.js +5 -5
- package/bin/version.js +3 -0
- package/package.json +12 -3
- package/src/agent-category.js +1 -1
- package/src/agent-evaluation.js +3 -3
- package/src/agent-org-chart.js +5 -5
- package/src/agent-synthesis.js +3 -3
- package/src/agent-usage.js +1 -1
- package/src/agents-client.js +4 -4
- package/src/ai.js +1 -1
- package/src/ansi.js +1 -1
- package/src/auth-client.js +18 -4
- package/src/auth-method-picker.js +1 -1
- package/src/auth-methods.js +1 -1
- package/src/auth-session-store.js +11 -8
- package/src/authorship-mismatch.js +1 -1
- package/src/authorship.js +2 -2
- package/src/automations-detector.js +1 -1
- package/src/backend-request.js +20 -3
- package/src/brand-ansi.js +1 -1
- package/src/browser-tools-detector.js +1 -1
- package/src/certify-disclaimer.js +1 -1
- package/src/certify-remediation-prompt.js +1 -1
- package/src/cli-args.js +2 -2
- package/src/command-audit.js +8 -5
- package/src/config-dir.js +1 -1
- package/src/config.js +112 -48
- package/src/consent-flow.js +4 -4
- package/src/consent-skip.js +1 -1
- package/src/detection-evidence.js +2 -2
- package/src/detection-noise.js +1 -1
- package/src/detectors.js +1 -1
- package/src/device-login.js +12 -1
- package/src/email-verification.js +3 -3
- package/src/env-paths.js +1 -1
- package/src/find-projects-client.js +12 -0
- package/src/find-projects-flow.js +1 -13
- package/src/git-activity.js +1 -1
- package/src/graph-certs.js +3 -3
- package/src/graph-scan.js +9 -9
- package/src/i18n.js +124 -124
- package/src/interactive-select.js +1 -1
- package/src/interview-client.js +6 -1
- package/src/interview-transcript.js +1 -1
- package/src/legal-notice.js +1 -1
- package/src/maturity.js +6 -6
- package/src/mcp-ai-usage-tool.js +1 -1
- package/src/mcp-auth-form.js +1 -1
- package/src/mcp-detector.js +3 -3
- package/src/mcp-device-login-tools.js +2 -1
- package/src/mcp-find-projects-tools.js +1 -2
- package/src/mcp-onboarding-tools.js +4 -4
- package/src/mcp-prompts.js +2 -2
- package/src/mcp-register-tools.js +1 -0
- package/src/mcp-server.js +5 -1
- package/src/mcp-signup-client.js +2 -2
- package/src/mcp-signup-tools.js +26 -2
- package/src/memory-structure-detector.js +1 -1
- package/src/model-call-record.js +2 -2
- package/src/onboarding-flow.js +16 -16
- package/src/onboarding-funnel.js +110 -17
- package/src/open-file.js +1 -1
- package/src/osc-link.js +1 -1
- package/src/portfolio-client.js +11 -1
- package/src/portfolio-git-info.js +1 -1
- package/src/profile-flow.js +1 -1
- package/src/profile-switch.js +5 -12
- package/src/render-certification.js +10 -10
- package/src/render-html.js +6 -6
- package/src/render-sheet.js +10 -10
- package/src/render-terminal.js +6 -6
- package/src/repo-scope.js +132 -0
- package/src/report-gating.js +1 -1
- package/src/report-store.js +6 -6
- package/src/roadmap-content.js +5 -5
- package/src/roadmap-personalization.js +3 -3
- package/src/roadmap-prompt.js +2 -2
- package/src/scan-exclusions.js +1 -1
- package/src/scanner.js +14 -14
- package/src/secret-prompt.js +1 -1
- package/src/session-gate.js +1 -1
- package/src/session-refresh.js +33 -3
- package/src/session-scan.js +1 -1
- package/src/shakers-wordmark.js +1 -1
- package/src/share-card.js +2 -2
- package/src/share-certification.js +4 -4
- package/src/share-consent-state.js +7 -7
- package/src/share-payload.js +7 -7
- package/src/share.js +7 -7
- package/src/signup-client.js +1 -1
- package/src/signup-copy.js +1 -1
- package/src/skills-client.js +1 -1
- package/src/start-add-agents.js +2 -2
- package/src/start-add-portfolio.js +3 -13
- package/src/start-add-skills.js +2 -2
- package/src/stdin-ask.js +3 -3
- package/src/strip-internal-comments.js +1 -1
- package/src/tech-detector.js +2 -2
- package/src/templates/report-sheet.html +11 -13
- package/src/terminal-agents.js +8 -8
- package/src/terminal-format.js +3 -3
- package/src/terminal-sections.js +11 -11
- package/src/tier-engine.js +3 -3
- package/src/tier-projection.js +2 -2
- package/src/transcript-turns.js +1 -1
- package/src/typewriter.js +1 -1
- package/src/usage-model-calls.js +6 -6
- package/src/usage-oneshot-commands.js +5 -5
- package/src/usage-repo-scope.js +11 -129
- package/src/usage-run-steps.js +32 -184
- package/src/usage-scan.js +235 -0
- package/src/work-streams.js +1 -1
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
const { palette, styleQuestion } = require('./ansi');
|
|
4
4
|
const { BRAND_ANSI } = require('./brand-ansi');
|
|
5
5
|
|
|
6
|
-
// Zero-dependency interactive multi-select
|
|
6
|
+
// Zero-dependency interactive multi-select.
|
|
7
7
|
|
|
8
8
|
// COLOUR goes through the one gate (src/ansi.js).
|
|
9
9
|
const ANSI = palette({
|
package/src/interview-client.js
CHANGED
|
@@ -100,7 +100,7 @@ class InterviewClient {
|
|
|
100
100
|
|
|
101
101
|
// The end of the interview is a protocol message, not a sentence: matching the
|
|
102
102
|
// goodbye text never fired (certs sends no closingMessage), so the CLI kept
|
|
103
|
-
// asking for answers until its turn-timeout
|
|
103
|
+
// asking for answers until its turn-timeout.
|
|
104
104
|
if (RoomEvent && RoomEvent.DataReceived && typeof room.on === 'function') {
|
|
105
105
|
room.on(RoomEvent.DataReceived, (payload, _participant, _kind, topic) => {
|
|
106
106
|
if (topic !== PROTOCOL_TOPIC) return;
|
|
@@ -216,8 +216,13 @@ class InterviewClient {
|
|
|
216
216
|
}
|
|
217
217
|
}
|
|
218
218
|
|
|
219
|
+
function createInterviewClient(opts) {
|
|
220
|
+
return new InterviewClient(opts);
|
|
221
|
+
}
|
|
222
|
+
|
|
219
223
|
module.exports = {
|
|
220
224
|
InterviewClient,
|
|
225
|
+
createInterviewClient,
|
|
221
226
|
LiveKitNotInstalledError,
|
|
222
227
|
loadLiveKitSdk,
|
|
223
228
|
CHAT_TOPIC,
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
// Upper case: certs validates the role against its AGENT/USER enum and 400s the
|
|
6
6
|
// whole PATCH otherwise, so every terminal onboarding lost its transcript
|
|
7
|
-
// ("no se pudo guardar la transcripcion (http-400)"
|
|
7
|
+
// ("no se pudo guardar la transcripcion (http-400)").
|
|
8
8
|
const ROLE_AGENT = 'AGENT';
|
|
9
9
|
const ROLE_USER = 'USER';
|
|
10
10
|
|
package/src/legal-notice.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
// Shared PRESENTATION for the CLI's legal/consent disclosures
|
|
3
|
+
// Shared PRESENTATION for the CLI's legal/consent disclosures.
|
|
4
4
|
|
|
5
5
|
const { palette, colorEnabled } = require('./ansi');
|
|
6
6
|
const { BRAND_ANSI } = require('./brand-ansi');
|
package/src/maturity.js
CHANGED
|
@@ -12,7 +12,7 @@ const LEVELS = [
|
|
|
12
12
|
{ level: 4, key: 'orchestrator', name: 'Orquestador', emoji: '●' },
|
|
13
13
|
];
|
|
14
14
|
|
|
15
|
-
// DECISION
|
|
15
|
+
// DECISION: the level definitions (LEVELS) have NOT been touched.
|
|
16
16
|
function depthTotals(tools) {
|
|
17
17
|
let instructions = 0; // project instructions/rules files
|
|
18
18
|
let mcp = 0; // configured MCP servers
|
|
@@ -30,7 +30,7 @@ function depthTotals(tools) {
|
|
|
30
30
|
return { instructions, mcp, custom, hooks };
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
-
// Score model 0-100 for the visual meter — RECALIBRATED
|
|
33
|
+
// Score model 0-100 for the visual meter — RECALIBRATED.
|
|
34
34
|
const SCORE_MODEL = [
|
|
35
35
|
{ key: 'breadth', weight: 12, full: 3 }, // number of AI tools detected
|
|
36
36
|
{ key: 'context', weight: 16, full: 2 }, // instructions + config files
|
|
@@ -61,7 +61,7 @@ function classify(report) {
|
|
|
61
61
|
? report.agentCounts.agents
|
|
62
62
|
: 0;
|
|
63
63
|
|
|
64
|
-
// Score scope reverted to PROJECT ∪ HOME
|
|
64
|
+
// Score scope reverted to PROJECT ∪ HOME.
|
|
65
65
|
const score = computeScore({
|
|
66
66
|
breadth,
|
|
67
67
|
context: d.instructions, // depthTotals already folds `config` into instructions
|
|
@@ -72,7 +72,7 @@ function classify(report) {
|
|
|
72
72
|
multiAgent: agentCount,
|
|
73
73
|
});
|
|
74
74
|
|
|
75
|
-
// Band 0-4 derived from the tier engine (
|
|
75
|
+
// Band 0-4 derived from the tier engine (single source of truth).
|
|
76
76
|
const tierResult = computeTierResult(report);
|
|
77
77
|
const level = tierResult.band;
|
|
78
78
|
const meta = LEVELS[level];
|
|
@@ -87,11 +87,11 @@ function classify(report) {
|
|
|
87
87
|
depth: d,
|
|
88
88
|
hasAgentic,
|
|
89
89
|
next: nextStep(level, { breadth, ...d, hasAgentic }),
|
|
90
|
-
// Tier (T0-T7
|
|
90
|
+
// Tier (T0-T7): the fine-grained axis the band is derived from.
|
|
91
91
|
tier: tierResult.tier,
|
|
92
92
|
tierKey: tierResult.tierKey,
|
|
93
93
|
tierName: tierResult.tierName,
|
|
94
|
-
// Setup Level
|
|
94
|
+
// Setup Level: the 3-value rollup shown to the talent, derived
|
|
95
95
|
// from the tier. `{key, code, rank, emoji}`; label localized via i18n.
|
|
96
96
|
setupLevel: tierResult.setupLevel,
|
|
97
97
|
};
|
package/src/mcp-ai-usage-tool.js
CHANGED
|
@@ -77,7 +77,7 @@ function buildArgv({ root, repoScope, lang }) {
|
|
|
77
77
|
|
|
78
78
|
function makeAiUsageTool(deps = {}) {
|
|
79
79
|
const {
|
|
80
|
-
scanUsage = (argv) => require('
|
|
80
|
+
scanUsage = (argv) => require('./usage-scan').scanUsage(argv),
|
|
81
81
|
autoShare = require('./share').autoShare,
|
|
82
82
|
recordConsent = require('./share').recordConsent,
|
|
83
83
|
isValidEmail = require('./share').isValidEmail,
|
package/src/mcp-auth-form.js
CHANGED
|
@@ -244,7 +244,7 @@ const LINKEDIN_IN = '<svg class="g" viewBox="0 0 24 24" aria-hidden="true">'
|
|
|
244
244
|
// Each window offers these methods, in this order. The sign-up window has no LinkedIn: only Google carries the claim code there.
|
|
245
245
|
const WINDOW_METHODS = { login: ['google', 'linkedin', 'email'], register: ['google', 'email'] };
|
|
246
246
|
|
|
247
|
-
// The method-choice screen
|
|
247
|
+
// The method-choice screen — the FIRST screen of the loopback form when more than one method is offered for this flow.
|
|
248
248
|
function methodChoiceHtml(mode, lang, methods) {
|
|
249
249
|
const c = copy(lang);
|
|
250
250
|
const title = mode === 'register' ? c.chooseTitleRegister : c.chooseTitleLogin;
|
package/src/mcp-detector.js
CHANGED
|
@@ -4,7 +4,7 @@ const fs = require('fs');
|
|
|
4
4
|
const path = require('path');
|
|
5
5
|
const { getHomeDir } = require('./env-paths');
|
|
6
6
|
|
|
7
|
-
// KEYWORD -> { SERVICE LABEL, CATEGORY }
|
|
7
|
+
// KEYWORD -> { SERVICE LABEL, CATEGORY }.
|
|
8
8
|
const SERVICE_CATALOG = [
|
|
9
9
|
// --- data ---
|
|
10
10
|
{ kw: 'postgresql', label: 'Postgres', category: 'data' },
|
|
@@ -87,7 +87,7 @@ function serviceForMcpServerName(name) {
|
|
|
87
87
|
return hit && hit.label ? hit.label : null;
|
|
88
88
|
}
|
|
89
89
|
|
|
90
|
-
// THE single inventory of MCP config locations
|
|
90
|
+
// THE single inventory of MCP config locations.
|
|
91
91
|
const MCP_CONFIG_LOCATIONS = [
|
|
92
92
|
// --- Claude Code (already read before 112) ---
|
|
93
93
|
{ tool: 'claude-code', scope: 'project', keys: ['mcpServers'], confidence: 'high', file: (root) => path.join(root, '.mcp.json') },
|
|
@@ -181,7 +181,7 @@ function detectMcpServers(root) {
|
|
|
181
181
|
// 'project' wins when a server is declared in both: the most specific
|
|
182
182
|
// scope is the honest label for "this repo uses it".
|
|
183
183
|
scope: entry.scopes.has('project') ? 'project' : 'home',
|
|
184
|
-
//
|
|
184
|
+
// The PRODUCT, derived from the name only. `null` when the
|
|
185
185
|
// name carries none — a state, not a hole.
|
|
186
186
|
service: serviceForMcpServerName(name),
|
|
187
187
|
};
|
|
@@ -24,6 +24,7 @@ function makeDeviceLoginTools(deps = {}) {
|
|
|
24
24
|
pollDeviceToken = require('./device-login').pollDeviceToken,
|
|
25
25
|
finalizeDeviceSession = require('./device-login').finalizeDeviceSession,
|
|
26
26
|
appendProviderParam = require('./device-login').appendProviderParam,
|
|
27
|
+
deviceSessionRecord = require('./device-login').deviceSessionRecord,
|
|
27
28
|
savePendingDevice = require('./device-auth-store').savePendingDevice,
|
|
28
29
|
loadPendingDevice = require('./device-auth-store').loadPendingDevice,
|
|
29
30
|
clearPendingDevice = require('./device-auth-store').clearPendingDevice,
|
|
@@ -144,7 +145,7 @@ function makeDeviceLoginTools(deps = {}) {
|
|
|
144
145
|
return { ok: false, status: 'error', reason: fin.reason };
|
|
145
146
|
}
|
|
146
147
|
try {
|
|
147
|
-
saveAuthSession({
|
|
148
|
+
saveAuthSession(deviceSessionRecord({ token: fin.token, sessionToken: res.accessToken, sessionExpiresIn: res.expiresIn, email: res.email || pending.email || null }));
|
|
148
149
|
} catch {
|
|
149
150
|
clearPendingDevice();
|
|
150
151
|
return { ok: false, status: 'error', reason: 'session-persist-failed' };
|
|
@@ -3,8 +3,7 @@
|
|
|
3
3
|
// MCP: list the Positions/Projects available to the talent (Find Positions),
|
|
4
4
|
// paginated + filtered server-side. Same params as the CLI so an agent can page.
|
|
5
5
|
|
|
6
|
-
const { TABS, ATTENDANCE, DEFAULT_LIMIT } = require('./find-projects-client');
|
|
7
|
-
const { splitRecommended } = require('./find-projects-flow');
|
|
6
|
+
const { TABS, ATTENDANCE, DEFAULT_LIMIT, splitRecommended } = require('./find-projects-client');
|
|
8
7
|
|
|
9
8
|
const FIND_PROJECTS_SCHEMA = {
|
|
10
9
|
type: 'object',
|
|
@@ -76,6 +76,7 @@ function makeOnboardingTools(deps = {}) {
|
|
|
76
76
|
requestDiscoveredInventory = require('./inventory-client').requestDiscoveredInventory,
|
|
77
77
|
requestDiscoveredInventoryWithRefresh = require('./inventory-client').requestDiscoveredInventoryWithRefresh,
|
|
78
78
|
ensureFreshSession = require('./session-refresh').ensureFreshSession,
|
|
79
|
+
endSession = require('./session-refresh').endSession,
|
|
79
80
|
requestDeclareSkill = require('./skills-client').requestDeclareSkill,
|
|
80
81
|
requestDeclareAgent = require('./agents-client').requestDeclareAgent,
|
|
81
82
|
requestListAgents = require('./agents-client').requestListAgents,
|
|
@@ -86,7 +87,7 @@ function makeOnboardingTools(deps = {}) {
|
|
|
86
87
|
getRemoteUrl = require('./portfolio-git-info').getRemoteUrl,
|
|
87
88
|
getCommitDateRange = require('./portfolio-git-info').getCommitDateRange,
|
|
88
89
|
consentCopy = () => require('./signup-copy').signupCopy(require('./signup-language').signupLanguage(require('./i18n').detectFlowLang())).consent,
|
|
89
|
-
normalizeDomain = require('./
|
|
90
|
+
normalizeDomain = require('./portfolio-client').normalizeDomain,
|
|
90
91
|
isValidEmail = require('./share').isValidEmail,
|
|
91
92
|
normalizeEmail = require('./share').normalizeEmail,
|
|
92
93
|
} = deps;
|
|
@@ -115,8 +116,7 @@ function makeOnboardingTools(deps = {}) {
|
|
|
115
116
|
};
|
|
116
117
|
|
|
117
118
|
async function logout() {
|
|
118
|
-
const wasLoggedIn =
|
|
119
|
-
try { clearAuthSession(); } catch { /* idempotent — nothing to clear */ }
|
|
119
|
+
const wasLoggedIn = await endSession(process.env, { loadAuthSession, clearAuthSession });
|
|
120
120
|
return { ok: true, wasLoggedIn };
|
|
121
121
|
}
|
|
122
122
|
|
|
@@ -251,7 +251,7 @@ function makeOnboardingTools(deps = {}) {
|
|
|
251
251
|
const relatedInput = Array.isArray(args.relatedPortfolios) ? args.relatedPortfolios : [];
|
|
252
252
|
if (relatedInput.length === 0 || !agentId) return { ...base, portfolios };
|
|
253
253
|
|
|
254
|
-
//
|
|
254
|
+
// Replace-set: validate every portfolioId against the talent's own
|
|
255
255
|
// list, then PATCH the whole set in ONE call.
|
|
256
256
|
const items = [];
|
|
257
257
|
const relateFailures = [];
|
package/src/mcp-prompts.js
CHANGED
|
@@ -37,10 +37,10 @@ const PROMPTS = [
|
|
|
37
37
|
},
|
|
38
38
|
{
|
|
39
39
|
name: 'certify',
|
|
40
|
-
description: 'See which role dimensions you can certify
|
|
40
|
+
description: 'See which role dimensions you can certify; the certification itself is taken on the Shakers web.',
|
|
41
41
|
tools: ['list_certifiable_dimensions'],
|
|
42
42
|
buildMessages: () => userText(
|
|
43
|
-
'Help me certify on Shakers: call `list_certifiable_dimensions
|
|
43
|
+
'Help me certify on Shakers: call `list_certifiable_dimensions` and present the dimensions I can certify by name, and when I can retry the others. The certification interview is taken on the Shakers web, not in this chat: offer to open it with `open_web`. Never show internal ids/codes.',
|
|
44
44
|
),
|
|
45
45
|
},
|
|
46
46
|
{
|
|
@@ -204,6 +204,7 @@ function makeRegisterTools(deps = {}) {
|
|
|
204
204
|
|
|
205
205
|
// Creates the interview, joins its room and reads the greeting; `room` lets a timed-out caller close what opened late.
|
|
206
206
|
async function openRoom(args, room) {
|
|
207
|
+
if (typeof resolved.makeInterviewClient !== 'function') return onWeb('connect', 'livekit-not-installed');
|
|
207
208
|
const start = await flow.createAndStartLivekitOnboarding(resolved, {
|
|
208
209
|
candidateId: args.candidateId,
|
|
209
210
|
language: args.language || lang,
|
package/src/mcp-server.js
CHANGED
|
@@ -34,7 +34,7 @@ function errorResponse(id, code, message, data) {
|
|
|
34
34
|
return { jsonrpc: JSONRPC, id: id === undefined ? null : id, error };
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
-
function createMcpServer({ tools = [], prompts = [], serverInfo = {}, protocolVersion, beforeCall = null, onInitialize = null, instructions = null, elicitationTimeoutMs = ELICITATION_TIMEOUT_MS } = {}) {
|
|
37
|
+
function createMcpServer({ tools = [], prompts = [], serverInfo = {}, protocolVersion, beforeCall = null, afterCall = null, onInitialize = null, instructions = null, elicitationTimeoutMs = ELICITATION_TIMEOUT_MS } = {}) {
|
|
38
38
|
const registry = new Map();
|
|
39
39
|
for (const t of tools) {
|
|
40
40
|
if (!t || !t.name || typeof t.handler !== 'function') {
|
|
@@ -185,6 +185,10 @@ function createMcpServer({ tools = [], prompts = [], serverInfo = {}, protocolVe
|
|
|
185
185
|
content: [{ type: 'text', text: `${name} tool error: ${e && e.message ? e.message : String(e)}` }],
|
|
186
186
|
isError: true,
|
|
187
187
|
};
|
|
188
|
+
} finally {
|
|
189
|
+
if (typeof afterCall === 'function') {
|
|
190
|
+
try { afterCall(name); } catch { /* hook must never break a tool call */ }
|
|
191
|
+
}
|
|
188
192
|
}
|
|
189
193
|
}
|
|
190
194
|
|
package/src/mcp-signup-client.js
CHANGED
|
@@ -2,12 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
const { postJsonWithTimeout, postMultipartWithTimeout } = require('./backend-request');
|
|
4
4
|
|
|
5
|
-
// Hub calls of the MCP sign-up
|
|
5
|
+
// Hub calls of the MCP sign-up: public sign-up + claim code, the signed-in talent's import status, and the one-time web login token.
|
|
6
6
|
|
|
7
7
|
const SIGNUP_TIMEOUT_MS = 60000;
|
|
8
8
|
const DEFAULT_TIMEOUT_MS = 15000;
|
|
9
9
|
|
|
10
|
-
// Hub's coded errors on the sign-up
|
|
10
|
+
// Hub's coded errors on the sign-up; none says whether an account exists.
|
|
11
11
|
const CODE_TO_REASON = {
|
|
12
12
|
MISSING_SOURCE: 'missing-source',
|
|
13
13
|
INVALID_CV: 'invalid-cv',
|
package/src/mcp-signup-tools.js
CHANGED
|
@@ -13,7 +13,7 @@ const { askAfterTexts, askConsent, optionFor, queueNotice, takeNotices } = requi
|
|
|
13
13
|
const { expandHome, tildify } = require('./env-paths');
|
|
14
14
|
const { NOOP_FUNNEL, weeklyHours, rateTypes, workSituationValue } = require('./onboarding-funnel');
|
|
15
15
|
|
|
16
|
-
// MCP sign-up
|
|
16
|
+
// MCP sign-up: hub imports into an UNREGISTERED profile that the local window claims with a code kept only in memory.
|
|
17
17
|
|
|
18
18
|
const SIGNUP_WINDOW_TIMEOUT_MS = 15 * 60 * 1000;
|
|
19
19
|
// The browser opens this long after the tool answers, so the AI's reply is on screen first.
|
|
@@ -302,6 +302,7 @@ function makeSignupTools(deps = {}) {
|
|
|
302
302
|
pollDeviceToken = require('./device-login').pollDeviceToken,
|
|
303
303
|
finalizeDeviceSession = require('./device-login').finalizeDeviceSession,
|
|
304
304
|
appendProviderParam = require('./device-login').appendProviderParam,
|
|
305
|
+
deviceSessionRecord = require('./device-login').deviceSessionRecord,
|
|
305
306
|
buildRegistrationContext = require('./signup-client').buildRegistrationContext,
|
|
306
307
|
runLoopbackAuth = require('./mcp-auth-form').runLoopbackAuth,
|
|
307
308
|
checkOnboardingCompleted = require('./onboarding-availability').checkOnboardingCompleted,
|
|
@@ -347,6 +348,10 @@ function makeSignupTools(deps = {}) {
|
|
|
347
348
|
funnel.completed('account_creation', { auth_method: next.via, is_returning_account: next.state === 'existing' });
|
|
348
349
|
funnel.identified();
|
|
349
350
|
}
|
|
351
|
+
if (next.state === 'failed' && before !== 'failed') {
|
|
352
|
+
funnel.errored('account_creation', next.reason);
|
|
353
|
+
funnel.flush();
|
|
354
|
+
}
|
|
350
355
|
if (state.wake) state.wake();
|
|
351
356
|
}
|
|
352
357
|
|
|
@@ -385,7 +390,7 @@ function makeSignupTools(deps = {}) {
|
|
|
385
390
|
}
|
|
386
391
|
const email = res.email || emailFromHubToken(fin.token);
|
|
387
392
|
try {
|
|
388
|
-
resolved.saveAuthSession({
|
|
393
|
+
resolved.saveAuthSession(deviceSessionRecord({ token: fin.token, sessionToken: res.accessToken, sessionExpiresIn: res.expiresIn, email }));
|
|
389
394
|
} catch {
|
|
390
395
|
setAccount({ state: 'failed', reason: 'session-persist-failed' });
|
|
391
396
|
return;
|
|
@@ -576,6 +581,7 @@ function makeSignupTools(deps = {}) {
|
|
|
576
581
|
return { ok: false, reason: 'draft-sources-required', message: "The draft needs sources: 'cv' if you read their CV, 'linkedin' only if you have their LinkedIn URL. Then call signup_draft again." };
|
|
577
582
|
}
|
|
578
583
|
if (!state.firstDraft) state.firstDraft = draft;
|
|
584
|
+
trackDraftShown(draft);
|
|
579
585
|
const asked = await askAfterTexts(ctx, {
|
|
580
586
|
tool: 'signup_draft',
|
|
581
587
|
texts: [renderDraft(lang(), draft), legalCopy(lang()).signupNotice(sources), knownAccount() ? c.windowNoticeSignIn : c.windowNotice],
|
|
@@ -951,6 +957,20 @@ function makeSignupTools(deps = {}) {
|
|
|
951
957
|
return c.ratesSaved(shown, !(state.draft && state.draft.ratesFromTalent));
|
|
952
958
|
}
|
|
953
959
|
|
|
960
|
+
function trackDraftShown(draft) {
|
|
961
|
+
funnel.viewed('work_situation', { is_prefilled: true });
|
|
962
|
+
funnel.viewed('availability');
|
|
963
|
+
funnel.viewed('rates', { suggested_rate_shown: draft.ratesFromTalent !== true });
|
|
964
|
+
funnel.viewed('languages');
|
|
965
|
+
funnel.viewed('location');
|
|
966
|
+
funnel.viewed('profile_summary');
|
|
967
|
+
}
|
|
968
|
+
|
|
969
|
+
function draftFieldsEdited() {
|
|
970
|
+
if (!state.firstDraft || !state.draft) return undefined;
|
|
971
|
+
return DRAFT_FIELDS.filter((key) => String(state.firstDraft[key] ?? '') !== String(state.draft[key] ?? '')).length;
|
|
972
|
+
}
|
|
973
|
+
|
|
954
974
|
function wasEdited(step) {
|
|
955
975
|
if (!state.firstDraft || !state.draft) return undefined;
|
|
956
976
|
return STEP_DRAFT_FIELDS[step].some((key) => String(state.firstDraft[key] ?? '') !== String(state.draft[key] ?? ''));
|
|
@@ -982,6 +1002,10 @@ function makeSignupTools(deps = {}) {
|
|
|
982
1002
|
const codes = details.languages.map((row) => String(row.language || '').trim().toLowerCase()).filter(Boolean);
|
|
983
1003
|
step('languages', { languages_count: codes.length, languages: codes });
|
|
984
1004
|
}
|
|
1005
|
+
const sections = Object.entries(results).filter(([name]) => name !== 'phone');
|
|
1006
|
+
if (sections.length && sections.every(([, r]) => r.ok === true)) {
|
|
1007
|
+
funnel.completed('profile_summary', { fields_edited_count: draftFieldsEdited() });
|
|
1008
|
+
}
|
|
985
1009
|
funnel.flush();
|
|
986
1010
|
}
|
|
987
1011
|
|
|
@@ -4,7 +4,7 @@ const fs = require('fs');
|
|
|
4
4
|
const path = require('path');
|
|
5
5
|
const { getHomeDir } = require('./env-paths');
|
|
6
6
|
|
|
7
|
-
// Deterministic (no-LLM) memory STRUCTURE detector
|
|
7
|
+
// Deterministic (no-LLM) memory STRUCTURE detector.
|
|
8
8
|
|
|
9
9
|
const MAX_FILE_BYTES = 2_000_000; // safety cap: never read a pathologically huge file
|
|
10
10
|
const MAX_IMPORT_DEPTH = 5; // mirrors Claude Code's own documented import-hop limit
|
package/src/model-call-record.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
// What happened with each model call, written down
|
|
3
|
+
// What happened with each model call, written down.
|
|
4
4
|
|
|
5
5
|
// The three keys.
|
|
6
6
|
const MODEL_CALL = Object.freeze({
|
|
@@ -163,7 +163,7 @@ function callPartial(report, key) {
|
|
|
163
163
|
return !!c && c.ok === true && Array.isArray(c.omitted) && c.omitted.length > 0;
|
|
164
164
|
}
|
|
165
165
|
|
|
166
|
-
// THE CALL LOST TIME TO A DEADLINE — and this is NOT `reason === TIMEOUT` (
|
|
166
|
+
// THE CALL LOST TIME TO A DEADLINE — and this is NOT `reason === TIMEOUT` (found by measuring, not by reading).
|
|
167
167
|
function callTimedOut(report, key) {
|
|
168
168
|
const c = modelCall(report, key);
|
|
169
169
|
if (!c || c.ok !== false) return false;
|
package/src/onboarding-flow.js
CHANGED
|
@@ -168,7 +168,7 @@ function normalizeAmount(value) {
|
|
|
168
168
|
return cents / 100;
|
|
169
169
|
}
|
|
170
170
|
|
|
171
|
-
// Pricing input
|
|
171
|
+
// Pricing input.
|
|
172
172
|
function validatePricing(input = {}) {
|
|
173
173
|
const fullSelected = input.fullTimeSelected === true;
|
|
174
174
|
const partSelected = input.partTimeSelected === true;
|
|
@@ -212,7 +212,7 @@ function buildPricingUpsert({ fullSelected, fullAmount, fullCurrency, partSelect
|
|
|
212
212
|
};
|
|
213
213
|
}
|
|
214
214
|
|
|
215
|
-
// Availability + location
|
|
215
|
+
// Availability + location.
|
|
216
216
|
function validateAvailability(input = {}) {
|
|
217
217
|
const body = { confirmed: true };
|
|
218
218
|
if (typeof input.available === 'boolean') body.available = input.available;
|
|
@@ -299,7 +299,6 @@ function makeDeps(overrides = {}) {
|
|
|
299
299
|
runMainRoleStep: require('./roles-flow').runOnboardingMainRoleStep,
|
|
300
300
|
makeRolesDeps: require('./roles-flow').makeRolesDeps,
|
|
301
301
|
requestDiscoveredInventory: inventory.requestDiscoveredInventory,
|
|
302
|
-
makeInterviewClient: (opts) => new (require('./interview-client').InterviewClient)(opts),
|
|
303
302
|
// Used by reattachConsentIdentity (A) to re-anchor the ingest identity.
|
|
304
303
|
recordConsent: require('./share-consent-state').recordConsent,
|
|
305
304
|
...overrides,
|
|
@@ -371,7 +370,7 @@ async function establishSessionFromCredentials(deps, { email, password } = {}) {
|
|
|
371
370
|
|
|
372
371
|
// Register a talent via a social provider (device flow, RFC 8628); a live session results either way.
|
|
373
372
|
async function registerWithSocialAccount(deps, { provider = 'google', freelanceType, language } = {}, { openBrowser, onAuthUrl } = {}) {
|
|
374
|
-
const { appendProviderParam } = require('./device-login');
|
|
373
|
+
const { appendProviderParam, deviceSessionRecord } = require('./device-login');
|
|
375
374
|
const authorizeEndpoint = deps.getDeviceAuthorizeEndpoint();
|
|
376
375
|
const tokenEndpoint = deps.getDeviceTokenEndpoint();
|
|
377
376
|
const authTokenEndpoint = typeof deps.getAuthTokenEndpoint === 'function' ? deps.getAuthTokenEndpoint() : null;
|
|
@@ -397,13 +396,8 @@ async function registerWithSocialAccount(deps, { provider = 'google', freelanceT
|
|
|
397
396
|
);
|
|
398
397
|
if (!result.ok) return { ok: false, reason: result.reason || 'register-failed' };
|
|
399
398
|
|
|
400
|
-
// Persist the API JWT the exchange minted (no cookie), like the retired Google flow did.
|
|
401
399
|
try {
|
|
402
|
-
deps.saveAuthSession(
|
|
403
|
-
accessToken: result.token,
|
|
404
|
-
hubAccessToken: result.token,
|
|
405
|
-
email: result.email || null,
|
|
406
|
-
});
|
|
400
|
+
deps.saveAuthSession(deviceSessionRecord(result));
|
|
407
401
|
} catch {
|
|
408
402
|
return { ok: false, reason: 'session-persist-failed' };
|
|
409
403
|
}
|
|
@@ -698,6 +692,12 @@ async function runOnboarding(io, deps = makeDeps(), { method = 'email', openBrow
|
|
|
698
692
|
funnel.completed('start');
|
|
699
693
|
io.section(c.signupTitle);
|
|
700
694
|
funnel.viewed('account_creation');
|
|
695
|
+
funnel.flush();
|
|
696
|
+
const accountFailed = (reason) => {
|
|
697
|
+
funnel.errored('account_creation', reason);
|
|
698
|
+
funnel.flush();
|
|
699
|
+
return { ok: false, reason, step: 'signup' };
|
|
700
|
+
};
|
|
701
701
|
|
|
702
702
|
if (method === 'google' || method === 'linkedin') {
|
|
703
703
|
// The provider supplies name/email; freelanceType is derived later from work situation.
|
|
@@ -709,7 +709,7 @@ async function runOnboarding(io, deps = makeDeps(), { method = 'email', openBrow
|
|
|
709
709
|
);
|
|
710
710
|
if (!reg.ok) {
|
|
711
711
|
io.error(c.signupError);
|
|
712
|
-
return
|
|
712
|
+
return accountFailed(reg.reason);
|
|
713
713
|
}
|
|
714
714
|
funnel.completed('account_creation', { auth_method: method, is_returning_account: reg.kind === 'account-already-exists' });
|
|
715
715
|
funnel.identified();
|
|
@@ -721,23 +721,23 @@ async function runOnboarding(io, deps = makeDeps(), { method = 'email', openBrow
|
|
|
721
721
|
notifyOk(io, c.accountReady);
|
|
722
722
|
} else {
|
|
723
723
|
const account = await io.askSignUp(c);
|
|
724
|
-
if (!account || !account.email) return
|
|
724
|
+
if (!account || !account.email) return accountFailed('missing-fields');
|
|
725
725
|
if (!isAcceptablePassword(account.password)) {
|
|
726
726
|
io.error(c.weakPassword);
|
|
727
|
-
return
|
|
727
|
+
return accountFailed('weak-password');
|
|
728
728
|
}
|
|
729
729
|
|
|
730
730
|
const su = await signUp(deps, { ...account, preferredLanguage: account.preferredLanguage || io.lang });
|
|
731
731
|
if (!su.ok) {
|
|
732
732
|
io.error(su.message || c.signupError);
|
|
733
|
-
return
|
|
733
|
+
return accountFailed(su.reason);
|
|
734
734
|
}
|
|
735
735
|
|
|
736
736
|
if (su.accountExists) {
|
|
737
737
|
// Existing account: sign them in, then STOP — no wizard re-run.
|
|
738
738
|
io.warn(c.accountExists);
|
|
739
739
|
const logged = await io.login(account.email);
|
|
740
|
-
if (!logged) return
|
|
740
|
+
if (!logged) return accountFailed('login-failed');
|
|
741
741
|
funnel.completed('account_creation', { auth_method: 'email', is_returning_account: true });
|
|
742
742
|
funnel.identified();
|
|
743
743
|
return { ok: true, alreadyRegistered: true };
|
|
@@ -745,7 +745,7 @@ async function runOnboarding(io, deps = makeDeps(), { method = 'email', openBrow
|
|
|
745
745
|
const sess = await establishSessionFromCredentials(deps, { email: account.email, password: account.password });
|
|
746
746
|
if (!sess.ok) {
|
|
747
747
|
io.error(c.sessionError);
|
|
748
|
-
return
|
|
748
|
+
return accountFailed(sess.reason);
|
|
749
749
|
}
|
|
750
750
|
funnel.completed('account_creation', { auth_method: 'email', is_returning_account: false });
|
|
751
751
|
funnel.identified();
|