shakers-cli 0.0.0-stage → 0.14.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/LICENSE +59 -0
- package/README.md +1233 -2
- package/bin/add-agent.js +88 -0
- package/bin/add-project.js +88 -0
- package/bin/add-role.js +61 -0
- package/bin/add-skill.js +88 -0
- package/bin/ai-usage.js +99 -0
- package/bin/alma.js +99 -0
- package/bin/applications.js +69 -0
- package/bin/availability.js +70 -0
- package/bin/certifications.js +69 -0
- package/bin/certify.js +73 -0
- package/bin/change-role.js +61 -0
- package/bin/config.js +124 -0
- package/bin/experiences.js +69 -0
- package/bin/find-projects.js +89 -0
- package/bin/invitations.js +69 -0
- package/bin/lang.js +70 -0
- package/bin/login.js +216 -0
- package/bin/mcp.js +124 -0
- package/bin/mcpb.js +5 -0
- package/bin/me.js +69 -0
- package/bin/onboarding.js +134 -0
- package/bin/portfolios.js +69 -0
- package/bin/rate.js +70 -0
- package/bin/register.js +337 -0
- package/bin/report-html.js +134 -0
- package/bin/save.js +77 -0
- package/bin/shakers.js +363 -0
- package/bin/share.js +72 -0
- package/bin/show-project.js +72 -0
- package/bin/socials.js +70 -0
- package/bin/superadmin.js +351 -0
- package/bin/update-check-worker.js +70 -0
- package/bin/update.js +111 -0
- package/package.json +40 -4
- package/src/agent-category.js +13 -0
- package/src/agent-evaluation.js +177 -0
- package/src/agent-org-chart.js +355 -0
- package/src/agent-synthesis.js +132 -0
- package/src/agent-usage.js +118 -0
- package/src/agents-client.js +165 -0
- package/src/ai-fluency-cell.js +72 -0
- package/src/ai-profile-client.js +45 -0
- package/src/ai-profile-preview.js +154 -0
- package/src/ai.js +66 -0
- package/src/alma-client.js +123 -0
- package/src/alma-flow.js +32 -0
- package/src/alma-repo-context.js +47 -0
- package/src/alma-shell.js +161 -0
- package/src/ansi.js +62 -0
- package/src/applications-client.js +79 -0
- package/src/applications-flow.js +58 -0
- package/src/auth-client.js +144 -0
- package/src/auth-device-client.js +118 -0
- package/src/auth-method-picker.js +44 -0
- package/src/auth-methods.js +35 -0
- package/src/auth-session-store.js +215 -0
- package/src/authorship-mismatch.js +62 -0
- package/src/authorship.js +204 -0
- package/src/automations-detector.js +170 -0
- package/src/availability-client.js +93 -0
- package/src/availability-flow.js +124 -0
- package/src/backend-request.js +191 -0
- package/src/blind-approval-extract.js +48 -0
- package/src/brand-ansi.js +14 -0
- package/src/brand-svg.js +9 -0
- package/src/browser-tools-detector.js +21 -0
- package/src/build-next-level.js +56 -0
- package/src/certifications-client.js +89 -0
- package/src/certifications-flow.js +75 -0
- package/src/certify-dimension-client.js +163 -0
- package/src/certify-dimension-flow.js +135 -0
- package/src/certify-disclaimer.js +51 -0
- package/src/certify-remediation-prompt.js +23 -0
- package/src/cli-args.js +83 -0
- package/src/command-audit.js +105 -0
- package/src/config-dir.js +120 -0
- package/src/config.js +877 -0
- package/src/consent-flow.js +115 -0
- package/src/consent-skip.js +27 -0
- package/src/cv-file.js +62 -0
- package/src/cv-text.js +92 -0
- package/src/decisions-extract.js +43 -0
- package/src/detect-repos.js +145 -0
- package/src/detected-tools.js +15 -0
- package/src/detection-evidence.js +105 -0
- package/src/detection-noise.js +132 -0
- package/src/detectors.js +221 -0
- package/src/device-auth-store.js +100 -0
- package/src/device-login.js +120 -0
- package/src/email-verification.js +152 -0
- package/src/env-paths.js +30 -0
- package/src/find-projects-cache.js +89 -0
- package/src/find-projects-client.js +148 -0
- package/src/find-projects-flow.js +164 -0
- package/src/git-activity.js +124 -0
- package/src/git-log-prefetch.js +53 -0
- package/src/graph-certs.js +58 -0
- package/src/graph-generator.js +230 -0
- package/src/graph-scan.js +239 -0
- package/src/i18n.js +3341 -0
- package/src/inter-fonts.js +6 -0
- package/src/interactive-select.js +183 -0
- package/src/interview-client.js +226 -0
- package/src/interview-transcript.js +56 -0
- package/src/inventory-client.js +90 -0
- package/src/invitations-client.js +66 -0
- package/src/invitations-flow.js +50 -0
- package/src/lang-client.js +147 -0
- package/src/lang-codes.js +14 -0
- package/src/lang-flow.js +85 -0
- package/src/legal-notice.js +53 -0
- package/src/locale.js +72 -0
- package/src/markdown-ansi.js +50 -0
- package/src/maturity.js +111 -0
- package/src/mcp-ai-usage-result-tool.js +111 -0
- package/src/mcp-ai-usage-tool.js +401 -0
- package/src/mcp-alma-tools.js +95 -0
- package/src/mcp-applications-tools.js +44 -0
- package/src/mcp-auth-form.js +480 -0
- package/src/mcp-availability-tools.js +45 -0
- package/src/mcp-certifications-tools.js +44 -0
- package/src/mcp-certify-dimension-tools.js +66 -0
- package/src/mcp-choice.js +95 -0
- package/src/mcp-detector.js +223 -0
- package/src/mcp-device-login-tools.js +177 -0
- package/src/mcp-find-projects-tools.js +69 -0
- package/src/mcp-install.js +275 -0
- package/src/mcp-instructions.js +27 -0
- package/src/mcp-invitations-tools.js +44 -0
- package/src/mcp-lang-tools.js +41 -0
- package/src/mcp-onboarding-tools.js +379 -0
- package/src/mcp-portfolios-tools.js +50 -0
- package/src/mcp-profile-tools.js +48 -0
- package/src/mcp-prompts.js +73 -0
- package/src/mcp-rate-tools.js +44 -0
- package/src/mcp-register-tools.js +441 -0
- package/src/mcp-report-tools.js +105 -0
- package/src/mcp-role-tools.js +372 -0
- package/src/mcp-saved-positions-tools.js +59 -0
- package/src/mcp-server.js +290 -0
- package/src/mcp-show-project-tools.js +52 -0
- package/src/mcp-signup-client.js +118 -0
- package/src/mcp-signup-tools.js +1171 -0
- package/src/mcp-socials-tools.js +41 -0
- package/src/memory-structure-detector.js +108 -0
- package/src/model-call-record.js +198 -0
- package/src/onboarding-availability.js +26 -0
- package/src/onboarding-client.js +374 -0
- package/src/onboarding-flow.js +1162 -0
- package/src/onboarding-funnel.js +272 -0
- package/src/onboarding-livekit-client.js +102 -0
- package/src/onboarding-status-client.js +57 -0
- package/src/open-file.js +32 -0
- package/src/osc-link.js +16 -0
- package/src/portfolio-client.js +164 -0
- package/src/portfolio-git-info.js +61 -0
- package/src/portfolios-client.js +85 -0
- package/src/portfolios-flow.js +79 -0
- package/src/profile-client.js +133 -0
- package/src/profile-flow.js +115 -0
- package/src/profile-switch.js +46 -0
- package/src/prompt-select.js +66 -0
- package/src/rate-client.js +81 -0
- package/src/rate-flow.js +137 -0
- package/src/register-context.js +145 -0
- package/src/render-ai-fluency.js +266 -0
- package/src/render-certification.js +177 -0
- package/src/render-html.js +140 -0
- package/src/render-report-text.js +27 -0
- package/src/render-sheet.js +261 -0
- package/src/render-terminal.js +109 -0
- package/src/render-usage-report-text.js +155 -0
- package/src/repo-context.js +215 -0
- package/src/report-gating.js +49 -0
- package/src/report-store.js +226 -0
- package/src/report-theme.js +177 -0
- package/src/report-view-client.js +50 -0
- package/src/reveal-output.js +29 -0
- package/src/roadmap-content.js +453 -0
- package/src/roadmap-personalization.js +132 -0
- package/src/roadmap-prompt.js +154 -0
- package/src/roles-client.js +112 -0
- package/src/roles-flow.js +192 -0
- package/src/sanitize-network-text.js +36 -0
- package/src/saved-positions-client.js +71 -0
- package/src/saved-positions-flow.js +50 -0
- package/src/scan-exclusions.js +50 -0
- package/src/scanner.js +624 -0
- package/src/scope-signals.js +148 -0
- package/src/secret-prompt.js +72 -0
- package/src/session-gate.js +30 -0
- package/src/session-normalizers.js +174 -0
- package/src/session-refresh.js +51 -0
- package/src/session-scan.js +475 -0
- package/src/session-signals.js +36 -0
- package/src/shakers-wordmark.js +17 -0
- package/src/share-card.js +362 -0
- package/src/share-certification.js +111 -0
- package/src/share-consent-state.js +157 -0
- package/src/share-payload.js +300 -0
- package/src/share.js +185 -0
- package/src/show-project-client.js +58 -0
- package/src/show-project-flow.js +119 -0
- package/src/signup-client.js +103 -0
- package/src/signup-copy.js +450 -0
- package/src/signup-language.js +21 -0
- package/src/skills-client.js +102 -0
- package/src/socials-client.js +91 -0
- package/src/socials-flow.js +83 -0
- package/src/specificity-extract.js +41 -0
- package/src/start-add-agents.js +306 -0
- package/src/start-add-portfolio.js +388 -0
- package/src/start-add-skills.js +260 -0
- package/src/stdin-ask.js +129 -0
- package/src/steering-extract.js +43 -0
- package/src/strip-internal-comments.js +45 -0
- package/src/tech-detector.js +295 -0
- package/src/templates/report-sheet.html +572 -0
- package/src/terminal-agents.js +207 -0
- package/src/terminal-format.js +85 -0
- package/src/terminal-progress.js +106 -0
- package/src/terminal-sections.js +219 -0
- package/src/tier-analysis.js +126 -0
- package/src/tier-engine.js +114 -0
- package/src/tier-projection.js +117 -0
- package/src/transcript-turns.js +49 -0
- package/src/translations-client.js +84 -0
- package/src/typewriter.js +154 -0
- package/src/update-flow.js +223 -0
- package/src/update-notifier.js +113 -0
- package/src/usage-model-calls.js +119 -0
- package/src/usage-oneshot-commands.js +180 -0
- package/src/usage-repo-scope.js +162 -0
- package/src/usage-run-steps.js +354 -0
- package/src/work-streams.js +79 -0
package/src/config.js
ADDED
|
@@ -0,0 +1,877 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const { readEnv } = require('./env-paths');
|
|
6
|
+
const { getConfigDir } = require('./config-dir');
|
|
7
|
+
|
|
8
|
+
// Persistent CLI config file (skill-code-certification, endpoint-config task).
|
|
9
|
+
function configDir(env = process.env) {
|
|
10
|
+
return getConfigDir(env);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function configFilePath(env = process.env) {
|
|
14
|
+
return path.join(configDir(env), 'config.json');
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function loadConfigFile(env = process.env) {
|
|
18
|
+
try {
|
|
19
|
+
const parsed = JSON.parse(fs.readFileSync(configFilePath(env), 'utf8'));
|
|
20
|
+
return parsed && typeof parsed === 'object' ? parsed : {};
|
|
21
|
+
} catch {
|
|
22
|
+
return {};
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// DISTRIBUTION PROFILE (talents-ai-score, ADR-058 fase 1).
|
|
27
|
+
const VALID_PROFILES = new Set(['external', 'talent']);
|
|
28
|
+
const PROFILE_DEFAULT_WHEN_ABSENT = 'talent';
|
|
29
|
+
|
|
30
|
+
function getProfile(env = process.env) {
|
|
31
|
+
const fromFile = loadConfigFile(env).profile;
|
|
32
|
+
return VALID_PROFILES.has(fromFile) ? fromFile : PROFILE_DEFAULT_WHEN_ABSENT;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function isTalentProfile(env = process.env) {
|
|
36
|
+
return getProfile(env) === 'talent';
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Explicit UI-language preference: SHAKERS_CLI_LANG env > config.json `lang`.
|
|
40
|
+
// Returns 'es' | 'en', or null when neither is set (OS detection then fills it, see i18n.js).
|
|
41
|
+
const VALID_UI_LANGS = new Set(['es', 'en']);
|
|
42
|
+
|
|
43
|
+
function getUiLang(env = process.env) {
|
|
44
|
+
const fromEnv = String(readEnv(env, 'LANG') || '').trim().toLowerCase();
|
|
45
|
+
if (VALID_UI_LANGS.has(fromEnv)) return fromEnv;
|
|
46
|
+
const fromFile = String(loadConfigFile(env).lang || '').trim().toLowerCase();
|
|
47
|
+
if (VALID_UI_LANGS.has(fromFile)) return fromFile;
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// config.json holds the superadmin session token (ADR-027) and consent/ endpoint data, so it must never be briefly world/group-readable.
|
|
52
|
+
function saveConfigFile(config, env = process.env) {
|
|
53
|
+
const dir = configDir(env);
|
|
54
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
55
|
+
fs.writeFileSync(configFilePath(env), JSON.stringify(config, null, 2), { mode: 0o600 });
|
|
56
|
+
try { fs.chmodSync(configFilePath(env), 0o600); } catch { /* e.g. Windows */ }
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Endpoint safety (skill-code-certification, endpoint-config task) — BOUNDED.
|
|
60
|
+
const LOCAL_HOSTS = new Set(['localhost', '127.0.0.1', '::1']);
|
|
61
|
+
const SHAKERS_CLI_DOMAINS = [];
|
|
62
|
+
|
|
63
|
+
function isAllowedShakersDomain(host) {
|
|
64
|
+
const h = String(host || '').toLowerCase();
|
|
65
|
+
return SHAKERS_CLI_DOMAINS.some((domain) => h === domain || h.endsWith(`.${domain}`));
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function validateEndpoint(value) {
|
|
69
|
+
const raw = typeof value === 'string' ? value.trim() : '';
|
|
70
|
+
if (!raw) return { ok: false, reason: 'empty' };
|
|
71
|
+
let u;
|
|
72
|
+
try {
|
|
73
|
+
u = new URL(raw);
|
|
74
|
+
} catch {
|
|
75
|
+
return { ok: false, reason: 'invalid-url' };
|
|
76
|
+
}
|
|
77
|
+
if (u.protocol !== 'http:' && u.protocol !== 'https:') {
|
|
78
|
+
return { ok: false, reason: 'bad-protocol' };
|
|
79
|
+
}
|
|
80
|
+
const host = u.hostname.replace(/^\[|\]$/g, '');
|
|
81
|
+
const isLocal = LOCAL_HOSTS.has(host);
|
|
82
|
+
if (!isLocal && u.protocol !== 'https:') {
|
|
83
|
+
return { ok: false, reason: 'insecure-remote' };
|
|
84
|
+
}
|
|
85
|
+
const isAllowlisted = isLocal || isAllowedShakersDomain(host);
|
|
86
|
+
return { ok: true, value: raw, host, isLocal, isAllowlisted };
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Persists the ingest endpoint into config.json after validating it.
|
|
90
|
+
function setIngestEndpoint(value, env = process.env, { confirmed = false } = {}) {
|
|
91
|
+
const v = validateEndpoint(value);
|
|
92
|
+
if (!v.ok) return { ok: false, reason: v.reason };
|
|
93
|
+
if (!v.isAllowlisted && !confirmed) return { ok: false, reason: 'needs-confirmation', host: v.host };
|
|
94
|
+
const config = loadConfigFile(env);
|
|
95
|
+
config.ingestEndpoint = v.value;
|
|
96
|
+
saveConfigFile(config, env);
|
|
97
|
+
return { ok: true, value: v.value, path: configFilePath(env) };
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const RETIRED_FALLBACK_CONFIG_KEY = 'ingestEndpointFallback';
|
|
101
|
+
|
|
102
|
+
// Whether the persisted config still carries the retired fallback key, so `--show-endpoint` can say it is being ignored.
|
|
103
|
+
function hasRetiredFallbackConfig(env = process.env) {
|
|
104
|
+
const value = loadConfigFile(env)[RETIRED_FALLBACK_CONFIG_KEY];
|
|
105
|
+
return typeof value === 'string' && value.trim().length > 0;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Resolves the effective ingest endpoint AND its source, for `--show-endpoint`.
|
|
109
|
+
function resolveIngestEndpoint(env = process.env) {
|
|
110
|
+
const fromEnv = readEnv(env, 'INGEST_ENDPOINT');
|
|
111
|
+
if (fromEnv && fromEnv.trim()) {
|
|
112
|
+
return { endpoint: fromEnv.trim(), source: 'env' };
|
|
113
|
+
}
|
|
114
|
+
const fromFile = loadConfigFile(env).ingestEndpoint;
|
|
115
|
+
if (fromFile && String(fromFile).trim()) {
|
|
116
|
+
const v = validateEndpoint(String(fromFile).trim());
|
|
117
|
+
if (v.ok) return { endpoint: v.value, source: 'config-file', path: configFilePath(env) };
|
|
118
|
+
return { endpoint: null, source: 'config-file-invalid', path: configFilePath(env), reason: v.reason };
|
|
119
|
+
}
|
|
120
|
+
return { endpoint: null, source: 'none' };
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Baked backend targets per distribution flavor (talents-ai-score, ADR-007/065).
|
|
124
|
+
// Only the DEFAULT bases; per-endpoint SHAKERS_CLI_* env and config.json overrides
|
|
125
|
+
// still win at each getter (env > config.json > baked profile).
|
|
126
|
+
const ENV_PROFILES = {
|
|
127
|
+
prod: {
|
|
128
|
+
certsBase: 'https://api.certifications.shakersworks.com/api/v1',
|
|
129
|
+
hubBase: 'https://api.hub.shakersworks.com/api/v1',
|
|
130
|
+
profileUrl: 'https://app.shakersworks.com/login',
|
|
131
|
+
almaBase: 'https://app.shakersworks.com',
|
|
132
|
+
},
|
|
133
|
+
staging: {
|
|
134
|
+
certsBase: 'https://api.staging.certifications.shakersworks.com/api/v1',
|
|
135
|
+
hubBase: 'https://api.hub.staging.shakersworks.com/api/v1',
|
|
136
|
+
profileUrl: 'https://works.staging.shakersworks.com/login',
|
|
137
|
+
almaBase: 'https://works.staging.shakersworks.com',
|
|
138
|
+
},
|
|
139
|
+
dev: {
|
|
140
|
+
certsBase: 'https://api.dev.certifications.shakersworks.com/api/v1',
|
|
141
|
+
hubBase: 'https://api.hub.new-works.dev.shakersworks.com/api/v1',
|
|
142
|
+
profileUrl: 'https://works.new-works.dev.shakersworks.com/login',
|
|
143
|
+
},
|
|
144
|
+
local: {
|
|
145
|
+
certsBase: 'http://localhost:3004/api/v1',
|
|
146
|
+
hubBase: 'http://localhost:3001/api/v1',
|
|
147
|
+
profileUrl: 'http://localhost:8081/login',
|
|
148
|
+
},
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
// Which flavor is baked: SHAKERS_CLI_ENV override wins, else derived from the
|
|
152
|
+
// published package name (public -> prod, beta -> staging, private -> dev), else dev (safe default).
|
|
153
|
+
const PACKAGE_NAME_ENV = { 'shakers-cli': 'prod', 'shakers-cli-beta': 'staging', '@shakers/shakers-cli': 'dev' };
|
|
154
|
+
const DEFAULT_ENV_PROFILE = 'dev';
|
|
155
|
+
|
|
156
|
+
function bakedEnvProfileName() {
|
|
157
|
+
try {
|
|
158
|
+
const name = require('../package.json').name;
|
|
159
|
+
if (PACKAGE_NAME_ENV[name]) return PACKAGE_NAME_ENV[name];
|
|
160
|
+
} catch { /* package.json unreadable — fall through to the safe default */ }
|
|
161
|
+
return DEFAULT_ENV_PROFILE;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function getEnvProfileName(env = process.env) {
|
|
165
|
+
const override = readEnv(env, 'ENV');
|
|
166
|
+
if (override && ENV_PROFILES[String(override).trim()]) return String(override).trim();
|
|
167
|
+
return bakedEnvProfileName();
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function getEnvProfile(env = process.env) {
|
|
171
|
+
return ENV_PROFILES[getEnvProfileName(env)] || ENV_PROFILES[DEFAULT_ENV_PROFILE];
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function explicitIngest(env = process.env) {
|
|
175
|
+
const value = readEnv(env, 'INGEST_ENDPOINT');
|
|
176
|
+
if (value && value.trim()) return value.trim();
|
|
177
|
+
const fromFile = loadConfigFile(env).ingestEndpoint;
|
|
178
|
+
if (fromFile && String(fromFile).trim()) {
|
|
179
|
+
const v = validateEndpoint(String(fromFile).trim());
|
|
180
|
+
if (!v.ok) return null;
|
|
181
|
+
// The ai-footprint -> shakers config migration rewrote the old hub ingest
|
|
182
|
+
// (`/works/ai-footprint/reports`) to `/works/usage/reports`, a route no service
|
|
183
|
+
// has: ingestion lives in certs. Kept, it silently sent every report and the
|
|
184
|
+
// agent evaluation there and both failed ("agentes sin categoria", 2026-09-25).
|
|
185
|
+
// Ignored, the endpoint derives from certsBase like a fresh install. The same
|
|
186
|
+
// goes for the loopback default install.sh used to bake into every config.
|
|
187
|
+
if (LEGACY_HUB_INGEST.test(v.value) || v.value === RETIRED_INSTALL_INGEST) return null;
|
|
188
|
+
return v.value;
|
|
189
|
+
}
|
|
190
|
+
return null;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
const LEGACY_HUB_INGEST = /\/works\/(usage|ai-footprint)\/reports\/?$/;
|
|
194
|
+
const RETIRED_INSTALL_INGEST = 'http://localhost:3004/api/v1/usage/reports';
|
|
195
|
+
|
|
196
|
+
function getCertsBase(env = process.env) {
|
|
197
|
+
const value = readEnv(env, 'CERTS_BASE');
|
|
198
|
+
if (value && value.trim()) return value.trim().replace(/\/+$/, '');
|
|
199
|
+
const fromFile = loadConfigFile(env).certsBase;
|
|
200
|
+
if (fromFile && String(fromFile).trim()) {
|
|
201
|
+
const v = validateEndpoint(String(fromFile).trim());
|
|
202
|
+
if (v.ok) return v.value.replace(/\/+$/, '');
|
|
203
|
+
}
|
|
204
|
+
const ingest = explicitIngest(env);
|
|
205
|
+
if (ingest) return ingest.replace(/\/usage\/reports\/?$/, '').replace(/\/+$/, '');
|
|
206
|
+
// No explicit override anywhere -> the baked flavor profile (never null when a
|
|
207
|
+
// profile exists), so switching the published flavor "just works" without a
|
|
208
|
+
// seeded config.json (ADR-065 revised).
|
|
209
|
+
const baked = getEnvProfile(env).certsBase;
|
|
210
|
+
return baked ? baked.replace(/\/+$/, '') : null;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function getIngestEndpoint(env = process.env) {
|
|
214
|
+
const explicit = explicitIngest(env);
|
|
215
|
+
if (explicit) return explicit;
|
|
216
|
+
const base = getCertsBase(env);
|
|
217
|
+
return base ? `${base}/usage/reports` : null;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// Agent-synthesis endpoint (talents-ai-score, ADR-010/ADR-011).
|
|
221
|
+
function getSynthesisEndpoint(env = process.env) {
|
|
222
|
+
const value = readEnv(env, 'SYNTHESIS_ENDPOINT');
|
|
223
|
+
if (value && value.trim()) return value.trim();
|
|
224
|
+
// Issue 111: derived as an ingest sibling when the override is unset.
|
|
225
|
+
return deriveFromIngest(env, 'agent-synthesis');
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function getRoadmapEndpoint(env = process.env) {
|
|
229
|
+
const value = readEnv(env, 'ROADMAP_ENDPOINT');
|
|
230
|
+
if (value && value.trim()) return value.trim();
|
|
231
|
+
return deriveFromIngest(env, 'roadmap-personalize');
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// Email-verification endpoints (ADR-006): the OTP "prove you own this email" step that gates PERSISTENCE. Sibling-of-ingest resolution (`new URL(relative, base)`); unset ingest -> null.
|
|
235
|
+
function deriveFromIngest(env, relativePath) {
|
|
236
|
+
const ingest = getIngestEndpoint(env);
|
|
237
|
+
if (!ingest) return null;
|
|
238
|
+
const base = ingest.replace(/\/+$/, '');
|
|
239
|
+
try {
|
|
240
|
+
return new URL(relativePath, base).href;
|
|
241
|
+
} catch {
|
|
242
|
+
return null;
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function deriveEmailVerificationUrl(env, segment) {
|
|
247
|
+
return deriveFromIngest(env, `email-verification/${segment}`);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// Agent-evaluation endpoint (footprint agent cards — RESTORED).
|
|
251
|
+
function getAgentEvaluationEndpoint(env = process.env) {
|
|
252
|
+
const explicit = readEnv(env, 'AGENT_EVAL_ENDPOINT');
|
|
253
|
+
if (explicit && explicit.trim()) return explicit.trim();
|
|
254
|
+
return deriveFromIngest(env, 'agent-evaluation');
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// Login endpoint — better-auth email sign-in (owner decision, supersedes ADR-031 for the email path): `POST {hubBase}/auth/sign-in/email` with `{email,password}`.
|
|
258
|
+
function getLoginEndpoint(env = process.env) {
|
|
259
|
+
const explicit = readEnv(env, 'LOGIN_ENDPOINT');
|
|
260
|
+
if (explicit && explicit.trim()) return explicit.trim();
|
|
261
|
+
return deriveFromHub(env, 'auth/sign-in/email');
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// better-auth JWT endpoint: `GET {hubBase}/auth/token` with the session cookie → `{token}` (ES256/JWKS, ~15-min TTL).
|
|
265
|
+
function getAuthTokenEndpoint(env = process.env) {
|
|
266
|
+
const explicit = readEnv(env, 'AUTH_TOKEN_ENDPOINT');
|
|
267
|
+
if (explicit && explicit.trim()) return explicit.trim();
|
|
268
|
+
return deriveFromHub(env, 'auth/token');
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
function getCompleteRegistrationEndpoint(env = process.env) {
|
|
272
|
+
const explicit = readEnv(env, 'COMPLETE_REGISTRATION_ENDPOINT');
|
|
273
|
+
if (explicit && explicit.trim()) return explicit.trim();
|
|
274
|
+
return deriveFromHub(env, 'works/talent/auth/complete-registration');
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// Google via device flow (RFC 8628): the Hub broker owns all Google config; the CLI holds no client_id.
|
|
278
|
+
function getDeviceAuthorizeEndpoint(env = process.env) {
|
|
279
|
+
const explicit = readEnv(env, 'DEVICE_AUTHORIZE_ENDPOINT');
|
|
280
|
+
if (explicit && explicit.trim()) return explicit.trim();
|
|
281
|
+
// better-auth device plugin, mounted at /auth.
|
|
282
|
+
return deriveFromHub(env, 'auth/device/code');
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
function getDeviceTokenEndpoint(env = process.env) {
|
|
286
|
+
const explicit = readEnv(env, 'DEVICE_TOKEN_ENDPOINT');
|
|
287
|
+
if (explicit && explicit.trim()) return explicit.trim();
|
|
288
|
+
// Same Nest prefix as complete-registration (the works auth controller).
|
|
289
|
+
return deriveFromHub(env, 'works/auth/device/token');
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// Dimension-certification endpoints. The LiveKit start/complete + `/interviews/:id/report`
|
|
293
|
+
// poll reuse `getOnboardingInterviewsEndpoint` (= `{certsBase}/interviews`).
|
|
294
|
+
function getMyCertificationsEndpoint(env = process.env) {
|
|
295
|
+
const explicit = readEnv(env, 'MY_CERTIFICATIONS_ENDPOINT');
|
|
296
|
+
if (explicit && explicit.trim()) return explicit.trim();
|
|
297
|
+
return deriveFromHub(env, 'works/talent/certifications/me');
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// Role/cluster endpoints (hub, talent Bearer). available-roles = the catalog the
|
|
301
|
+
// talent can add; main-role = set the preferred main; assigned-clusters = the
|
|
302
|
+
// talent's own GROWTH/RECOMMENDED roles (GET add POST, remove DELETE).
|
|
303
|
+
function getAvailableRolesEndpoint(env = process.env) {
|
|
304
|
+
const explicit = readEnv(env, 'AVAILABLE_ROLES_ENDPOINT');
|
|
305
|
+
if (explicit && explicit.trim()) return explicit.trim();
|
|
306
|
+
return deriveFromHub(env, 'works/talent/certifications/me/available-roles');
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
function getSetMainRoleEndpoint(env = process.env) {
|
|
310
|
+
const explicit = readEnv(env, 'SET_MAIN_ROLE_ENDPOINT');
|
|
311
|
+
if (explicit && explicit.trim()) return explicit.trim();
|
|
312
|
+
return deriveFromHub(env, 'works/talent/certifications/me/main-role');
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
function getAssignedClustersEndpoint(env = process.env) {
|
|
316
|
+
const explicit = readEnv(env, 'ASSIGNED_CLUSTERS_ENDPOINT');
|
|
317
|
+
if (explicit && explicit.trim()) return explicit.trim();
|
|
318
|
+
return deriveFromHub(env, 'works/talent/me/work-details/assigned-clusters');
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
function getTemplatesByDimensionEndpoint(env = process.env) {
|
|
322
|
+
const explicit = readEnv(env, 'TEMPLATES_BY_DIMENSION_ENDPOINT');
|
|
323
|
+
if (explicit && explicit.trim()) return explicit.trim();
|
|
324
|
+
const base = getCertsBase(env);
|
|
325
|
+
return base ? `${base}/templates/by-dimension` : null;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
// The Talent's assessments with their retry availability (certs, talent Bearer).
|
|
329
|
+
function getMyAssessmentsEndpoint(env = process.env) {
|
|
330
|
+
const explicit = readEnv(env, 'MY_ASSESSMENTS_ENDPOINT');
|
|
331
|
+
if (explicit && explicit.trim()) return explicit.trim();
|
|
332
|
+
const base = getCertsBase(env);
|
|
333
|
+
return base ? `${base}/users/me/assessments` : null;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
// `certify` creates its dimension interview here (certs, talent Bearer).
|
|
337
|
+
function getCertificationInterviewsEndpoint(env = process.env) {
|
|
338
|
+
const explicit = readEnv(env, 'CERTIFICATION_INTERVIEWS_ENDPOINT');
|
|
339
|
+
if (explicit && explicit.trim()) return explicit.trim();
|
|
340
|
+
const base = getCertsBase(env);
|
|
341
|
+
return base ? `${base}/users/me/certification-interviews` : null;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// Find Positions list (hub `works/positions/find`, talent Bearer). Serves the
|
|
345
|
+
// All/Recommended/Saved tabs via `criteria.savedByMe`; ordered by match desc.
|
|
346
|
+
function getFindPositionsEndpoint(env = process.env) {
|
|
347
|
+
const explicit = readEnv(env, 'FIND_POSITIONS_ENDPOINT');
|
|
348
|
+
if (explicit && explicit.trim()) return explicit.trim();
|
|
349
|
+
return deriveFromHub(env, 'works/talent/positions/find');
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
// Marketplace read/write (hub, talent Bearer). The id-taking getters return null
|
|
353
|
+
// on a non-scalar first arg so config.test.js's `fn(env)` sweep stays honest.
|
|
354
|
+
function getPositionDetailEndpoint(positionId, env = process.env) {
|
|
355
|
+
const explicit = readEnv(env, 'POSITION_DETAIL_ENDPOINT');
|
|
356
|
+
if (explicit && explicit.trim()) return explicit.trim();
|
|
357
|
+
if (positionId == null || typeof positionId === 'object') return null;
|
|
358
|
+
return deriveFromHub(env, `works/talent/positions/${encodeURIComponent(positionId)}/detail`);
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
function getSavePositionEndpoint(env = process.env) {
|
|
362
|
+
const explicit = readEnv(env, 'SAVE_POSITION_ENDPOINT');
|
|
363
|
+
if (explicit && explicit.trim()) return explicit.trim();
|
|
364
|
+
return deriveFromHub(env, 'works/talent/saved-positions');
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
function getUnsavePositionEndpoint(positionId, env = process.env) {
|
|
368
|
+
const explicit = readEnv(env, 'UNSAVE_POSITION_ENDPOINT');
|
|
369
|
+
if (explicit && explicit.trim()) return explicit.trim();
|
|
370
|
+
if (positionId == null || typeof positionId === 'object') return null;
|
|
371
|
+
return deriveFromHub(env, `works/talent/saved-positions/${encodeURIComponent(positionId)}`);
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
function getReceivedInvitationsEndpoint(env = process.env) {
|
|
375
|
+
const explicit = readEnv(env, 'RECEIVED_INVITATIONS_ENDPOINT');
|
|
376
|
+
if (explicit && explicit.trim()) return explicit.trim();
|
|
377
|
+
return deriveFromHub(env, 'works/positions/received-invitations');
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
function getMyCandidaturesEndpoint(env = process.env) {
|
|
381
|
+
const explicit = readEnv(env, 'MY_CANDIDATURES_ENDPOINT');
|
|
382
|
+
if (explicit && explicit.trim()) return explicit.trim();
|
|
383
|
+
return deriveFromHub(env, 'works/talent/candidatures/me');
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
function getMeAvailabilityEndpoint(env = process.env) {
|
|
387
|
+
const explicit = readEnv(env, 'ME_AVAILABILITY_ENDPOINT');
|
|
388
|
+
if (explicit && explicit.trim()) return explicit.trim();
|
|
389
|
+
return deriveFromHub(env, 'works/me/availability');
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
function getMeProfileEndpoint(env = process.env) {
|
|
393
|
+
const explicit = readEnv(env, 'ME_PROFILE_ENDPOINT');
|
|
394
|
+
if (explicit && explicit.trim()) return explicit.trim();
|
|
395
|
+
return deriveFromHub(env, 'works/talent/me/profile-summary');
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
function getTalentMeProfileEndpoint(env = process.env) {
|
|
399
|
+
const explicit = readEnv(env, 'TALENT_ME_PROFILE_ENDPOINT');
|
|
400
|
+
if (explicit && explicit.trim()) return explicit.trim();
|
|
401
|
+
return deriveFromHub(env, 'works/talent/me/profile');
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
// Alma (the AI assistant) is a SEPARATE service — NOT derived from hubBase.
|
|
405
|
+
// SHAKERS_CLI_ALMA_BASE > the baked profile's almaBase > the local/dev container.
|
|
406
|
+
const DEFAULT_ALMA_BASE = 'http://127.0.0.1:5030';
|
|
407
|
+
|
|
408
|
+
function getAlmaBase(env = process.env) {
|
|
409
|
+
const explicit = readEnv(env, 'ALMA_BASE');
|
|
410
|
+
if (explicit && explicit.trim()) return explicit.trim().replace(/\/+$/, '');
|
|
411
|
+
return getEnvProfile(env).almaBase || DEFAULT_ALMA_BASE;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
function getAlmaChatEndpoint(env = process.env) {
|
|
415
|
+
const explicit = readEnv(env, 'ALMA_CHAT_ENDPOINT');
|
|
416
|
+
if (explicit && explicit.trim()) return explicit.trim();
|
|
417
|
+
const base = getAlmaBase(env);
|
|
418
|
+
return base ? `${base}/alma/chat` : null;
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
function getAlmaDecisionEndpoint(env = process.env) {
|
|
422
|
+
const explicit = readEnv(env, 'ALMA_DECISION_ENDPOINT');
|
|
423
|
+
if (explicit && explicit.trim()) return explicit.trim();
|
|
424
|
+
const base = getAlmaBase(env);
|
|
425
|
+
return base ? `${base}/alma/decision` : null;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
function getSkillsDeclareEndpoint(env = process.env) {
|
|
429
|
+
const explicit = readEnv(env, 'SKILLS_DECLARE_ENDPOINT');
|
|
430
|
+
if (explicit && explicit.trim()) return explicit.trim();
|
|
431
|
+
return deriveFromIngest(env, '../works/talents/me/skills/declare');
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
function getSkillsResolveAddableEndpoint(env = process.env) {
|
|
435
|
+
const explicit = readEnv(env, 'SKILLS_RESOLVE_ADDABLE_ENDPOINT');
|
|
436
|
+
if (explicit && explicit.trim()) return explicit.trim();
|
|
437
|
+
return deriveFromIngest(env, '../works/talents/me/skills/resolve-addable');
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
function getSkillsResolveMatchedEndpoint(env = process.env) {
|
|
441
|
+
const explicit = readEnv(env, 'SKILLS_RESOLVE_MATCHED_ENDPOINT');
|
|
442
|
+
if (explicit && explicit.trim()) return explicit.trim();
|
|
443
|
+
return deriveFromIngest(env, '../works/talents/me/skills/resolve-matched');
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
function getUsageDiscoveredInventoryEndpoint(env = process.env) {
|
|
447
|
+
const explicit = readEnv(env, 'USAGE_DISCOVERED_INVENTORY_ENDPOINT');
|
|
448
|
+
if (explicit && explicit.trim()) return explicit.trim();
|
|
449
|
+
return deriveFromIngest(env, 'discovered-inventory');
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
function getUsageReportEndpoint(env = process.env) {
|
|
453
|
+
const explicit = readEnv(env, 'USAGE_REPORT_ENDPOINT');
|
|
454
|
+
if (explicit && explicit.trim()) return explicit.trim();
|
|
455
|
+
return deriveFromIngest(env, 'report');
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
// Hub "My work with AI" read model (same endpoint the front paints).
|
|
459
|
+
function getAiProfileEndpoint(env = process.env) {
|
|
460
|
+
const explicit = readEnv(env, 'AI_PROFILE_ENDPOINT');
|
|
461
|
+
if (explicit && explicit.trim()) return explicit.trim();
|
|
462
|
+
return deriveFromHub(env, 'works/talent/me/ai-profile');
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
// Evidence emission endpoint for the `cli_scan` instrument (talents-ai-score Phase 2).
|
|
466
|
+
function getEvidenceCliScanEndpoint(env = process.env) {
|
|
467
|
+
const explicit = readEnv(env, 'EVIDENCE_CLI_SCAN_ENDPOINT');
|
|
468
|
+
if (explicit && explicit.trim()) return explicit.trim();
|
|
469
|
+
return deriveFromIngest(env, '../evidence/cli-scan');
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
// Talent-portfolio endpoints (talents-ai-score, ADR-059 — `start`'s "Add project to portfolio" route, talent-only).
|
|
473
|
+
function getPortfoliosDeclareEndpoint(env = process.env) {
|
|
474
|
+
const explicit = readEnv(env, 'PORTFOLIOS_DECLARE_ENDPOINT');
|
|
475
|
+
if (explicit && explicit.trim()) return explicit.trim();
|
|
476
|
+
return deriveFromIngest(env, '../works/talents/me/portfolios/declare');
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
function getPortfoliosListEndpoint(env = process.env) {
|
|
480
|
+
const explicit = readEnv(env, 'PORTFOLIOS_LIST_ENDPOINT');
|
|
481
|
+
if (explicit && explicit.trim()) return explicit.trim();
|
|
482
|
+
return deriveFromIngest(env, '../works/talents/me/portfolios');
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
function getPortfolioDraftDescriptionEndpoint(env = process.env) {
|
|
486
|
+
const explicit = readEnv(env, 'PORTFOLIO_DRAFT_DESCRIPTION_ENDPOINT');
|
|
487
|
+
if (explicit && explicit.trim()) return explicit.trim();
|
|
488
|
+
return deriveFromIngest(env, '../works/talents/me/portfolios/draft-description');
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
function getPortfolioUpdateEndpoint(env = process.env) {
|
|
492
|
+
const explicit = readEnv(env, 'PORTFOLIO_UPDATE_ENDPOINT');
|
|
493
|
+
if (explicit && explicit.trim()) return explicit.trim();
|
|
494
|
+
return deriveFromHub(env, 'works/talent/portfolios');
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
function getAgentsDeclareEndpoint(env = process.env) {
|
|
498
|
+
const explicit = readEnv(env, 'AGENTS_DECLARE_ENDPOINT');
|
|
499
|
+
if (explicit && explicit.trim()) return explicit.trim();
|
|
500
|
+
return deriveFromIngest(env, '../works/talents/me/agents/declare');
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
function getAgentsListEndpoint(env = process.env) {
|
|
504
|
+
const explicit = readEnv(env, 'AGENTS_LIST_ENDPOINT');
|
|
505
|
+
if (explicit && explicit.trim()) return explicit.trim();
|
|
506
|
+
return deriveFromIngest(env, '../works/talents/me/agents');
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
function getAgentsDraftFieldsEndpoint(env = process.env) {
|
|
510
|
+
const explicit = readEnv(env, 'AGENTS_DRAFT_FIELDS_ENDPOINT');
|
|
511
|
+
if (explicit && explicit.trim()) return explicit.trim();
|
|
512
|
+
return deriveFromIngest(env, '../works/talents/me/agents/draft-fields');
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
// Agent<->experience relate endpoint (ADR-041, `start`'s "Add agent" relate step / MCP `add_agent.relatedPortfolios`).
|
|
516
|
+
function getAgentPortfoliosEndpoint(agentId, env = process.env) {
|
|
517
|
+
// Null on missing/non-scalar id: no bogus URL, and keeps config.test.js's `fn(env)`
|
|
518
|
+
// getter sweep honest when the env object lands in `agentId`.
|
|
519
|
+
if (agentId == null || typeof agentId === 'object') return null;
|
|
520
|
+
return deriveFromHub(env, `works/talent/me/agents/${encodeURIComponent(agentId)}/portfolios`);
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
// Onboarding endpoints (evaluation-mcp, ADR-011..015/024 — the register flow).
|
|
524
|
+
function getHubBase(env = process.env) {
|
|
525
|
+
const explicit = readEnv(env, 'HUB_BASE');
|
|
526
|
+
if (explicit && explicit.trim()) return explicit.trim().replace(/\/+$/, '');
|
|
527
|
+
const fromFile = loadConfigFile(env).hubBase;
|
|
528
|
+
if (fromFile && String(fromFile).trim()) {
|
|
529
|
+
const v = validateEndpoint(String(fromFile).trim());
|
|
530
|
+
if (v.ok) return v.value.replace(/\/+$/, '');
|
|
531
|
+
}
|
|
532
|
+
const baked = getEnvProfile(env).hubBase;
|
|
533
|
+
return baked ? baked.replace(/\/+$/, '') : null;
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
function deriveFromHub(env, relativePath) {
|
|
537
|
+
const base = getHubBase(env);
|
|
538
|
+
if (!base) return null;
|
|
539
|
+
return `${base}/${relativePath}`;
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
function getSignUpEndpoint(env = process.env) {
|
|
543
|
+
const explicit = readEnv(env, 'SIGNUP_ENDPOINT');
|
|
544
|
+
if (explicit && explicit.trim()) return explicit.trim();
|
|
545
|
+
return deriveFromHub(env, 'auth/sign-up/email');
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
function getImportProfileEndpoint(env = process.env) {
|
|
549
|
+
const explicit = readEnv(env, 'IMPORT_PROFILE_ENDPOINT');
|
|
550
|
+
if (explicit && explicit.trim()) return explicit.trim();
|
|
551
|
+
return deriveFromHub(env, 'works/talent/me/import-profile');
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
// MCP sign-up (ticket 39540): public UNREGISTERED + claim code, its import status, and the one-time web login.
|
|
555
|
+
function getMcpSignupEndpoint(env = process.env) {
|
|
556
|
+
return deriveFromHub(env, 'works-ai/talents/mcp-signup');
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
function getMyImportStatusEndpoint(env = process.env) {
|
|
560
|
+
return deriveFromHub(env, 'works/talent/me/import-profile/status');
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
function getOnboardingEventsEndpoint(env = process.env) {
|
|
564
|
+
return deriveFromHub(env, 'works/talent/me/onboarding-events');
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
function getOneTimeTokenEndpoint(env = process.env) {
|
|
568
|
+
return deriveFromHub(env, 'auth/one-time-token/generate');
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
// The ONE place the web route that redeems a one-time token is spelled (the route itself is a later works-frontend ticket).
|
|
572
|
+
function getOneTimeLoginUrl(token, env = process.env) {
|
|
573
|
+
if (typeof token !== 'string' || !token) return null;
|
|
574
|
+
const profile = getTalentProfileUrl(env);
|
|
575
|
+
if (!profile) return null;
|
|
576
|
+
try {
|
|
577
|
+
return `${new URL(profile).origin}/auth/one-time?token=${encodeURIComponent(token)}`;
|
|
578
|
+
} catch {
|
|
579
|
+
return null;
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
function getProfessionalDetailsEndpoint(env = process.env) {
|
|
584
|
+
const explicit = readEnv(env, 'PROFESSIONAL_DETAILS_ENDPOINT');
|
|
585
|
+
if (explicit && explicit.trim()) return explicit.trim();
|
|
586
|
+
return deriveFromHub(env, 'works/talent/me/professional-details');
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
function getPricingRateEndpoint(env = process.env) {
|
|
590
|
+
const explicit = readEnv(env, 'PRICING_RATE_ENDPOINT');
|
|
591
|
+
if (explicit && explicit.trim()) return explicit.trim();
|
|
592
|
+
return deriveFromHub(env, 'works/talent/me/work-details/pricing-rate');
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
function getSetAvailabilityEndpoint(env = process.env) {
|
|
596
|
+
const explicit = readEnv(env, 'AVAILABILITY_ENDPOINT');
|
|
597
|
+
if (explicit && explicit.trim()) return explicit.trim();
|
|
598
|
+
return deriveFromHub(env, 'works/talent/me/work-details/availability');
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
function getLanguagesEndpoint(env = process.env) {
|
|
602
|
+
const explicit = readEnv(env, 'LANGUAGES_ENDPOINT');
|
|
603
|
+
if (explicit && explicit.trim()) return explicit.trim();
|
|
604
|
+
return deriveFromHub(env, 'works/talent/me/work-details/languages');
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
function getLanguagesCatalogEndpoint(env = process.env) {
|
|
608
|
+
const explicit = readEnv(env, 'LANGUAGES_CATALOG_ENDPOINT');
|
|
609
|
+
if (explicit && explicit.trim()) return explicit.trim();
|
|
610
|
+
return deriveFromHub(env, 'static-data/languages');
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
// Talent social links (GET + PUT upsert of the whole object).
|
|
614
|
+
function getMeSocialEndpoint(env = process.env) {
|
|
615
|
+
const explicit = readEnv(env, 'ME_SOCIAL_ENDPOINT');
|
|
616
|
+
if (explicit && explicit.trim()) return explicit.trim();
|
|
617
|
+
return deriveFromHub(env, 'works/me/social');
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
// Talent portfolio entries (GET; ?type=EXPERIENCE|PORTFOLIO — one entity, two subsets).
|
|
621
|
+
function getMePortfoliosEndpoint(env = process.env) {
|
|
622
|
+
const explicit = readEnv(env, 'ME_PORTFOLIOS_ENDPOINT');
|
|
623
|
+
if (explicit && explicit.trim()) return explicit.trim();
|
|
624
|
+
return deriveFromHub(env, 'works/me/portfolios');
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
// i18n translations (GET ?namespace=static-data): resolves static-data KEYS
|
|
628
|
+
// (e.g. staticDataSkillsSkill_N) to readable names per language. Bearer required.
|
|
629
|
+
function getTranslationsEndpoint(env = process.env) {
|
|
630
|
+
const explicit = readEnv(env, 'TRANSLATIONS_ENDPOINT');
|
|
631
|
+
if (explicit && explicit.trim()) return explicit.trim();
|
|
632
|
+
return deriveFromHub(env, 'translations');
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
// The signed-in user as hub sees it (GET only): read back the phone after saving it.
|
|
636
|
+
function getUserMeEndpoint(env = process.env) {
|
|
637
|
+
const explicit = readEnv(env, 'USER_ME_ENDPOINT');
|
|
638
|
+
if (explicit && explicit.trim()) return explicit.trim();
|
|
639
|
+
return deriveFromHub(env, 'works/users/me');
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
// Talent base fields (PATCH name, phone...). works/users/me is GET only.
|
|
643
|
+
function getTalentMeEndpoint(env = process.env) {
|
|
644
|
+
const explicit = readEnv(env, 'TALENT_ME_ENDPOINT');
|
|
645
|
+
if (explicit && explicit.trim()) return explicit.trim();
|
|
646
|
+
return deriveFromHub(env, 'works/talent/me');
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
function getOnboardingInterviewsEndpoint(env = process.env) {
|
|
650
|
+
const explicit = readEnv(env, 'ONBOARDING_INTERVIEWS_ENDPOINT');
|
|
651
|
+
if (explicit && explicit.trim()) return explicit.trim();
|
|
652
|
+
const base = getCertsBase(env);
|
|
653
|
+
return base ? `${base}/interviews` : null;
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
// Overwrite-repeat of the onboarding interview (certs, talent Bearer) — resets the
|
|
657
|
+
// taken interview in-place so it can be re-run.
|
|
658
|
+
function getOnboardingRestartEndpoint(env = process.env) {
|
|
659
|
+
const explicit = readEnv(env, 'ONBOARDING_RESTART_ENDPOINT');
|
|
660
|
+
if (explicit && explicit.trim()) return explicit.trim();
|
|
661
|
+
const base = getCertsBase(env);
|
|
662
|
+
return base ? `${base}/interviews/onboarding/restart` : null;
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
function getCompleteOnboardingEndpoint(env = process.env) {
|
|
666
|
+
const explicit = readEnv(env, 'COMPLETE_ONBOARDING_ENDPOINT');
|
|
667
|
+
if (explicit && explicit.trim()) return explicit.trim();
|
|
668
|
+
return deriveFromHub(env, 'works/talents/me/complete-onboarding');
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
function getOnboardingStatusEndpoint(env = process.env) {
|
|
672
|
+
const explicit = readEnv(env, 'ONBOARDING_STATUS_ENDPOINT');
|
|
673
|
+
if (explicit && explicit.trim()) return explicit.trim();
|
|
674
|
+
const base = getCertsBase(env);
|
|
675
|
+
return base ? `${base}/interviews/onboarding/status` : null;
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
// Talent profile web URL (evaluation-mcp, ADR-011 — the step-5 skip fallback).
|
|
679
|
+
function getTalentProfileUrl(env = process.env) {
|
|
680
|
+
const explicit = readEnv(env, 'PROFILE_URL');
|
|
681
|
+
if (explicit && explicit.trim()) return explicit.trim().replace(/\/+$/, '');
|
|
682
|
+
const fromFile = loadConfigFile(env).profileUrl;
|
|
683
|
+
if (fromFile && String(fromFile).trim()) return String(fromFile).trim().replace(/\/+$/, '');
|
|
684
|
+
const baked = getEnvProfile(env).profileUrl;
|
|
685
|
+
return baked ? baked.replace(/\/+$/, '') : null;
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
// The endpoint keys a flavor profile owns. config.json must hold only EXPLICIT
|
|
689
|
+
// user overrides of these, never the baked profile itself.
|
|
690
|
+
const PROFILE_ENDPOINT_KEYS = Object.keys(ENV_PROFILES[DEFAULT_ENV_PROFILE]);
|
|
691
|
+
|
|
692
|
+
// Boot migration (ADR-065 revised). The CLI used to SEED config.json with the
|
|
693
|
+
// baked bases on first run; the getters now fall back to getEnvProfile() directly,
|
|
694
|
+
// so seeding is unnecessary AND harmful: `npm uninstall` leaves the config dir, so
|
|
695
|
+
// a config.json seeded by a previous flavor (e.g. the private @shakers build → dev)
|
|
696
|
+
// survives into a new flavor (the public beta → staging) and pins the old endpoints
|
|
697
|
+
// forever, since config.json wins over the baked profile. This sweep removes any
|
|
698
|
+
// endpoint key whose value is byte-identical to that SAME field in a KNOWN profile
|
|
699
|
+
// OTHER than the baked one — i.e. a value that could only have gotten there by the
|
|
700
|
+
// old seeding of a different flavor. A value that matches no known profile is a
|
|
701
|
+
// genuine user override (set via `shakers config set`) and is left untouched. We
|
|
702
|
+
// never touch env, auth-session.json or consent.json, and never remove the dir.
|
|
703
|
+
function sanitizeStaleBakedEndpoints(env = process.env) {
|
|
704
|
+
const bakedName = getEnvProfileName(env);
|
|
705
|
+
const config = loadConfigFile(env);
|
|
706
|
+
let changed = false;
|
|
707
|
+
for (const key of PROFILE_ENDPOINT_KEYS) {
|
|
708
|
+
const current = config[key];
|
|
709
|
+
if (typeof current !== 'string' || !current.trim()) continue;
|
|
710
|
+
const normalized = current.trim().replace(/\/+$/, '');
|
|
711
|
+
const matchesForeignProfile = Object.entries(ENV_PROFILES).some(
|
|
712
|
+
([name, profile]) =>
|
|
713
|
+
name !== bakedName && String(profile[key]).replace(/\/+$/, '') === normalized,
|
|
714
|
+
);
|
|
715
|
+
if (matchesForeignProfile) {
|
|
716
|
+
delete config[key];
|
|
717
|
+
changed = true;
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
if (changed) {
|
|
721
|
+
try { saveConfigFile(config, env); } catch { return false; }
|
|
722
|
+
}
|
|
723
|
+
return changed;
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
function getEmailVerificationRequestUrl(env = process.env) {
|
|
727
|
+
return deriveEmailVerificationUrl(env, 'request');
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
function getEmailVerificationVerifyUrl(env = process.env) {
|
|
731
|
+
return deriveEmailVerificationUrl(env, 'verify');
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
function getSuperadminSessionEndpoint(env = process.env) {
|
|
735
|
+
return deriveFromIngest(env, 'superadmin/session');
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
// Superadmin READ-ONLY certification inspection endpoint (ADR-025, NON-PROD only) — a sibling of the ingest endpoint.
|
|
739
|
+
function getInspectCertificationsEndpoint(env = process.env) {
|
|
740
|
+
return deriveFromIngest(env, 'superadmin/inspect-certifications');
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
// ADR-027 superadmin SESSION persistence.
|
|
744
|
+
function loadSuperadminSession(env = process.env) {
|
|
745
|
+
const config = loadConfigFile(env);
|
|
746
|
+
const s = config && typeof config === 'object' ? config.superadminSession : null;
|
|
747
|
+
if (!s || typeof s !== 'object' || typeof s.token !== 'string' || !s.token) return null;
|
|
748
|
+
if (s.expiresAt && Date.parse(s.expiresAt) <= Date.now()) return null; // expired
|
|
749
|
+
return { email: s.email || null, token: s.token, expiresAt: s.expiresAt || null };
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
function saveSuperadminSession(session, env = process.env) {
|
|
753
|
+
const config = loadConfigFile(env);
|
|
754
|
+
config.superadminSession = {
|
|
755
|
+
email: session.email || null,
|
|
756
|
+
token: session.token,
|
|
757
|
+
expiresAt: session.expiresAt || null,
|
|
758
|
+
};
|
|
759
|
+
saveConfigFile(config, env);
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
function clearSuperadminSession(env = process.env) {
|
|
763
|
+
const config = loadConfigFile(env);
|
|
764
|
+
if (config && typeof config === 'object' && 'superadminSession' in config) {
|
|
765
|
+
delete config.superadminSession;
|
|
766
|
+
saveConfigFile(config, env);
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
module.exports = {
|
|
771
|
+
getIngestEndpoint,
|
|
772
|
+
getSynthesisEndpoint,
|
|
773
|
+
getRoadmapEndpoint,
|
|
774
|
+
getAgentEvaluationEndpoint,
|
|
775
|
+
getLoginEndpoint,
|
|
776
|
+
getAuthTokenEndpoint,
|
|
777
|
+
getCompleteRegistrationEndpoint,
|
|
778
|
+
getDeviceAuthorizeEndpoint,
|
|
779
|
+
getDeviceTokenEndpoint,
|
|
780
|
+
// Certification-by-dimension endpoints (`certify` = dimension LiveKit interview).
|
|
781
|
+
getMyCertificationsEndpoint,
|
|
782
|
+
// Role/cluster management (`add-role` / `change-role` / onboarding main-role step).
|
|
783
|
+
getAvailableRolesEndpoint,
|
|
784
|
+
getSetMainRoleEndpoint,
|
|
785
|
+
getAssignedClustersEndpoint,
|
|
786
|
+
getTemplatesByDimensionEndpoint,
|
|
787
|
+
getMyAssessmentsEndpoint,
|
|
788
|
+
getCertificationInterviewsEndpoint,
|
|
789
|
+
// Find Positions list (`find-projects` command / `find_projects` MCP tool).
|
|
790
|
+
getFindPositionsEndpoint,
|
|
791
|
+
// Marketplace (show-project / save-project / unsave-project / invitations).
|
|
792
|
+
getPositionDetailEndpoint,
|
|
793
|
+
getSavePositionEndpoint,
|
|
794
|
+
getUnsavePositionEndpoint,
|
|
795
|
+
getReceivedInvitationsEndpoint,
|
|
796
|
+
getMyCandidaturesEndpoint,
|
|
797
|
+
getMeAvailabilityEndpoint,
|
|
798
|
+
getMeProfileEndpoint,
|
|
799
|
+
getTalentMeProfileEndpoint,
|
|
800
|
+
// Alma assistant (`ask` command / `alma_chat` MCP tool) — separate service.
|
|
801
|
+
getAlmaBase,
|
|
802
|
+
getAlmaChatEndpoint,
|
|
803
|
+
getAlmaDecisionEndpoint,
|
|
804
|
+
getSkillsDeclareEndpoint,
|
|
805
|
+
getSkillsResolveAddableEndpoint,
|
|
806
|
+
getSkillsResolveMatchedEndpoint,
|
|
807
|
+
getUsageDiscoveredInventoryEndpoint,
|
|
808
|
+
getUsageReportEndpoint,
|
|
809
|
+
getAiProfileEndpoint,
|
|
810
|
+
// Evidence emission (talents-ai-score Phase 2, evidence-based evaluation).
|
|
811
|
+
getEvidenceCliScanEndpoint,
|
|
812
|
+
// Talent-portfolio endpoints (ADR-059).
|
|
813
|
+
getPortfoliosDeclareEndpoint,
|
|
814
|
+
getPortfoliosListEndpoint,
|
|
815
|
+
getPortfolioDraftDescriptionEndpoint,
|
|
816
|
+
getPortfolioUpdateEndpoint,
|
|
817
|
+
// Talent-agent endpoints (talents-ai-score Phase 2 — "Add agent to profile").
|
|
818
|
+
getAgentsDeclareEndpoint,
|
|
819
|
+
getAgentsListEndpoint,
|
|
820
|
+
getAgentsDraftFieldsEndpoint,
|
|
821
|
+
getAgentPortfoliosEndpoint,
|
|
822
|
+
// Onboarding / register flow (evaluation-mcp, ADR-011..015/024).
|
|
823
|
+
getCertsBase,
|
|
824
|
+
getHubBase,
|
|
825
|
+
sanitizeStaleBakedEndpoints,
|
|
826
|
+
getSignUpEndpoint,
|
|
827
|
+
getImportProfileEndpoint,
|
|
828
|
+
getMcpSignupEndpoint,
|
|
829
|
+
getMyImportStatusEndpoint,
|
|
830
|
+
getOnboardingEventsEndpoint,
|
|
831
|
+
getOneTimeTokenEndpoint,
|
|
832
|
+
getOneTimeLoginUrl,
|
|
833
|
+
getProfessionalDetailsEndpoint,
|
|
834
|
+
getPricingRateEndpoint,
|
|
835
|
+
getSetAvailabilityEndpoint,
|
|
836
|
+
getLanguagesEndpoint,
|
|
837
|
+
getLanguagesCatalogEndpoint,
|
|
838
|
+
getMeSocialEndpoint,
|
|
839
|
+
getMePortfoliosEndpoint,
|
|
840
|
+
getTranslationsEndpoint,
|
|
841
|
+
getTalentMeEndpoint,
|
|
842
|
+
getUserMeEndpoint,
|
|
843
|
+
getOnboardingInterviewsEndpoint,
|
|
844
|
+
getCompleteOnboardingEndpoint,
|
|
845
|
+
getOnboardingRestartEndpoint,
|
|
846
|
+
getOnboardingStatusEndpoint,
|
|
847
|
+
getTalentProfileUrl,
|
|
848
|
+
getEmailVerificationRequestUrl,
|
|
849
|
+
getEmailVerificationVerifyUrl,
|
|
850
|
+
getSuperadminSessionEndpoint,
|
|
851
|
+
getInspectCertificationsEndpoint,
|
|
852
|
+
// ADR-027 superadmin session persistence.
|
|
853
|
+
loadSuperadminSession,
|
|
854
|
+
saveSuperadminSession,
|
|
855
|
+
clearSuperadminSession,
|
|
856
|
+
// Persistent config file + endpoint safety (endpoint-config task).
|
|
857
|
+
configFilePath,
|
|
858
|
+
loadConfigFile,
|
|
859
|
+
saveConfigFile,
|
|
860
|
+
validateEndpoint,
|
|
861
|
+
isAllowedShakersDomain,
|
|
862
|
+
SHAKERS_CLI_DOMAINS,
|
|
863
|
+
setIngestEndpoint,
|
|
864
|
+
hasRetiredFallbackConfig,
|
|
865
|
+
resolveIngestEndpoint,
|
|
866
|
+
// Distribution profile (ADR-058 fase 1).
|
|
867
|
+
VALID_PROFILES,
|
|
868
|
+
getProfile,
|
|
869
|
+
isTalentProfile,
|
|
870
|
+
// UI language preference (env SHAKERS_CLI_LANG > config.json `lang`).
|
|
871
|
+
VALID_UI_LANGS,
|
|
872
|
+
getUiLang,
|
|
873
|
+
// Baked backend target flavor (staging | dev | local).
|
|
874
|
+
ENV_PROFILES,
|
|
875
|
+
getEnvProfileName,
|
|
876
|
+
getEnvProfile,
|
|
877
|
+
};
|