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
package/src/config.js
CHANGED
|
@@ -5,7 +5,7 @@ const path = require('path');
|
|
|
5
5
|
const { readEnv } = require('./env-paths');
|
|
6
6
|
const { getConfigDir } = require('./config-dir');
|
|
7
7
|
|
|
8
|
-
// Persistent CLI config file
|
|
8
|
+
// Persistent CLI config file.
|
|
9
9
|
function configDir(env = process.env) {
|
|
10
10
|
return getConfigDir(env);
|
|
11
11
|
}
|
|
@@ -23,7 +23,7 @@ function loadConfigFile(env = process.env) {
|
|
|
23
23
|
}
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
-
// DISTRIBUTION PROFILE
|
|
26
|
+
// DISTRIBUTION PROFILE.
|
|
27
27
|
const VALID_PROFILES = new Set(['external', 'talent']);
|
|
28
28
|
const PROFILE_DEFAULT_WHEN_ABSENT = 'talent';
|
|
29
29
|
|
|
@@ -48,7 +48,7 @@ function getUiLang(env = process.env) {
|
|
|
48
48
|
return null;
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
-
// config.json holds the superadmin session token
|
|
51
|
+
// config.json holds the superadmin session token and consent/ endpoint data, so it must never be briefly world/group-readable.
|
|
52
52
|
function saveConfigFile(config, env = process.env) {
|
|
53
53
|
const dir = configDir(env);
|
|
54
54
|
fs.mkdirSync(dir, { recursive: true });
|
|
@@ -56,7 +56,7 @@ function saveConfigFile(config, env = process.env) {
|
|
|
56
56
|
try { fs.chmodSync(configFilePath(env), 0o600); } catch { /* e.g. Windows */ }
|
|
57
57
|
}
|
|
58
58
|
|
|
59
|
-
// Endpoint safety
|
|
59
|
+
// Endpoint safety — BOUNDED.
|
|
60
60
|
const LOCAL_HOSTS = new Set(['localhost', '127.0.0.1', '::1']);
|
|
61
61
|
const SHAKERS_CLI_DOMAINS = [];
|
|
62
62
|
|
|
@@ -120,7 +120,7 @@ function resolveIngestEndpoint(env = process.env) {
|
|
|
120
120
|
return { endpoint: null, source: 'none' };
|
|
121
121
|
}
|
|
122
122
|
|
|
123
|
-
// Baked backend targets per distribution flavor
|
|
123
|
+
// Baked backend targets per distribution flavor.
|
|
124
124
|
// Only the DEFAULT bases; per-endpoint SHAKERS_CLI_* env and config.json overrides
|
|
125
125
|
// still win at each getter (env > config.json > baked profile).
|
|
126
126
|
const ENV_PROFILES = {
|
|
@@ -130,41 +130,86 @@ const ENV_PROFILES = {
|
|
|
130
130
|
profileUrl: 'https://app.shakersworks.com/login',
|
|
131
131
|
almaBase: 'https://app.shakersworks.com',
|
|
132
132
|
},
|
|
133
|
+
dev: {
|
|
134
|
+
certsBase: 'https://api.dev.certifications.shakersworks.com/api/v1',
|
|
135
|
+
hubBase: 'https://api.hub.dev.shakersworks.com/api/v1',
|
|
136
|
+
profileUrl: 'https://works.dev.shakersworks.com/login',
|
|
137
|
+
almaBase: 'https://works.dev.shakersworks.com',
|
|
138
|
+
},
|
|
139
|
+
local: {
|
|
140
|
+
certsBase: 'http://localhost:3004/api/v1',
|
|
141
|
+
hubBase: 'http://localhost:3001/api/v1',
|
|
142
|
+
profileUrl: 'http://localhost:8081/login',
|
|
143
|
+
},
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
const RETIRED_ENV_PROFILES = {
|
|
133
147
|
staging: {
|
|
134
148
|
certsBase: 'https://api.staging.certifications.shakersworks.com/api/v1',
|
|
135
149
|
hubBase: 'https://api.hub.staging.shakersworks.com/api/v1',
|
|
136
150
|
profileUrl: 'https://works.staging.shakersworks.com/login',
|
|
137
151
|
almaBase: 'https://works.staging.shakersworks.com',
|
|
138
152
|
},
|
|
139
|
-
dev: {
|
|
140
|
-
certsBase: 'https://api.dev.certifications.shakersworks.com/api/v1',
|
|
153
|
+
'dev-new-works': {
|
|
141
154
|
hubBase: 'https://api.hub.new-works.dev.shakersworks.com/api/v1',
|
|
142
155
|
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',
|
|
156
|
+
almaBase: 'https://works.new-works.dev.shakersworks.com',
|
|
148
157
|
},
|
|
149
158
|
};
|
|
150
159
|
|
|
151
160
|
// Which flavor is baked: SHAKERS_CLI_ENV override wins, else derived from the
|
|
152
|
-
// published package name (
|
|
153
|
-
const PACKAGE_NAME_ENV = { 'shakers-cli': 'prod', 'shakers-cli-beta': '
|
|
161
|
+
// published package name (shakers-cli -> prod, shakers-cli-beta -> dev), else dev (safe default).
|
|
162
|
+
const PACKAGE_NAME_ENV = { 'shakers-cli': 'prod', 'shakers-cli-beta': 'dev' };
|
|
154
163
|
const DEFAULT_ENV_PROFILE = 'dev';
|
|
155
164
|
|
|
156
|
-
function
|
|
165
|
+
function envProfileForPackageName(name) {
|
|
166
|
+
return PACKAGE_NAME_ENV[name] || DEFAULT_ENV_PROFILE;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function readPackageJson() {
|
|
157
170
|
try {
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
171
|
+
return require('../package.json');
|
|
172
|
+
} catch {
|
|
173
|
+
return {};
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function bakedEnvProfileName() {
|
|
178
|
+
return envProfileForPackageName(readPackageJson().name);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function envOverrideVariable(env) {
|
|
182
|
+
const next = env.SHAKERS_CLI_ENV;
|
|
183
|
+
return next !== undefined && next !== '' ? 'SHAKERS_CLI_ENV' : 'AI_FOOTPRINT_ENV';
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
class UnknownEnvProfileError extends Error {
|
|
187
|
+
constructor(variable, value) {
|
|
188
|
+
const known = Object.keys(ENV_PROFILES).join(', ');
|
|
189
|
+
super(RETIRED_ENV_PROFILES[value]
|
|
190
|
+
? `${variable}=${value} is a retired profile. Use one of ${known}, or unset it.`
|
|
191
|
+
: `${variable}=${value} is not a known profile. Use one of ${known}, or unset it.`);
|
|
192
|
+
this.name = 'UnknownEnvProfileError';
|
|
193
|
+
this.variable = variable;
|
|
194
|
+
this.value = value;
|
|
195
|
+
}
|
|
162
196
|
}
|
|
163
197
|
|
|
164
198
|
function getEnvProfileName(env = process.env) {
|
|
165
|
-
const override = readEnv(env, 'ENV');
|
|
166
|
-
if (override
|
|
167
|
-
|
|
199
|
+
const override = String(readEnv(env, 'ENV') || '').trim();
|
|
200
|
+
if (!override) return bakedEnvProfileName();
|
|
201
|
+
if (ENV_PROFILES[override]) return override;
|
|
202
|
+
throw new UnknownEnvProfileError(envOverrideVariable(env), override);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function envProfileProblem(env = process.env) {
|
|
206
|
+
try {
|
|
207
|
+
getEnvProfileName(env);
|
|
208
|
+
return null;
|
|
209
|
+
} catch (e) {
|
|
210
|
+
if (e instanceof UnknownEnvProfileError) return e.message;
|
|
211
|
+
throw e;
|
|
212
|
+
}
|
|
168
213
|
}
|
|
169
214
|
|
|
170
215
|
function getEnvProfile(env = process.env) {
|
|
@@ -205,7 +250,7 @@ function getCertsBase(env = process.env) {
|
|
|
205
250
|
if (ingest) return ingest.replace(/\/usage\/reports\/?$/, '').replace(/\/+$/, '');
|
|
206
251
|
// No explicit override anywhere -> the baked flavor profile (never null when a
|
|
207
252
|
// profile exists), so switching the published flavor "just works" without a
|
|
208
|
-
// seeded config.json
|
|
253
|
+
// seeded config.json.
|
|
209
254
|
const baked = getEnvProfile(env).certsBase;
|
|
210
255
|
return baked ? baked.replace(/\/+$/, '') : null;
|
|
211
256
|
}
|
|
@@ -217,11 +262,11 @@ function getIngestEndpoint(env = process.env) {
|
|
|
217
262
|
return base ? `${base}/usage/reports` : null;
|
|
218
263
|
}
|
|
219
264
|
|
|
220
|
-
// Agent-synthesis endpoint
|
|
265
|
+
// Agent-synthesis endpoint.
|
|
221
266
|
function getSynthesisEndpoint(env = process.env) {
|
|
222
267
|
const value = readEnv(env, 'SYNTHESIS_ENDPOINT');
|
|
223
268
|
if (value && value.trim()) return value.trim();
|
|
224
|
-
//
|
|
269
|
+
// Derived as an ingest sibling when the override is unset.
|
|
225
270
|
return deriveFromIngest(env, 'agent-synthesis');
|
|
226
271
|
}
|
|
227
272
|
|
|
@@ -231,7 +276,7 @@ function getRoadmapEndpoint(env = process.env) {
|
|
|
231
276
|
return deriveFromIngest(env, 'roadmap-personalize');
|
|
232
277
|
}
|
|
233
278
|
|
|
234
|
-
// Email-verification endpoints
|
|
279
|
+
// Email-verification endpoints: the OTP "prove you own this email" step that gates PERSISTENCE. Sibling-of-ingest resolution (`new URL(relative, base)`); unset ingest -> null.
|
|
235
280
|
function deriveFromIngest(env, relativePath) {
|
|
236
281
|
const ingest = getIngestEndpoint(env);
|
|
237
282
|
if (!ingest) return null;
|
|
@@ -254,7 +299,7 @@ function getAgentEvaluationEndpoint(env = process.env) {
|
|
|
254
299
|
return deriveFromIngest(env, 'agent-evaluation');
|
|
255
300
|
}
|
|
256
301
|
|
|
257
|
-
// Login endpoint — better-auth email sign-in
|
|
302
|
+
// Login endpoint — better-auth email sign-in: `POST {hubBase}/auth/sign-in/email` with `{email,password}`.
|
|
258
303
|
function getLoginEndpoint(env = process.env) {
|
|
259
304
|
const explicit = readEnv(env, 'LOGIN_ENDPOINT');
|
|
260
305
|
if (explicit && explicit.trim()) return explicit.trim();
|
|
@@ -262,6 +307,10 @@ function getLoginEndpoint(env = process.env) {
|
|
|
262
307
|
}
|
|
263
308
|
|
|
264
309
|
// better-auth JWT endpoint: `GET {hubBase}/auth/token` with the session cookie → `{token}` (ES256/JWKS, ~15-min TTL).
|
|
310
|
+
function getSignOutEndpoint(env = process.env) {
|
|
311
|
+
return deriveFromHub(env, 'auth/sign-out');
|
|
312
|
+
}
|
|
313
|
+
|
|
265
314
|
function getAuthTokenEndpoint(env = process.env) {
|
|
266
315
|
const explicit = readEnv(env, 'AUTH_TOKEN_ENDPOINT');
|
|
267
316
|
if (explicit && explicit.trim()) return explicit.trim();
|
|
@@ -462,14 +511,14 @@ function getAiProfileEndpoint(env = process.env) {
|
|
|
462
511
|
return deriveFromHub(env, 'works/talent/me/ai-profile');
|
|
463
512
|
}
|
|
464
513
|
|
|
465
|
-
// Evidence emission endpoint for the `cli_scan` instrument
|
|
514
|
+
// Evidence emission endpoint for the `cli_scan` instrument.
|
|
466
515
|
function getEvidenceCliScanEndpoint(env = process.env) {
|
|
467
516
|
const explicit = readEnv(env, 'EVIDENCE_CLI_SCAN_ENDPOINT');
|
|
468
517
|
if (explicit && explicit.trim()) return explicit.trim();
|
|
469
518
|
return deriveFromIngest(env, '../evidence/cli-scan');
|
|
470
519
|
}
|
|
471
520
|
|
|
472
|
-
// Talent-portfolio endpoints (
|
|
521
|
+
// Talent-portfolio endpoints (`start`'s "Add project to portfolio" route, talent-only).
|
|
473
522
|
function getPortfoliosDeclareEndpoint(env = process.env) {
|
|
474
523
|
const explicit = readEnv(env, 'PORTFOLIOS_DECLARE_ENDPOINT');
|
|
475
524
|
if (explicit && explicit.trim()) return explicit.trim();
|
|
@@ -512,7 +561,7 @@ function getAgentsDraftFieldsEndpoint(env = process.env) {
|
|
|
512
561
|
return deriveFromIngest(env, '../works/talents/me/agents/draft-fields');
|
|
513
562
|
}
|
|
514
563
|
|
|
515
|
-
// Agent<->experience relate endpoint (
|
|
564
|
+
// Agent<->experience relate endpoint (`start`'s "Add agent" relate step / MCP `add_agent.relatedPortfolios`).
|
|
516
565
|
function getAgentPortfoliosEndpoint(agentId, env = process.env) {
|
|
517
566
|
// Null on missing/non-scalar id: no bogus URL, and keeps config.test.js's `fn(env)`
|
|
518
567
|
// getter sweep honest when the env object lands in `agentId`.
|
|
@@ -520,7 +569,7 @@ function getAgentPortfoliosEndpoint(agentId, env = process.env) {
|
|
|
520
569
|
return deriveFromHub(env, `works/talent/me/agents/${encodeURIComponent(agentId)}/portfolios`);
|
|
521
570
|
}
|
|
522
571
|
|
|
523
|
-
// Onboarding endpoints (
|
|
572
|
+
// Onboarding endpoints (the register flow).
|
|
524
573
|
function getHubBase(env = process.env) {
|
|
525
574
|
const explicit = readEnv(env, 'HUB_BASE');
|
|
526
575
|
if (explicit && explicit.trim()) return explicit.trim().replace(/\/+$/, '');
|
|
@@ -551,7 +600,7 @@ function getImportProfileEndpoint(env = process.env) {
|
|
|
551
600
|
return deriveFromHub(env, 'works/talent/me/import-profile');
|
|
552
601
|
}
|
|
553
602
|
|
|
554
|
-
// MCP sign-up
|
|
603
|
+
// MCP sign-up: public UNREGISTERED + claim code, its import status, and the one-time web login.
|
|
555
604
|
function getMcpSignupEndpoint(env = process.env) {
|
|
556
605
|
return deriveFromHub(env, 'works-ai/talents/mcp-signup');
|
|
557
606
|
}
|
|
@@ -564,11 +613,20 @@ function getOnboardingEventsEndpoint(env = process.env) {
|
|
|
564
613
|
return deriveFromHub(env, 'works/talent/me/onboarding-events');
|
|
565
614
|
}
|
|
566
615
|
|
|
616
|
+
const AMPLITUDE_HTTP_API = 'https://api.eu.amplitude.com/2/httpapi';
|
|
617
|
+
|
|
618
|
+
function getAmplitudeIngestion(env = process.env) {
|
|
619
|
+
const stamped = readPackageJson().amplitude;
|
|
620
|
+
if (!stamped || typeof stamped.apiKey !== 'string' || !stamped.apiKey.trim()) return null;
|
|
621
|
+
if (stamped.env !== getEnvProfileName(env)) return null;
|
|
622
|
+
return { endpoint: AMPLITUDE_HTTP_API, apiKey: stamped.apiKey.trim() };
|
|
623
|
+
}
|
|
624
|
+
|
|
567
625
|
function getOneTimeTokenEndpoint(env = process.env) {
|
|
568
626
|
return deriveFromHub(env, 'auth/one-time-token/generate');
|
|
569
627
|
}
|
|
570
628
|
|
|
571
|
-
// The ONE place the web route that redeems a one-time token is spelled
|
|
629
|
+
// The ONE place the web route that redeems a one-time token is spelled.
|
|
572
630
|
function getOneTimeLoginUrl(token, env = process.env) {
|
|
573
631
|
if (typeof token !== 'string' || !token) return null;
|
|
574
632
|
const profile = getTalentProfileUrl(env);
|
|
@@ -675,7 +733,7 @@ function getOnboardingStatusEndpoint(env = process.env) {
|
|
|
675
733
|
return base ? `${base}/interviews/onboarding/status` : null;
|
|
676
734
|
}
|
|
677
735
|
|
|
678
|
-
// Talent profile web URL (
|
|
736
|
+
// Talent profile web URL (the step-5 skip fallback).
|
|
679
737
|
function getTalentProfileUrl(env = process.env) {
|
|
680
738
|
const explicit = readEnv(env, 'PROFILE_URL');
|
|
681
739
|
if (explicit && explicit.trim()) return explicit.trim().replace(/\/+$/, '');
|
|
@@ -689,14 +747,14 @@ function getTalentProfileUrl(env = process.env) {
|
|
|
689
747
|
// user overrides of these, never the baked profile itself.
|
|
690
748
|
const PROFILE_ENDPOINT_KEYS = Object.keys(ENV_PROFILES[DEFAULT_ENV_PROFILE]);
|
|
691
749
|
|
|
692
|
-
// Boot migration
|
|
750
|
+
// Boot migration. The CLI used to SEED config.json with the
|
|
693
751
|
// baked bases on first run; the getters now fall back to getEnvProfile() directly,
|
|
694
752
|
// 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
|
|
696
|
-
// survives into a new flavor (the
|
|
753
|
+
// a config.json seeded by a previous flavor (e.g. the beta when it baked staging)
|
|
754
|
+
// survives into a new flavor (the beta now baking dev) and pins the old endpoints
|
|
697
755
|
// forever, since config.json wins over the baked profile. This sweep removes any
|
|
698
756
|
// 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
|
|
757
|
+
// (current or retired) OTHER than the baked one — i.e. a value that could only have gotten there by the
|
|
700
758
|
// old seeding of a different flavor. A value that matches no known profile is a
|
|
701
759
|
// genuine user override (set via `shakers config set`) and is left untouched. We
|
|
702
760
|
// never touch env, auth-session.json or consent.json, and never remove the dir.
|
|
@@ -708,7 +766,7 @@ function sanitizeStaleBakedEndpoints(env = process.env) {
|
|
|
708
766
|
const current = config[key];
|
|
709
767
|
if (typeof current !== 'string' || !current.trim()) continue;
|
|
710
768
|
const normalized = current.trim().replace(/\/+$/, '');
|
|
711
|
-
const matchesForeignProfile = Object.entries(ENV_PROFILES).some(
|
|
769
|
+
const matchesForeignProfile = Object.entries({ ...RETIRED_ENV_PROFILES, ...ENV_PROFILES }).some(
|
|
712
770
|
([name, profile]) =>
|
|
713
771
|
name !== bakedName && String(profile[key]).replace(/\/+$/, '') === normalized,
|
|
714
772
|
);
|
|
@@ -735,12 +793,12 @@ function getSuperadminSessionEndpoint(env = process.env) {
|
|
|
735
793
|
return deriveFromIngest(env, 'superadmin/session');
|
|
736
794
|
}
|
|
737
795
|
|
|
738
|
-
// Superadmin READ-ONLY certification inspection endpoint (
|
|
796
|
+
// Superadmin READ-ONLY certification inspection endpoint (NON-PROD only) — a sibling of the ingest endpoint.
|
|
739
797
|
function getInspectCertificationsEndpoint(env = process.env) {
|
|
740
798
|
return deriveFromIngest(env, 'superadmin/inspect-certifications');
|
|
741
799
|
}
|
|
742
800
|
|
|
743
|
-
//
|
|
801
|
+
// Superadmin SESSION persistence.
|
|
744
802
|
function loadSuperadminSession(env = process.env) {
|
|
745
803
|
const config = loadConfigFile(env);
|
|
746
804
|
const s = config && typeof config === 'object' ? config.superadminSession : null;
|
|
@@ -774,6 +832,7 @@ module.exports = {
|
|
|
774
832
|
getAgentEvaluationEndpoint,
|
|
775
833
|
getLoginEndpoint,
|
|
776
834
|
getAuthTokenEndpoint,
|
|
835
|
+
getSignOutEndpoint,
|
|
777
836
|
getCompleteRegistrationEndpoint,
|
|
778
837
|
getDeviceAuthorizeEndpoint,
|
|
779
838
|
getDeviceTokenEndpoint,
|
|
@@ -807,19 +866,19 @@ module.exports = {
|
|
|
807
866
|
getUsageDiscoveredInventoryEndpoint,
|
|
808
867
|
getUsageReportEndpoint,
|
|
809
868
|
getAiProfileEndpoint,
|
|
810
|
-
// Evidence emission
|
|
869
|
+
// Evidence emission.
|
|
811
870
|
getEvidenceCliScanEndpoint,
|
|
812
|
-
// Talent-portfolio endpoints
|
|
871
|
+
// Talent-portfolio endpoints.
|
|
813
872
|
getPortfoliosDeclareEndpoint,
|
|
814
873
|
getPortfoliosListEndpoint,
|
|
815
874
|
getPortfolioDraftDescriptionEndpoint,
|
|
816
875
|
getPortfolioUpdateEndpoint,
|
|
817
|
-
// Talent-agent endpoints (
|
|
876
|
+
// Talent-agent endpoints ("Add agent to profile").
|
|
818
877
|
getAgentsDeclareEndpoint,
|
|
819
878
|
getAgentsListEndpoint,
|
|
820
879
|
getAgentsDraftFieldsEndpoint,
|
|
821
880
|
getAgentPortfoliosEndpoint,
|
|
822
|
-
// Onboarding / register flow
|
|
881
|
+
// Onboarding / register flow.
|
|
823
882
|
getCertsBase,
|
|
824
883
|
getHubBase,
|
|
825
884
|
sanitizeStaleBakedEndpoints,
|
|
@@ -828,6 +887,8 @@ module.exports = {
|
|
|
828
887
|
getMcpSignupEndpoint,
|
|
829
888
|
getMyImportStatusEndpoint,
|
|
830
889
|
getOnboardingEventsEndpoint,
|
|
890
|
+
getAmplitudeIngestion,
|
|
891
|
+
AMPLITUDE_HTTP_API,
|
|
831
892
|
getOneTimeTokenEndpoint,
|
|
832
893
|
getOneTimeLoginUrl,
|
|
833
894
|
getProfessionalDetailsEndpoint,
|
|
@@ -849,7 +910,7 @@ module.exports = {
|
|
|
849
910
|
getEmailVerificationVerifyUrl,
|
|
850
911
|
getSuperadminSessionEndpoint,
|
|
851
912
|
getInspectCertificationsEndpoint,
|
|
852
|
-
//
|
|
913
|
+
// Superadmin session persistence.
|
|
853
914
|
loadSuperadminSession,
|
|
854
915
|
saveSuperadminSession,
|
|
855
916
|
clearSuperadminSession,
|
|
@@ -863,15 +924,18 @@ module.exports = {
|
|
|
863
924
|
setIngestEndpoint,
|
|
864
925
|
hasRetiredFallbackConfig,
|
|
865
926
|
resolveIngestEndpoint,
|
|
866
|
-
// Distribution profile
|
|
927
|
+
// Distribution profile.
|
|
867
928
|
VALID_PROFILES,
|
|
868
929
|
getProfile,
|
|
869
930
|
isTalentProfile,
|
|
870
931
|
// UI language preference (env SHAKERS_CLI_LANG > config.json `lang`).
|
|
871
932
|
VALID_UI_LANGS,
|
|
872
933
|
getUiLang,
|
|
873
|
-
// Baked backend target flavor (
|
|
934
|
+
// Baked backend target flavor (prod | dev | local).
|
|
874
935
|
ENV_PROFILES,
|
|
875
936
|
getEnvProfileName,
|
|
876
937
|
getEnvProfile,
|
|
938
|
+
envProfileForPackageName,
|
|
939
|
+
envProfileProblem,
|
|
940
|
+
UnknownEnvProfileError,
|
|
877
941
|
};
|
package/src/consent-flow.js
CHANGED
|
@@ -8,7 +8,7 @@ const {
|
|
|
8
8
|
const { runEmailVerification } = require('./email-verification');
|
|
9
9
|
const { renderLegalNotice } = require('./legal-notice');
|
|
10
10
|
|
|
11
|
-
// Consent prompt (
|
|
11
|
+
// Consent prompt (replaces the earlier disclosure wall; its ordering is revised in turn, see below).
|
|
12
12
|
|
|
13
13
|
const MAX_ATTEMPTS = 5;
|
|
14
14
|
|
|
@@ -45,7 +45,7 @@ async function defaultVerifyEmail({ email, ask, catalog, env = process.env }) {
|
|
|
45
45
|
});
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
-
// Runs the short consent-to-persist prompt + (if accepted) email collection +
|
|
48
|
+
// Runs the short consent-to-persist prompt + (if accepted) email collection + EMAIL-OWNERSHIP VERIFICATION.
|
|
49
49
|
async function runConsentPrompt({
|
|
50
50
|
ask,
|
|
51
51
|
catalog,
|
|
@@ -57,7 +57,7 @@ async function runConsentPrompt({
|
|
|
57
57
|
profile = 'talent',
|
|
58
58
|
}) {
|
|
59
59
|
const c = catalog.consent;
|
|
60
|
-
// Presentation only
|
|
60
|
+
// Presentation only: framed by src/legal-notice.js on a
|
|
61
61
|
// real TTY, byte-identical (no heading, no rule) otherwise.
|
|
62
62
|
process.stdout.write(`\n ${renderLegalNotice(c.persistIntro, catalog)}\n`);
|
|
63
63
|
|
|
@@ -100,7 +100,7 @@ async function runConsentPrompt({
|
|
|
100
100
|
|
|
101
101
|
process.stdout.write(` ${c.savedUnderEmail(email)}\n`);
|
|
102
102
|
|
|
103
|
-
// Prove email ownership BEFORE persisting
|
|
103
|
+
// Prove email ownership BEFORE persisting — every profile, external included.
|
|
104
104
|
const verification = await verifyEmail({ email, ask, catalog, env });
|
|
105
105
|
if (verification && verification.verified) {
|
|
106
106
|
recordConsent('granted', email, { verified: true });
|
package/src/consent-skip.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
// Explains WHY the consent-to-persist prompt is skipped (or might not complete) this run, instead of doing so silently
|
|
3
|
+
// Explains WHY the consent-to-persist prompt is skipped (or might not complete) this run, instead of doing so silently.
|
|
4
4
|
function computeConsentSkip({ decision, emailVerified, stdinIsTTY, consentFilePath, catalog } = {}) {
|
|
5
5
|
const c = catalog && catalog.consent;
|
|
6
6
|
|
|
@@ -5,7 +5,7 @@ const { detectTechnologyManifests } = require('./tech-detector');
|
|
|
5
5
|
const { agentSourceFilesByName } = require('./agent-org-chart');
|
|
6
6
|
const { classifyVendored, collectProvenance } = require('./detection-noise');
|
|
7
7
|
|
|
8
|
-
// Per-CANDIDATE authorship + depth
|
|
8
|
+
// Per-CANDIDATE authorship + depth.
|
|
9
9
|
|
|
10
10
|
const SKILL_DEPTH_SATURATION = 3;
|
|
11
11
|
const AGENT_DEPTH_SATURATION = 8;
|
|
@@ -38,7 +38,7 @@ function recencyDaysForPaths(relPaths, authorship, nowSeconds) {
|
|
|
38
38
|
return Math.max(0, Math.floor((nowSeconds - mostRecent) / 86400));
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
-
// Attributes a candidate's evidence files against the talent's own email via the
|
|
41
|
+
// Attributes a candidate's evidence files against the talent's own email via the authorship gate.
|
|
42
42
|
function attributionCounts(relPaths, verifiedEmail, authorship) {
|
|
43
43
|
if (!Array.isArray(relPaths) || relPaths.length === 0) {
|
|
44
44
|
return { authoredFileCount: 0, totalFileCount: 0, authorshipKnown: false };
|
package/src/detection-noise.js
CHANGED
|
@@ -4,7 +4,7 @@ const { execFileSync } = require('child_process');
|
|
|
4
4
|
const fs = require('fs');
|
|
5
5
|
const path = require('path');
|
|
6
6
|
|
|
7
|
-
// Anti-noise CLASSIFICATION for detected candidates
|
|
7
|
+
// Anti-noise CLASSIFICATION for detected candidates.
|
|
8
8
|
|
|
9
9
|
const GIT_TIMEOUT_MS = 15000;
|
|
10
10
|
const README_MAX_BYTES = 8 * 1024;
|
package/src/detectors.js
CHANGED
package/src/device-login.js
CHANGED
|
@@ -95,7 +95,7 @@ async function runDeviceLogin(
|
|
|
95
95
|
timeoutMs,
|
|
96
96
|
});
|
|
97
97
|
if (!fin.ok) return { ok: false, reason: fin.reason };
|
|
98
|
-
return { ok: true, token: fin.token, sessionToken: res.accessToken, isNewUser: res.isNewUser, email: res.email || null };
|
|
98
|
+
return { ok: true, token: fin.token, sessionToken: res.accessToken, sessionExpiresIn: res.expiresIn, isNewUser: res.isNewUser, email: res.email || null };
|
|
99
99
|
}
|
|
100
100
|
if (res.status === 'pending') continue;
|
|
101
101
|
if (res.status === 'slow_down') {
|
|
@@ -108,7 +108,18 @@ async function runDeviceLogin(
|
|
|
108
108
|
}
|
|
109
109
|
}
|
|
110
110
|
|
|
111
|
+
function deviceSessionRecord({ token, sessionToken = null, sessionExpiresIn = null, email = null }, now = Date.now()) {
|
|
112
|
+
return {
|
|
113
|
+
accessToken: token,
|
|
114
|
+
hubAccessToken: token,
|
|
115
|
+
sessionToken: sessionToken || null,
|
|
116
|
+
sessionExpiresAt: sessionExpiresIn ? new Date(now + sessionExpiresIn * 1000).toISOString() : null,
|
|
117
|
+
email: email || null,
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
|
|
111
121
|
module.exports = {
|
|
122
|
+
deviceSessionRecord,
|
|
112
123
|
startDeviceLogin,
|
|
113
124
|
pollDeviceToken,
|
|
114
125
|
finalizeDeviceSession,
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
const { requestBackend } = require('./backend-request');
|
|
4
4
|
|
|
5
|
-
// Email-verification client + wait-mode loop
|
|
5
|
+
// Email-verification client + wait-mode loop.
|
|
6
6
|
|
|
7
7
|
const DEFAULT_TIMEOUT_MS = 15000;
|
|
8
8
|
// Bounded so a Talent who can never produce a matching code can't loop forever — after this many CODE-ENTRY attempts we give up (nothing persisted, asked again next run).
|
|
@@ -71,7 +71,7 @@ function classifyVerifyReason(reason) {
|
|
|
71
71
|
}
|
|
72
72
|
|
|
73
73
|
// The interactive "modo espera": send a code, then loop reading the pasted code until it verifies, the Talent cancels, or attempts run out.
|
|
74
|
-
//
|
|
74
|
+
// `requestUrl`/`verifyUrl` address the ONE backend (the certifications service).
|
|
75
75
|
async function runEmailVerification({
|
|
76
76
|
email,
|
|
77
77
|
ask,
|
|
@@ -103,7 +103,7 @@ async function runEmailVerification({
|
|
|
103
103
|
write(` ${v.waitHint}\n`);
|
|
104
104
|
|
|
105
105
|
for (let attempt = 0; attempt < MAX_CODE_ATTEMPTS; ) {
|
|
106
|
-
// The OTP is a secret too
|
|
106
|
+
// The OTP is a secret too: same reader, same echo, and the fix is the same call.
|
|
107
107
|
const readCode = typeof ask.secret === 'function' ? ask.secret.bind(ask) : ask;
|
|
108
108
|
const raw = String(await readCode(v.codePrompt)).trim();
|
|
109
109
|
|
package/src/env-paths.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
const os = require('os');
|
|
4
4
|
|
|
5
|
-
// Shared home-directory + env resolution (
|
|
5
|
+
// Shared home-directory + env resolution (tier scope = project ∪ home, applied uniformly to every category).
|
|
6
6
|
function readEnv(env, suffix) {
|
|
7
7
|
const next = env[`SHAKERS_CLI_${suffix}`];
|
|
8
8
|
if (next !== undefined && next !== '') return next;
|
|
@@ -136,6 +136,17 @@ async function countOpenPositions(deps = {}, { hubAccessToken, clusterId, timeou
|
|
|
136
136
|
return total === null ? { ok: false, reason: 'no-total' } : { ok: true, total };
|
|
137
137
|
}
|
|
138
138
|
|
|
139
|
+
// `--recommended` is a CLIENT-SIDE filter over the fetched page (the hub has no
|
|
140
|
+
// server-side "recommended" criterion — `match` is output-only). Recommended =
|
|
141
|
+
// items that have a match. If none on this page, fall back to the available
|
|
142
|
+
// items so the user is never left with an empty screen.
|
|
143
|
+
function splitRecommended(items, recommended) {
|
|
144
|
+
if (!recommended) return { shown: items, mode: null };
|
|
145
|
+
const rec = items.filter((it) => it.match != null);
|
|
146
|
+
if (rec.length > 0) return { shown: rec, mode: 'recommended' };
|
|
147
|
+
return { shown: items, mode: 'fallback' };
|
|
148
|
+
}
|
|
149
|
+
|
|
139
150
|
module.exports = {
|
|
140
151
|
TABS,
|
|
141
152
|
countOpenPositions,
|
|
@@ -145,4 +156,5 @@ module.exports = {
|
|
|
145
156
|
normalizeItem,
|
|
146
157
|
requestFindProjects,
|
|
147
158
|
fetchFindProjects,
|
|
159
|
+
splitRecommended,
|
|
148
160
|
};
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// `find-projects` flow: list the Positions/Projects available to the talent,
|
|
4
4
|
// paginated and filtered server-side via the endpoint's `criteria`. Read-only.
|
|
5
5
|
|
|
6
|
-
const { TABS, ATTENDANCE, DEFAULT_LIMIT } = require('./find-projects-client');
|
|
6
|
+
const { TABS, ATTENDANCE, DEFAULT_LIMIT, splitRecommended } = require('./find-projects-client');
|
|
7
7
|
const { styleLabelPrefix } = require('./ansi');
|
|
8
8
|
|
|
9
9
|
function makeFindProjectsDeps(overrides = {}) {
|
|
@@ -30,17 +30,6 @@ function resolveQuery(opts = {}) {
|
|
|
30
30
|
return { tab, limit, page, offset, attendance, country, attendanceInvalid, countryInvalid, recommended: !!opts.recommended };
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
-
// `--recommended` is a CLIENT-SIDE filter over the fetched page (the hub has no
|
|
34
|
-
// server-side "recommended" criterion — `match` is output-only). Recommended =
|
|
35
|
-
// items that have a match. If none on this page, fall back to the available
|
|
36
|
-
// items so the user is never left with an empty screen.
|
|
37
|
-
function splitRecommended(items, recommended) {
|
|
38
|
-
if (!recommended) return { shown: items, mode: null };
|
|
39
|
-
const rec = items.filter((it) => it.match != null);
|
|
40
|
-
if (rec.length > 0) return { shown: rec, mode: 'recommended' };
|
|
41
|
-
return { shown: items, mode: 'fallback' };
|
|
42
|
-
}
|
|
43
|
-
|
|
44
33
|
function budgetLine(item, fp) {
|
|
45
34
|
if (item.budget && item.budget.display) return item.budget.display;
|
|
46
35
|
if (item.budget && (item.budget.from != null || item.budget.to != null)) {
|
|
@@ -157,7 +146,6 @@ module.exports = {
|
|
|
157
146
|
makeFindProjectsDeps,
|
|
158
147
|
runFindProjects,
|
|
159
148
|
resolveQuery,
|
|
160
|
-
splitRecommended,
|
|
161
149
|
formatItem,
|
|
162
150
|
paginationFooter,
|
|
163
151
|
recommendedFooter,
|
package/src/git-activity.js
CHANGED
|
@@ -55,7 +55,7 @@ function round2(n) {
|
|
|
55
55
|
return Math.round(n * 100) / 100;
|
|
56
56
|
}
|
|
57
57
|
|
|
58
|
-
// Deterministic git-activity signals for the project where `shakers ai-usage` runs
|
|
58
|
+
// Deterministic git-activity signals for the project where `shakers ai-usage` runs.
|
|
59
59
|
function collectGitActivity(root, verifiedEmail) {
|
|
60
60
|
if (!isGitRepo(root)) return null;
|
|
61
61
|
|
package/src/graph-certs.js
CHANGED
|
@@ -24,10 +24,10 @@ function buildCertsPayload(project, lang) {
|
|
|
24
24
|
.filter(Boolean)
|
|
25
25
|
.map((item) => {
|
|
26
26
|
const res = item.result || {};
|
|
27
|
-
// `skillName` and `technology` both come off the wire (the service's certifiable list / verdict)
|
|
27
|
+
// `skillName` and `technology` both come off the wire (the service's certifiable list / verdict).
|
|
28
28
|
const base = sanitizeRenderText(item.skillName) || (item.skillId != null ? String(item.skillId) : 'Skill');
|
|
29
29
|
const name = item.technology ? `${base} · ${sanitizeRenderText(item.technology)}` : base;
|
|
30
|
-
//
|
|
30
|
+
// Named level (Middle/Senior/Expert) instead of the numeric grade.
|
|
31
31
|
const skillReport = (t.certify && t.certify.report) || {};
|
|
32
32
|
const skillLevelNames = skillReport.skillLevels || {};
|
|
33
33
|
const { key: levelKey, band } = levelInfoFromCombinedLevel(res.combinedLevel) || skillLevelForScore(res.score);
|
|
@@ -36,7 +36,7 @@ function buildCertsPayload(project, lang) {
|
|
|
36
36
|
band, // retained for the badge colour (unchanged palette)
|
|
37
37
|
levelKey,
|
|
38
38
|
levelName: levelKey ? (skillLevelNames[levelKey] || levelKey) : (skillLevelNames.na || '—'),
|
|
39
|
-
// Model-written text off the wire; sanitised
|
|
39
|
+
// Model-written text off the wire; sanitised.
|
|
40
40
|
rationale: typeof res.rationale === 'string' ? sanitizeRenderText(res.rationale) : '',
|
|
41
41
|
improvements: Array.isArray(res.improvements)
|
|
42
42
|
? res.improvements.map((i) => (typeof i === 'string' ? sanitizeRenderText(i) : i))
|