shakers-cli 0.15.0 → 0.17.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 +56 -1203
- package/bin/ai-usage.js +32 -58
- package/bin/login.js +10 -10
- package/bin/mcp.js +17 -12
- package/bin/mcpb.js +1 -1
- package/bin/onboarding.js +3 -2
- package/bin/register.js +19 -5
- 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/account-marker.js +40 -0
- package/src/account-trace.js +23 -0
- 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-usage-review.js +101 -0
- 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 +18 -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 +5 -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 +114 -48
- package/src/consent-flow.js +7 -6
- 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 +148 -138
- 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-result-tool.js +34 -12
- package/src/mcp-ai-usage-tool.js +282 -163
- package/src/mcp-auth-form.js +7 -3
- 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-install.js +279 -44
- package/src/mcp-instructions.js +1 -1
- package/src/mcp-onboarding-tools.js +4 -4
- package/src/mcp-prompts.js +4 -4
- package/src/mcp-register-tools.js +1 -0
- package/src/mcp-role-tools.js +24 -1
- package/src/mcp-server.js +5 -1
- package/src/mcp-signup-client.js +2 -2
- package/src/mcp-signup-tools.js +39 -9
- package/src/memory-structure-detector.js +1 -1
- package/src/model-call-record.js +2 -2
- package/src/onboarding-flow.js +63 -42
- package/src/onboarding-funnel.js +153 -20
- 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-report-text.js +1 -1
- 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 +5 -3
- package/src/report-store.js +13 -8
- package/src/roadmap-content.js +5 -5
- package/src/roadmap-personalization.js +3 -3
- package/src/roadmap-prompt.js +2 -2
- package/src/roles-flow.js +8 -5
- 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 +9 -9
- 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 +177 -2
- 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 +16 -4
- 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 +82 -181
- package/src/usage-scan.js +235 -0
- package/src/work-streams.js +1 -1
package/src/config.js
CHANGED
|
@@ -4,8 +4,9 @@ const fs = require('fs');
|
|
|
4
4
|
const path = require('path');
|
|
5
5
|
const { readEnv } = require('./env-paths');
|
|
6
6
|
const { getConfigDir } = require('./config-dir');
|
|
7
|
+
const { recordAccountMarker } = require('./account-marker');
|
|
7
8
|
|
|
8
|
-
// Persistent CLI config file
|
|
9
|
+
// Persistent CLI config file.
|
|
9
10
|
function configDir(env = process.env) {
|
|
10
11
|
return getConfigDir(env);
|
|
11
12
|
}
|
|
@@ -23,7 +24,7 @@ function loadConfigFile(env = process.env) {
|
|
|
23
24
|
}
|
|
24
25
|
}
|
|
25
26
|
|
|
26
|
-
// DISTRIBUTION PROFILE
|
|
27
|
+
// DISTRIBUTION PROFILE.
|
|
27
28
|
const VALID_PROFILES = new Set(['external', 'talent']);
|
|
28
29
|
const PROFILE_DEFAULT_WHEN_ABSENT = 'talent';
|
|
29
30
|
|
|
@@ -48,7 +49,7 @@ function getUiLang(env = process.env) {
|
|
|
48
49
|
return null;
|
|
49
50
|
}
|
|
50
51
|
|
|
51
|
-
// config.json holds the superadmin session token
|
|
52
|
+
// config.json holds the superadmin session token and consent/ endpoint data, so it must never be briefly world/group-readable.
|
|
52
53
|
function saveConfigFile(config, env = process.env) {
|
|
53
54
|
const dir = configDir(env);
|
|
54
55
|
fs.mkdirSync(dir, { recursive: true });
|
|
@@ -56,7 +57,7 @@ function saveConfigFile(config, env = process.env) {
|
|
|
56
57
|
try { fs.chmodSync(configFilePath(env), 0o600); } catch { /* e.g. Windows */ }
|
|
57
58
|
}
|
|
58
59
|
|
|
59
|
-
// Endpoint safety
|
|
60
|
+
// Endpoint safety — BOUNDED.
|
|
60
61
|
const LOCAL_HOSTS = new Set(['localhost', '127.0.0.1', '::1']);
|
|
61
62
|
const SHAKERS_CLI_DOMAINS = [];
|
|
62
63
|
|
|
@@ -120,7 +121,7 @@ function resolveIngestEndpoint(env = process.env) {
|
|
|
120
121
|
return { endpoint: null, source: 'none' };
|
|
121
122
|
}
|
|
122
123
|
|
|
123
|
-
// Baked backend targets per distribution flavor
|
|
124
|
+
// Baked backend targets per distribution flavor.
|
|
124
125
|
// Only the DEFAULT bases; per-endpoint SHAKERS_CLI_* env and config.json overrides
|
|
125
126
|
// still win at each getter (env > config.json > baked profile).
|
|
126
127
|
const ENV_PROFILES = {
|
|
@@ -130,41 +131,86 @@ const ENV_PROFILES = {
|
|
|
130
131
|
profileUrl: 'https://app.shakersworks.com/login',
|
|
131
132
|
almaBase: 'https://app.shakersworks.com',
|
|
132
133
|
},
|
|
134
|
+
dev: {
|
|
135
|
+
certsBase: 'https://api.dev.certifications.shakersworks.com/api/v1',
|
|
136
|
+
hubBase: 'https://api.hub.dev.shakersworks.com/api/v1',
|
|
137
|
+
profileUrl: 'https://works.dev.shakersworks.com/login',
|
|
138
|
+
almaBase: 'https://works.dev.shakersworks.com',
|
|
139
|
+
},
|
|
140
|
+
local: {
|
|
141
|
+
certsBase: 'http://localhost:3004/api/v1',
|
|
142
|
+
hubBase: 'http://localhost:3001/api/v1',
|
|
143
|
+
profileUrl: 'http://localhost:8081/login',
|
|
144
|
+
},
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
const RETIRED_ENV_PROFILES = {
|
|
133
148
|
staging: {
|
|
134
149
|
certsBase: 'https://api.staging.certifications.shakersworks.com/api/v1',
|
|
135
150
|
hubBase: 'https://api.hub.staging.shakersworks.com/api/v1',
|
|
136
151
|
profileUrl: 'https://works.staging.shakersworks.com/login',
|
|
137
152
|
almaBase: 'https://works.staging.shakersworks.com',
|
|
138
153
|
},
|
|
139
|
-
dev: {
|
|
140
|
-
certsBase: 'https://api.dev.certifications.shakersworks.com/api/v1',
|
|
154
|
+
'dev-new-works': {
|
|
141
155
|
hubBase: 'https://api.hub.new-works.dev.shakersworks.com/api/v1',
|
|
142
156
|
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',
|
|
157
|
+
almaBase: 'https://works.new-works.dev.shakersworks.com',
|
|
148
158
|
},
|
|
149
159
|
};
|
|
150
160
|
|
|
151
161
|
// 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': '
|
|
162
|
+
// published package name (shakers-cli -> prod, shakers-cli-beta -> dev), else dev (safe default).
|
|
163
|
+
const PACKAGE_NAME_ENV = { 'shakers-cli': 'prod', 'shakers-cli-beta': 'dev' };
|
|
154
164
|
const DEFAULT_ENV_PROFILE = 'dev';
|
|
155
165
|
|
|
156
|
-
function
|
|
166
|
+
function envProfileForPackageName(name) {
|
|
167
|
+
return PACKAGE_NAME_ENV[name] || DEFAULT_ENV_PROFILE;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function readPackageJson() {
|
|
157
171
|
try {
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
172
|
+
return require('../package.json');
|
|
173
|
+
} catch {
|
|
174
|
+
return {};
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function bakedEnvProfileName() {
|
|
179
|
+
return envProfileForPackageName(readPackageJson().name);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function envOverrideVariable(env) {
|
|
183
|
+
const next = env.SHAKERS_CLI_ENV;
|
|
184
|
+
return next !== undefined && next !== '' ? 'SHAKERS_CLI_ENV' : 'AI_FOOTPRINT_ENV';
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
class UnknownEnvProfileError extends Error {
|
|
188
|
+
constructor(variable, value) {
|
|
189
|
+
const known = Object.keys(ENV_PROFILES).join(', ');
|
|
190
|
+
super(RETIRED_ENV_PROFILES[value]
|
|
191
|
+
? `${variable}=${value} is a retired profile. Use one of ${known}, or unset it.`
|
|
192
|
+
: `${variable}=${value} is not a known profile. Use one of ${known}, or unset it.`);
|
|
193
|
+
this.name = 'UnknownEnvProfileError';
|
|
194
|
+
this.variable = variable;
|
|
195
|
+
this.value = value;
|
|
196
|
+
}
|
|
162
197
|
}
|
|
163
198
|
|
|
164
199
|
function getEnvProfileName(env = process.env) {
|
|
165
|
-
const override = readEnv(env, 'ENV');
|
|
166
|
-
if (override
|
|
167
|
-
|
|
200
|
+
const override = String(readEnv(env, 'ENV') || '').trim();
|
|
201
|
+
if (!override) return bakedEnvProfileName();
|
|
202
|
+
if (ENV_PROFILES[override]) return override;
|
|
203
|
+
throw new UnknownEnvProfileError(envOverrideVariable(env), override);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function envProfileProblem(env = process.env) {
|
|
207
|
+
try {
|
|
208
|
+
getEnvProfileName(env);
|
|
209
|
+
return null;
|
|
210
|
+
} catch (e) {
|
|
211
|
+
if (e instanceof UnknownEnvProfileError) return e.message;
|
|
212
|
+
throw e;
|
|
213
|
+
}
|
|
168
214
|
}
|
|
169
215
|
|
|
170
216
|
function getEnvProfile(env = process.env) {
|
|
@@ -205,7 +251,7 @@ function getCertsBase(env = process.env) {
|
|
|
205
251
|
if (ingest) return ingest.replace(/\/usage\/reports\/?$/, '').replace(/\/+$/, '');
|
|
206
252
|
// No explicit override anywhere -> the baked flavor profile (never null when a
|
|
207
253
|
// profile exists), so switching the published flavor "just works" without a
|
|
208
|
-
// seeded config.json
|
|
254
|
+
// seeded config.json.
|
|
209
255
|
const baked = getEnvProfile(env).certsBase;
|
|
210
256
|
return baked ? baked.replace(/\/+$/, '') : null;
|
|
211
257
|
}
|
|
@@ -217,11 +263,11 @@ function getIngestEndpoint(env = process.env) {
|
|
|
217
263
|
return base ? `${base}/usage/reports` : null;
|
|
218
264
|
}
|
|
219
265
|
|
|
220
|
-
// Agent-synthesis endpoint
|
|
266
|
+
// Agent-synthesis endpoint.
|
|
221
267
|
function getSynthesisEndpoint(env = process.env) {
|
|
222
268
|
const value = readEnv(env, 'SYNTHESIS_ENDPOINT');
|
|
223
269
|
if (value && value.trim()) return value.trim();
|
|
224
|
-
//
|
|
270
|
+
// Derived as an ingest sibling when the override is unset.
|
|
225
271
|
return deriveFromIngest(env, 'agent-synthesis');
|
|
226
272
|
}
|
|
227
273
|
|
|
@@ -231,7 +277,7 @@ function getRoadmapEndpoint(env = process.env) {
|
|
|
231
277
|
return deriveFromIngest(env, 'roadmap-personalize');
|
|
232
278
|
}
|
|
233
279
|
|
|
234
|
-
// Email-verification endpoints
|
|
280
|
+
// 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
281
|
function deriveFromIngest(env, relativePath) {
|
|
236
282
|
const ingest = getIngestEndpoint(env);
|
|
237
283
|
if (!ingest) return null;
|
|
@@ -254,7 +300,7 @@ function getAgentEvaluationEndpoint(env = process.env) {
|
|
|
254
300
|
return deriveFromIngest(env, 'agent-evaluation');
|
|
255
301
|
}
|
|
256
302
|
|
|
257
|
-
// Login endpoint — better-auth email sign-in
|
|
303
|
+
// Login endpoint — better-auth email sign-in: `POST {hubBase}/auth/sign-in/email` with `{email,password}`.
|
|
258
304
|
function getLoginEndpoint(env = process.env) {
|
|
259
305
|
const explicit = readEnv(env, 'LOGIN_ENDPOINT');
|
|
260
306
|
if (explicit && explicit.trim()) return explicit.trim();
|
|
@@ -262,6 +308,10 @@ function getLoginEndpoint(env = process.env) {
|
|
|
262
308
|
}
|
|
263
309
|
|
|
264
310
|
// better-auth JWT endpoint: `GET {hubBase}/auth/token` with the session cookie → `{token}` (ES256/JWKS, ~15-min TTL).
|
|
311
|
+
function getSignOutEndpoint(env = process.env) {
|
|
312
|
+
return deriveFromHub(env, 'auth/sign-out');
|
|
313
|
+
}
|
|
314
|
+
|
|
265
315
|
function getAuthTokenEndpoint(env = process.env) {
|
|
266
316
|
const explicit = readEnv(env, 'AUTH_TOKEN_ENDPOINT');
|
|
267
317
|
if (explicit && explicit.trim()) return explicit.trim();
|
|
@@ -462,14 +512,14 @@ function getAiProfileEndpoint(env = process.env) {
|
|
|
462
512
|
return deriveFromHub(env, 'works/talent/me/ai-profile');
|
|
463
513
|
}
|
|
464
514
|
|
|
465
|
-
// Evidence emission endpoint for the `cli_scan` instrument
|
|
515
|
+
// Evidence emission endpoint for the `cli_scan` instrument.
|
|
466
516
|
function getEvidenceCliScanEndpoint(env = process.env) {
|
|
467
517
|
const explicit = readEnv(env, 'EVIDENCE_CLI_SCAN_ENDPOINT');
|
|
468
518
|
if (explicit && explicit.trim()) return explicit.trim();
|
|
469
519
|
return deriveFromIngest(env, '../evidence/cli-scan');
|
|
470
520
|
}
|
|
471
521
|
|
|
472
|
-
// Talent-portfolio endpoints (
|
|
522
|
+
// Talent-portfolio endpoints (`start`'s "Add project to portfolio" route, talent-only).
|
|
473
523
|
function getPortfoliosDeclareEndpoint(env = process.env) {
|
|
474
524
|
const explicit = readEnv(env, 'PORTFOLIOS_DECLARE_ENDPOINT');
|
|
475
525
|
if (explicit && explicit.trim()) return explicit.trim();
|
|
@@ -512,7 +562,7 @@ function getAgentsDraftFieldsEndpoint(env = process.env) {
|
|
|
512
562
|
return deriveFromIngest(env, '../works/talents/me/agents/draft-fields');
|
|
513
563
|
}
|
|
514
564
|
|
|
515
|
-
// Agent<->experience relate endpoint (
|
|
565
|
+
// Agent<->experience relate endpoint (`start`'s "Add agent" relate step / MCP `add_agent.relatedPortfolios`).
|
|
516
566
|
function getAgentPortfoliosEndpoint(agentId, env = process.env) {
|
|
517
567
|
// Null on missing/non-scalar id: no bogus URL, and keeps config.test.js's `fn(env)`
|
|
518
568
|
// getter sweep honest when the env object lands in `agentId`.
|
|
@@ -520,7 +570,7 @@ function getAgentPortfoliosEndpoint(agentId, env = process.env) {
|
|
|
520
570
|
return deriveFromHub(env, `works/talent/me/agents/${encodeURIComponent(agentId)}/portfolios`);
|
|
521
571
|
}
|
|
522
572
|
|
|
523
|
-
// Onboarding endpoints (
|
|
573
|
+
// Onboarding endpoints (the register flow).
|
|
524
574
|
function getHubBase(env = process.env) {
|
|
525
575
|
const explicit = readEnv(env, 'HUB_BASE');
|
|
526
576
|
if (explicit && explicit.trim()) return explicit.trim().replace(/\/+$/, '');
|
|
@@ -551,7 +601,7 @@ function getImportProfileEndpoint(env = process.env) {
|
|
|
551
601
|
return deriveFromHub(env, 'works/talent/me/import-profile');
|
|
552
602
|
}
|
|
553
603
|
|
|
554
|
-
// MCP sign-up
|
|
604
|
+
// MCP sign-up: public UNREGISTERED + claim code, its import status, and the one-time web login.
|
|
555
605
|
function getMcpSignupEndpoint(env = process.env) {
|
|
556
606
|
return deriveFromHub(env, 'works-ai/talents/mcp-signup');
|
|
557
607
|
}
|
|
@@ -564,11 +614,20 @@ function getOnboardingEventsEndpoint(env = process.env) {
|
|
|
564
614
|
return deriveFromHub(env, 'works/talent/me/onboarding-events');
|
|
565
615
|
}
|
|
566
616
|
|
|
617
|
+
const AMPLITUDE_HTTP_API = 'https://api.eu.amplitude.com/2/httpapi';
|
|
618
|
+
|
|
619
|
+
function getAmplitudeIngestion(env = process.env) {
|
|
620
|
+
const stamped = readPackageJson().amplitude;
|
|
621
|
+
if (!stamped || typeof stamped.apiKey !== 'string' || !stamped.apiKey.trim()) return null;
|
|
622
|
+
if (stamped.env !== getEnvProfileName(env)) return null;
|
|
623
|
+
return { endpoint: AMPLITUDE_HTTP_API, apiKey: stamped.apiKey.trim() };
|
|
624
|
+
}
|
|
625
|
+
|
|
567
626
|
function getOneTimeTokenEndpoint(env = process.env) {
|
|
568
627
|
return deriveFromHub(env, 'auth/one-time-token/generate');
|
|
569
628
|
}
|
|
570
629
|
|
|
571
|
-
// The ONE place the web route that redeems a one-time token is spelled
|
|
630
|
+
// The ONE place the web route that redeems a one-time token is spelled.
|
|
572
631
|
function getOneTimeLoginUrl(token, env = process.env) {
|
|
573
632
|
if (typeof token !== 'string' || !token) return null;
|
|
574
633
|
const profile = getTalentProfileUrl(env);
|
|
@@ -675,7 +734,7 @@ function getOnboardingStatusEndpoint(env = process.env) {
|
|
|
675
734
|
return base ? `${base}/interviews/onboarding/status` : null;
|
|
676
735
|
}
|
|
677
736
|
|
|
678
|
-
// Talent profile web URL (
|
|
737
|
+
// Talent profile web URL (the step-5 skip fallback).
|
|
679
738
|
function getTalentProfileUrl(env = process.env) {
|
|
680
739
|
const explicit = readEnv(env, 'PROFILE_URL');
|
|
681
740
|
if (explicit && explicit.trim()) return explicit.trim().replace(/\/+$/, '');
|
|
@@ -689,14 +748,14 @@ function getTalentProfileUrl(env = process.env) {
|
|
|
689
748
|
// user overrides of these, never the baked profile itself.
|
|
690
749
|
const PROFILE_ENDPOINT_KEYS = Object.keys(ENV_PROFILES[DEFAULT_ENV_PROFILE]);
|
|
691
750
|
|
|
692
|
-
// Boot migration
|
|
751
|
+
// Boot migration. The CLI used to SEED config.json with the
|
|
693
752
|
// baked bases on first run; the getters now fall back to getEnvProfile() directly,
|
|
694
753
|
// 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
|
|
754
|
+
// a config.json seeded by a previous flavor (e.g. the beta when it baked staging)
|
|
755
|
+
// survives into a new flavor (the beta now baking dev) and pins the old endpoints
|
|
697
756
|
// forever, since config.json wins over the baked profile. This sweep removes any
|
|
698
757
|
// 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
|
|
758
|
+
// (current or retired) OTHER than the baked one — i.e. a value that could only have gotten there by the
|
|
700
759
|
// old seeding of a different flavor. A value that matches no known profile is a
|
|
701
760
|
// genuine user override (set via `shakers config set`) and is left untouched. We
|
|
702
761
|
// never touch env, auth-session.json or consent.json, and never remove the dir.
|
|
@@ -708,7 +767,7 @@ function sanitizeStaleBakedEndpoints(env = process.env) {
|
|
|
708
767
|
const current = config[key];
|
|
709
768
|
if (typeof current !== 'string' || !current.trim()) continue;
|
|
710
769
|
const normalized = current.trim().replace(/\/+$/, '');
|
|
711
|
-
const matchesForeignProfile = Object.entries(ENV_PROFILES).some(
|
|
770
|
+
const matchesForeignProfile = Object.entries({ ...RETIRED_ENV_PROFILES, ...ENV_PROFILES }).some(
|
|
712
771
|
([name, profile]) =>
|
|
713
772
|
name !== bakedName && String(profile[key]).replace(/\/+$/, '') === normalized,
|
|
714
773
|
);
|
|
@@ -735,12 +794,12 @@ function getSuperadminSessionEndpoint(env = process.env) {
|
|
|
735
794
|
return deriveFromIngest(env, 'superadmin/session');
|
|
736
795
|
}
|
|
737
796
|
|
|
738
|
-
// Superadmin READ-ONLY certification inspection endpoint (
|
|
797
|
+
// Superadmin READ-ONLY certification inspection endpoint (NON-PROD only) — a sibling of the ingest endpoint.
|
|
739
798
|
function getInspectCertificationsEndpoint(env = process.env) {
|
|
740
799
|
return deriveFromIngest(env, 'superadmin/inspect-certifications');
|
|
741
800
|
}
|
|
742
801
|
|
|
743
|
-
//
|
|
802
|
+
// Superadmin SESSION persistence.
|
|
744
803
|
function loadSuperadminSession(env = process.env) {
|
|
745
804
|
const config = loadConfigFile(env);
|
|
746
805
|
const s = config && typeof config === 'object' ? config.superadminSession : null;
|
|
@@ -757,6 +816,7 @@ function saveSuperadminSession(session, env = process.env) {
|
|
|
757
816
|
expiresAt: session.expiresAt || null,
|
|
758
817
|
};
|
|
759
818
|
saveConfigFile(config, env);
|
|
819
|
+
recordAccountMarker('superadmin', env);
|
|
760
820
|
}
|
|
761
821
|
|
|
762
822
|
function clearSuperadminSession(env = process.env) {
|
|
@@ -774,6 +834,7 @@ module.exports = {
|
|
|
774
834
|
getAgentEvaluationEndpoint,
|
|
775
835
|
getLoginEndpoint,
|
|
776
836
|
getAuthTokenEndpoint,
|
|
837
|
+
getSignOutEndpoint,
|
|
777
838
|
getCompleteRegistrationEndpoint,
|
|
778
839
|
getDeviceAuthorizeEndpoint,
|
|
779
840
|
getDeviceTokenEndpoint,
|
|
@@ -807,19 +868,19 @@ module.exports = {
|
|
|
807
868
|
getUsageDiscoveredInventoryEndpoint,
|
|
808
869
|
getUsageReportEndpoint,
|
|
809
870
|
getAiProfileEndpoint,
|
|
810
|
-
// Evidence emission
|
|
871
|
+
// Evidence emission.
|
|
811
872
|
getEvidenceCliScanEndpoint,
|
|
812
|
-
// Talent-portfolio endpoints
|
|
873
|
+
// Talent-portfolio endpoints.
|
|
813
874
|
getPortfoliosDeclareEndpoint,
|
|
814
875
|
getPortfoliosListEndpoint,
|
|
815
876
|
getPortfolioDraftDescriptionEndpoint,
|
|
816
877
|
getPortfolioUpdateEndpoint,
|
|
817
|
-
// Talent-agent endpoints (
|
|
878
|
+
// Talent-agent endpoints ("Add agent to profile").
|
|
818
879
|
getAgentsDeclareEndpoint,
|
|
819
880
|
getAgentsListEndpoint,
|
|
820
881
|
getAgentsDraftFieldsEndpoint,
|
|
821
882
|
getAgentPortfoliosEndpoint,
|
|
822
|
-
// Onboarding / register flow
|
|
883
|
+
// Onboarding / register flow.
|
|
823
884
|
getCertsBase,
|
|
824
885
|
getHubBase,
|
|
825
886
|
sanitizeStaleBakedEndpoints,
|
|
@@ -828,6 +889,8 @@ module.exports = {
|
|
|
828
889
|
getMcpSignupEndpoint,
|
|
829
890
|
getMyImportStatusEndpoint,
|
|
830
891
|
getOnboardingEventsEndpoint,
|
|
892
|
+
getAmplitudeIngestion,
|
|
893
|
+
AMPLITUDE_HTTP_API,
|
|
831
894
|
getOneTimeTokenEndpoint,
|
|
832
895
|
getOneTimeLoginUrl,
|
|
833
896
|
getProfessionalDetailsEndpoint,
|
|
@@ -849,7 +912,7 @@ module.exports = {
|
|
|
849
912
|
getEmailVerificationVerifyUrl,
|
|
850
913
|
getSuperadminSessionEndpoint,
|
|
851
914
|
getInspectCertificationsEndpoint,
|
|
852
|
-
//
|
|
915
|
+
// Superadmin session persistence.
|
|
853
916
|
loadSuperadminSession,
|
|
854
917
|
saveSuperadminSession,
|
|
855
918
|
clearSuperadminSession,
|
|
@@ -863,15 +926,18 @@ module.exports = {
|
|
|
863
926
|
setIngestEndpoint,
|
|
864
927
|
hasRetiredFallbackConfig,
|
|
865
928
|
resolveIngestEndpoint,
|
|
866
|
-
// Distribution profile
|
|
929
|
+
// Distribution profile.
|
|
867
930
|
VALID_PROFILES,
|
|
868
931
|
getProfile,
|
|
869
932
|
isTalentProfile,
|
|
870
933
|
// UI language preference (env SHAKERS_CLI_LANG > config.json `lang`).
|
|
871
934
|
VALID_UI_LANGS,
|
|
872
935
|
getUiLang,
|
|
873
|
-
// Baked backend target flavor (
|
|
936
|
+
// Baked backend target flavor (prod | dev | local).
|
|
874
937
|
ENV_PROFILES,
|
|
875
938
|
getEnvProfileName,
|
|
876
939
|
getEnvProfile,
|
|
940
|
+
envProfileForPackageName,
|
|
941
|
+
envProfileProblem,
|
|
942
|
+
UnknownEnvProfileError,
|
|
877
943
|
};
|
package/src/consent-flow.js
CHANGED
|
@@ -8,17 +8,18 @@ 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
|
|
|
15
15
|
const YES_RE = /^(y|yes|s|si|sí)$/i;
|
|
16
16
|
const NO_RE = /^(n|no)$/i;
|
|
17
17
|
|
|
18
|
-
async function askYesNo(ask, question, catalog, { defaultNo = false } = {}) {
|
|
18
|
+
async function askYesNo(ask, question, catalog, { defaultNo = false, defaultYes = false } = {}) {
|
|
19
19
|
for (let attempt = 0; attempt < MAX_ATTEMPTS; attempt++) {
|
|
20
20
|
const raw = String(await ask(question)).trim();
|
|
21
21
|
if (defaultNo && raw === '') return false;
|
|
22
|
+
if (defaultYes && raw === '') return true;
|
|
22
23
|
if (YES_RE.test(raw)) return true;
|
|
23
24
|
if (NO_RE.test(raw)) return false;
|
|
24
25
|
process.stdout.write(` ${catalog.consent.invalidAnswer}\n`);
|
|
@@ -45,7 +46,7 @@ async function defaultVerifyEmail({ email, ask, catalog, env = process.env }) {
|
|
|
45
46
|
});
|
|
46
47
|
}
|
|
47
48
|
|
|
48
|
-
// Runs the short consent-to-persist prompt + (if accepted) email collection +
|
|
49
|
+
// Runs the short consent-to-persist prompt + (if accepted) email collection + EMAIL-OWNERSHIP VERIFICATION.
|
|
49
50
|
async function runConsentPrompt({
|
|
50
51
|
ask,
|
|
51
52
|
catalog,
|
|
@@ -57,7 +58,7 @@ async function runConsentPrompt({
|
|
|
57
58
|
profile = 'talent',
|
|
58
59
|
}) {
|
|
59
60
|
const c = catalog.consent;
|
|
60
|
-
// Presentation only
|
|
61
|
+
// Presentation only: framed by src/legal-notice.js on a
|
|
61
62
|
// real TTY, byte-identical (no heading, no rule) otherwise.
|
|
62
63
|
process.stdout.write(`\n ${renderLegalNotice(c.persistIntro, catalog)}\n`);
|
|
63
64
|
|
|
@@ -100,7 +101,7 @@ async function runConsentPrompt({
|
|
|
100
101
|
|
|
101
102
|
process.stdout.write(` ${c.savedUnderEmail(email)}\n`);
|
|
102
103
|
|
|
103
|
-
// Prove email ownership BEFORE persisting
|
|
104
|
+
// Prove email ownership BEFORE persisting — every profile, external included.
|
|
104
105
|
const verification = await verifyEmail({ email, ask, catalog, env });
|
|
105
106
|
if (verification && verification.verified) {
|
|
106
107
|
recordConsent('granted', email, { verified: true });
|
|
@@ -112,4 +113,4 @@ async function runConsentPrompt({
|
|
|
112
113
|
return null;
|
|
113
114
|
}
|
|
114
115
|
|
|
115
|
-
module.exports = { runConsentPrompt, defaultVerifyEmail };
|
|
116
|
+
module.exports = { runConsentPrompt, defaultVerifyEmail, askYesNo };
|
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))
|