shakers-cli 0.16.0 → 0.18.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +21 -6
- package/bin/ai-usage.js +31 -12
- package/bin/command-audit-worker.js +19 -0
- package/bin/login.js +4 -8
- package/bin/mcp.js +32 -21
- package/bin/register.js +12 -30
- package/bin/shakers-cli.js +15 -10
- package/package.json +1 -1
- package/src/account-marker.js +40 -0
- package/src/account-trace.js +23 -0
- package/src/agents-client.js +1 -0
- package/src/ai-usage-review.js +103 -0
- package/src/auth-client.js +7 -107
- package/src/auth-session-store.js +12 -11
- package/src/backend-request.js +3 -24
- package/src/cli-skill.js +77 -0
- package/src/command-audit.js +47 -4
- package/src/config.js +2 -16
- package/src/consent-flow.js +9 -3
- package/src/detect-repos.js +4 -8
- package/src/detectors.js +23 -0
- package/src/device-auth-store.js +2 -2
- package/src/device-login.js +11 -9
- package/src/env-paths.js +9 -1
- package/src/i18n.js +186 -102
- package/src/mcp-ai-usage-result-tool.js +38 -14
- package/src/mcp-ai-usage-tool.js +317 -172
- package/src/mcp-choice.js +171 -34
- package/src/mcp-detector.js +26 -2
- package/src/mcp-device-login-tools.js +40 -8
- package/src/mcp-install.js +292 -53
- package/src/mcp-instructions.js +43 -17
- package/src/mcp-onboarding-tools.js +1 -3
- package/src/mcp-prompts.js +8 -6
- package/src/mcp-register-tools.js +25 -14
- package/src/mcp-role-tools.js +59 -18
- package/src/mcp-server-name.js +7 -0
- package/src/mcp-server.js +35 -7
- package/src/mcp-signup-client.js +3 -6
- package/src/mcp-signup-flow.js +389 -0
- package/src/mcp-signup-tools.js +250 -133
- package/src/onboarding-flow.js +72 -146
- package/src/onboarding-funnel.js +46 -6
- package/src/opencode-sessions.js +92 -0
- package/src/profile-agents.js +26 -0
- package/src/render-report-text.js +1 -1
- package/src/report-gating.js +4 -2
- package/src/report-store.js +7 -2
- package/src/roles-flow.js +8 -5
- package/src/saved-positions-flow.js +3 -3
- package/src/scanner.js +104 -12
- package/src/session-gate.js +1 -4
- package/src/session-normalizers.js +114 -8
- package/src/session-refresh.js +5 -16
- package/src/session-scan.js +167 -22
- package/src/share-consent-state.js +2 -2
- package/src/share-payload.js +4 -1
- package/src/share.js +2 -2
- package/src/signup-client.js +12 -39
- package/src/signup-copy.js +545 -58
- package/src/skill-install.js +102 -0
- package/src/stdin-ask.js +13 -1
- package/src/tier-engine.js +14 -2
- package/src/trust-narration.js +32 -0
- package/src/usage-run-steps.js +68 -6
- package/src/usage-scan.js +11 -8
- package/src/brand-svg.js +0 -9
- package/src/inter-fonts.js +0 -6
- package/src/mcp-auth-form.js +0 -480
package/README.md
CHANGED
|
@@ -64,14 +64,29 @@ Run `shakers` with no command to see every command.
|
|
|
64
64
|
## Privacy
|
|
65
65
|
|
|
66
66
|
- The `ai-usage` scan runs on your machine and its report is shown there first.
|
|
67
|
-
- Saving it to Shakers is opt-in.
|
|
68
|
-
|
|
69
|
-
`--consent-
|
|
70
|
-
|
|
71
|
-
|
|
67
|
+
- Saving it to Shakers is opt-in. Every time, before anything is uploaded, you see what
|
|
68
|
+
would be sent and what Shakers does with it, and it is uploaded only if you say yes. You
|
|
69
|
+
can leave out agents and text excerpts there. `--consent-revoke` stops future uploads
|
|
70
|
+
from this computer; it does not delete what was already uploaded. To ask for that, write
|
|
71
|
+
to talent@shakersworks.com.
|
|
72
|
+
- What is sent is mostly derived data (counts, categories, tier, detected technologies,
|
|
73
|
+
sessions, active days, commits and lines changed). Literal text travels too, and the review
|
|
74
|
+
shows how much: up to 40 fragments of your messages to AI assistants and 20 commands they
|
|
75
|
+
ran (200 characters at most each), and the definition (up to 2,000 characters) and short
|
|
76
|
+
description of each of your AI agents, all after removing anything that looks like a
|
|
77
|
+
secret, a path or an email. The fragments come from the conversations in the repositories
|
|
78
|
+
analysed, or from every conversation on this computer when you analyse the whole machine
|
|
79
|
+
(registration always does). Never your code, your full conversations, the names of your
|
|
80
|
+
repositories, environment variables or credentials.
|
|
81
|
+
- An AI model from an external provider (OpenAI, Google or Anthropic) analyses that text
|
|
82
|
+
automatically for Shakers to compute your evaluation, and then it is discarded. Only the result is stored: levels, scores, counts and the names
|
|
83
|
+
of your tools and agents.
|
|
84
|
+
- Do not run it on a machine or with conversations that hold information you are not
|
|
85
|
+
allowed to share: client, employer or third-party data. Secret removal is automatic and
|
|
86
|
+
best effort; names or business details you wrote can remain in the fragments.
|
|
72
87
|
- Certification sends secret-scrubbed samples of your code and your interview answers to
|
|
73
88
|
Shakers to assess them.
|
|
74
|
-
- Read the full policy at <https://shakersworks.com/
|
|
89
|
+
- Read the full policy at <https://www.shakersworks.com/en/politica-de-privacidad>.
|
|
75
90
|
|
|
76
91
|
## License
|
|
77
92
|
|
package/bin/ai-usage.js
CHANGED
|
@@ -4,7 +4,9 @@
|
|
|
4
4
|
const { detectReportLang, getCatalog } = require('../src/i18n');
|
|
5
5
|
const { parseArgs } = require('../src/cli-args');
|
|
6
6
|
const { handleOneShotCommand } = require('../src/usage-oneshot-commands');
|
|
7
|
+
const { createLazyStdinAsk } = require('../src/stdin-ask');
|
|
7
8
|
const { computeUsage } = require('../src/usage-scan');
|
|
9
|
+
const { hasAccountTrace } = require('../src/account-trace');
|
|
8
10
|
const {
|
|
9
11
|
resolveRepoScope,
|
|
10
12
|
machineWideToplevels,
|
|
@@ -32,18 +34,35 @@ async function run(argv = process.argv.slice(2), { ask: injectedAsk = null, bypa
|
|
|
32
34
|
// One-shot commands (consent/endpoint config): act immediately, do NOT scan.
|
|
33
35
|
if (await handleOneShotCommand({ opts, catalog, injectedAsk })) return;
|
|
34
36
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
37
|
+
if (opts.roadmap && hasAccountTrace()) {
|
|
38
|
+
process.stdout.write(`\n ${catalog.cli.roadmapNotForAccounts}\n\n`);
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const ask = injectedAsk || createLazyStdinAsk();
|
|
43
|
+
try {
|
|
44
|
+
const { report, maturity, root, gate, repos } = await computeUsage({
|
|
45
|
+
opts,
|
|
46
|
+
catalog,
|
|
47
|
+
lang,
|
|
48
|
+
inRegistration,
|
|
49
|
+
prepareIdentity: prepareIdentityAndUsage,
|
|
50
|
+
resolveConsent: (args) => resolveAiConsent({ ...args, injectedAsk: ask }),
|
|
51
|
+
resolveScope: async (args) => {
|
|
52
|
+
if (injectedAsk) return resolveRepoScope({ ...args, injectedAsk });
|
|
53
|
+
ask.suspend();
|
|
54
|
+
try {
|
|
55
|
+
return await resolveRepoScope(args);
|
|
56
|
+
} finally {
|
|
57
|
+
ask.resume();
|
|
58
|
+
}
|
|
59
|
+
},
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
await renderAndFinalize({ report, maturity, lang, opts, root, gate, catalog, bypassThrottle, repos, injectedAsk: ask });
|
|
63
|
+
} finally {
|
|
64
|
+
if (!injectedAsk) ask.close();
|
|
65
|
+
}
|
|
47
66
|
}
|
|
48
67
|
|
|
49
68
|
module.exports = { run, resolveRepoScope, machineWideToplevels, promptScope };
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
const { ensureFreshSession } = require('../src/session-refresh');
|
|
5
|
+
const { emitCommandAudit, PAYLOAD_VAR } = require('../src/command-audit');
|
|
6
|
+
|
|
7
|
+
async function main() {
|
|
8
|
+
let payload;
|
|
9
|
+
try {
|
|
10
|
+
payload = JSON.parse(process.env[PAYLOAD_VAR]);
|
|
11
|
+
} catch {
|
|
12
|
+
return;
|
|
13
|
+
}
|
|
14
|
+
if (!payload || typeof payload.command !== 'string') return;
|
|
15
|
+
await ensureFreshSession(process.env).catch(() => null);
|
|
16
|
+
await emitCommandAudit({ command: payload.command, argv: payload.args ? [payload.args] : [], env: process.env, id: typeof payload.id === 'string' ? payload.id : null });
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
main();
|
package/bin/login.js
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
|
|
6
6
|
const { detectReportLang, getCatalog } = require('../src/i18n');
|
|
7
7
|
const { getAuthTokenEndpoint, getDeviceAuthorizeEndpoint, getDeviceTokenEndpoint, getCompleteRegistrationEndpoint } = require('../src/config');
|
|
8
|
-
const { runDeviceLogin,
|
|
8
|
+
const { runDeviceLogin, withDeviceParams, deviceSessionRecord } = require('../src/device-login');
|
|
9
9
|
const { buildRegistrationContext } = require('../src/signup-client');
|
|
10
10
|
const { openPath } = require('../src/open-file');
|
|
11
11
|
const { endSession } = require('../src/session-refresh');
|
|
@@ -100,14 +100,10 @@ function persistAndReport(result, c, email = null, output = process.stdout) {
|
|
|
100
100
|
}
|
|
101
101
|
}
|
|
102
102
|
try {
|
|
103
|
-
// `cookie`/`cookieExpiresAt` are present for the email (better-auth) flow and
|
|
104
|
-
// absent for Google (auth-works JWT) — saveAuthSession picks the right model.
|
|
105
103
|
saveAuthSession({
|
|
106
104
|
accessToken: result.accessToken,
|
|
107
105
|
expiresAt: result.expiresAt,
|
|
108
106
|
accessTokenExpiresAt: result.expiresAt,
|
|
109
|
-
cookie: result.cookie || null,
|
|
110
|
-
cookieExpiresAt: result.cookieExpiresAt || null,
|
|
111
107
|
sessionToken: result.sessionToken || null,
|
|
112
108
|
sessionExpiresAt: result.sessionExpiresAt || null,
|
|
113
109
|
email,
|
|
@@ -120,7 +116,7 @@ function persistAndReport(result, c, email = null, output = process.stdout) {
|
|
|
120
116
|
}
|
|
121
117
|
output.write(`\n ${c.success}\n`);
|
|
122
118
|
if (result.expiresAt) output.write(` ${c.expiresAt(result.expiresAt)}\n`);
|
|
123
|
-
output.write(
|
|
119
|
+
output.write(`\n ${c.deviceGranted}\n\n`);
|
|
124
120
|
return true;
|
|
125
121
|
}
|
|
126
122
|
|
|
@@ -157,8 +153,8 @@ async function runDeviceFlow(provider, c, { openBrowser = openPath, sleep = unde
|
|
|
157
153
|
{
|
|
158
154
|
...(sleep ? { sleep } : {}),
|
|
159
155
|
onPrompt: ({ verificationUriComplete, userCode }) => {
|
|
160
|
-
const url =
|
|
161
|
-
output.write(`\n ${c.deviceVisit}\n ${url}\n\n ${c.deviceCodeLabel(userCode)}\n\n ${c.deviceWaiting}\n`);
|
|
156
|
+
const url = withDeviceParams(verificationUriComplete, { provider, mode: 'login', surface: 'cli' });
|
|
157
|
+
output.write(`\n ${c.deviceTrust}\n\n ${c.deviceVisit}\n ${url}\n\n ${c.deviceCodeLabel(userCode)}\n\n ${c.deviceWaiting}\n`);
|
|
162
158
|
if (provider === 'email') output.write(` ${c.deviceApproveHint}\n`);
|
|
163
159
|
try { openBrowser(url); } catch { /* the printed URL covers it */ }
|
|
164
160
|
},
|
package/bin/mcp.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
'use strict';
|
|
3
3
|
|
|
4
4
|
const { createMcpServer } = require('../src/mcp-server');
|
|
5
|
-
const {
|
|
5
|
+
const { makeAiUsageTools } = require('../src/mcp-ai-usage-tool');
|
|
6
6
|
const { makeAiUsageResultTool } = require('../src/mcp-ai-usage-result-tool');
|
|
7
7
|
const { makeCertifyDimensionTools } = require('../src/mcp-certify-dimension-tools');
|
|
8
8
|
const { makeFindProjectsTools } = require('../src/mcp-find-projects-tools');
|
|
@@ -23,13 +23,20 @@ const { makeRoleTools } = require('../src/mcp-role-tools');
|
|
|
23
23
|
const { makeDeviceLoginTools } = require('../src/mcp-device-login-tools');
|
|
24
24
|
const { makeRegisterTools } = require('../src/mcp-register-tools');
|
|
25
25
|
const { makeSignupTools } = require('../src/mcp-signup-tools');
|
|
26
|
+
const { makeSignupTool, outsideSignup, narratedTool, narrationLanguage, ABSORBED_TOOLS } = require('../src/mcp-signup-flow');
|
|
26
27
|
const { detectFlowLang } = require('../src/i18n');
|
|
27
28
|
const { makeReportTools } = require('../src/mcp-report-tools');
|
|
28
29
|
const { makePrompts } = require('../src/mcp-prompts');
|
|
29
30
|
const { buildServerInstructions } = require('../src/mcp-instructions');
|
|
31
|
+
const { mcpNarration } = require('../src/trust-narration');
|
|
30
32
|
const { ensureFreshSession } = require('../src/session-refresh');
|
|
33
|
+
const { emitCommandAudit } = require('../src/command-audit');
|
|
31
34
|
const { createFunnel } = require('../src/onboarding-funnel');
|
|
32
35
|
|
|
36
|
+
function narrateTool(lang) {
|
|
37
|
+
return (name, { failed = false, args } = {}) => mcpNarration(narratedTool(name, args), narrationLanguage(name, args, lang), { failed });
|
|
38
|
+
}
|
|
39
|
+
|
|
33
40
|
let VERSION = '';
|
|
34
41
|
try {
|
|
35
42
|
VERSION = require('../package.json').version || '';
|
|
@@ -43,32 +50,32 @@ const HELP = `\n shakers mcp — local MCP server (stdio)\n\n`
|
|
|
43
50
|
+ ` Usage: shakers mcp\n\n`
|
|
44
51
|
+ ` This command speaks JSON-RPC on stdio; it is meant to be launched by an\n`
|
|
45
52
|
+ ` MCP client, not run by hand.\n`
|
|
46
|
-
+ ` Tools:
|
|
47
|
-
+ `
|
|
48
|
-
+ `
|
|
49
|
-
+ `
|
|
50
|
-
+ `
|
|
51
|
-
+ `
|
|
52
|
-
+ `
|
|
53
|
-
+ `
|
|
54
|
-
+ `
|
|
55
|
-
+ ` report, share,\n`
|
|
56
|
-
+ ` list_certifiable_dimensions, find_projects, show_project,\n`
|
|
57
|
-
+ ` save_project, unsave_project, list_invitations,\n`
|
|
58
|
-
+ ` list_applications, get_availability,\n`
|
|
53
|
+
+ ` Tools: signup (the whole sign-up, step by step), ai_usage, ai_usage_upload,\n`
|
|
54
|
+
+ ` ai_usage_result, logout, social_signin_start, social_signin_poll,\n`
|
|
55
|
+
+ ` list_addable, add_skill, add_agent, add_project, open_web,\n`
|
|
56
|
+
+ ` onboarding_interview_start, onboarding_interview_turn,\n`
|
|
57
|
+
+ ` onboarding_interview_complete, repeat_onboarding_interview,\n`
|
|
58
|
+
+ ` onboarding_finish, list_available_roles, add_role, list_my_roles,\n`
|
|
59
|
+
+ ` set_main_role, report, share, list_certifiable_dimensions,\n`
|
|
60
|
+
+ ` find_projects, show_project, save_project, unsave_project,\n`
|
|
61
|
+
+ ` list_invitations, list_applications, get_availability,\n`
|
|
59
62
|
+ ` list_certifications, get_profile, get_rate, get_languages,\n`
|
|
60
63
|
+ ` list_socials, list_experiences, list_portfolios, alma_chat, alma_decision.\n\n`;
|
|
61
64
|
|
|
62
65
|
// The LOCAL (filesystem/machine) tools — the only ones the .mcpb bundle exposes; the
|
|
63
66
|
// server-backed tools belong to the remote connector.
|
|
64
|
-
const LOCAL_ONLY_TOOL_NAMES = new Set(['ai_usage', 'ai_usage_result', 'read_cv', 'suggest_register_context']);
|
|
67
|
+
const LOCAL_ONLY_TOOL_NAMES = new Set(['ai_usage', 'ai_usage_upload', 'ai_usage_result', 'read_cv', 'suggest_register_context']);
|
|
65
68
|
|
|
66
69
|
// `localOnly` builds the scan-only server for the Claude .mcpb bundle, reusing the SAME
|
|
67
70
|
// tool registry (just filtered) — no duplicate registration.
|
|
68
71
|
function buildServer({ localOnly = false, funnel = createFunnel({ surface: 'mcp' }), makeInterviewClient = null } = {}) {
|
|
69
72
|
const lang = detectFlowLang();
|
|
70
|
-
const
|
|
71
|
-
const
|
|
73
|
+
const aiUsage = makeAiUsageTools();
|
|
74
|
+
const allTools = [aiUsage.usage, aiUsage.upload, makeAiUsageResultTool(), ...makeOnboardingTools(), ...makeRoleTools({ lang, funnel }), ...makeDeviceLoginTools({ lang, funnel }), ...makeSignupTools({ lang, funnel }), ...makeRegisterTools({ lang, funnel, flowDeps: { makeInterviewClient } }), ...makeReportTools(), ...makeCertifyDimensionTools(), ...makeFindProjectsTools(), ...makeShowProjectTools(), ...makeSavedPositionsTools(), ...makeInvitationsTools(), ...makeApplicationsTools(), ...makeAvailabilityTools(), ...makeCertificationsTools(), ...makeProfileTools(), ...makeRateTools(), ...makeLangTools(), ...makeSocialsTools(), ...makePortfolioTools(), ...makeAlmaTools()];
|
|
75
|
+
// The sign-up runs through one tool (`signup`), so Claude Desktop asks the talent once instead of once per step.
|
|
76
|
+
const tools = localOnly
|
|
77
|
+
? allTools.filter((t) => LOCAL_ONLY_TOOL_NAMES.has(t.name))
|
|
78
|
+
: [makeSignupTool(allTools), ...allTools.filter((t) => !ABSORBED_TOOLS.has(t.name)).map(outsideSignup)];
|
|
72
79
|
// Slash-command prompts, kept to the tools actually registered in this mode.
|
|
73
80
|
const prompts = makePrompts(new Set(tools.map((t) => t.name)), lang);
|
|
74
81
|
return createMcpServer({
|
|
@@ -76,10 +83,14 @@ function buildServer({ localOnly = false, funnel = createFunnel({ surface: 'mcp'
|
|
|
76
83
|
prompts,
|
|
77
84
|
serverInfo: { name: localOnly ? 'Shakers (local scan)' : 'Shakers', version: VERSION || '0.0.0' },
|
|
78
85
|
instructions: localOnly ? null : (client) => buildServerInstructions(client),
|
|
79
|
-
// Long-lived server: refresh the email session's hub JWT from the stored
|
|
80
|
-
// cookie before each tool call so a stale (~15-min) JWT never reaches a tool.
|
|
81
86
|
beforeCall: () => ensureFreshSession(process.env),
|
|
82
|
-
afterCall: () => {
|
|
87
|
+
afterCall: (name) => {
|
|
88
|
+
funnel.flush();
|
|
89
|
+
ensureFreshSession(process.env)
|
|
90
|
+
.catch(() => null)
|
|
91
|
+
.then(() => emitCommandAudit({ command: `mcp:${name}`, env: process.env }));
|
|
92
|
+
},
|
|
93
|
+
narrate: narrateTool(lang),
|
|
83
94
|
onInitialize: ({ clientInfo }) => funnel.setMcpClient(clientInfo.name),
|
|
84
95
|
});
|
|
85
96
|
}
|
|
@@ -121,7 +132,7 @@ async function run(argv = process.argv.slice(2), { makeInterviewClient = null }
|
|
|
121
132
|
await funnel.settle();
|
|
122
133
|
}
|
|
123
134
|
|
|
124
|
-
module.exports = { run, buildServer, buildServerInstructions, prepareProcess };
|
|
135
|
+
module.exports = { run, buildServer, buildServerInstructions, prepareProcess, narrateTool };
|
|
125
136
|
|
|
126
137
|
if (require.main === module) {
|
|
127
138
|
run();
|
package/bin/register.js
CHANGED
|
@@ -2,9 +2,7 @@
|
|
|
2
2
|
'use strict';
|
|
3
3
|
|
|
4
4
|
const { detectFlowLang, getCatalog } = require('../src/i18n');
|
|
5
|
-
const { loadAuthSession, sessionStatus } = require('../src/auth-session-store');
|
|
6
5
|
const { createStdinAsk } = require('../src/stdin-ask');
|
|
7
|
-
const { run: runLogin } = require('./login');
|
|
8
6
|
const { run: runAiUsage } = require('./ai-usage');
|
|
9
7
|
const {
|
|
10
8
|
runOnboarding, makeDeps, CURRENCIES, MONTHLY_HOURS, normalizeAmount,
|
|
@@ -20,6 +18,7 @@ const { BRAND_ANSI } = require('../src/brand-ansi');
|
|
|
20
18
|
const { typeOut } = require('../src/typewriter');
|
|
21
19
|
const { withSpinner } = require('../src/terminal-progress');
|
|
22
20
|
const { openPath } = require('../src/open-file');
|
|
21
|
+
const { termsText, legalCopy } = require('../src/signup-copy');
|
|
23
22
|
const { c: fmt } = require('../src/terminal-format');
|
|
24
23
|
const { createFunnel } = require('../src/onboarding-funnel');
|
|
25
24
|
const red = (s) => `${fmt.danger}${s}${fmt.reset}`;
|
|
@@ -122,9 +121,10 @@ function makeIo({ ask, lang, out, acceptDisclaimer, stdinIsTTY = false }) {
|
|
|
122
121
|
}
|
|
123
122
|
return { fullTimeSelected: true, fullTimeAmount, fullTimeCurrency, partTimeSelected, partTimeAmount, partTimeCurrency };
|
|
124
123
|
},
|
|
125
|
-
askAvailability: async (c) => {
|
|
124
|
+
askAvailability: async (c, { onLocation = () => {} } = {}) => {
|
|
126
125
|
const available = isYes(await ask(` ${c.availableAsk} `));
|
|
127
126
|
const monthlyHours = await promptSelect({ ask, stdinIsTTY, out: selectOut, items: MONTHLY_HOURS, header: ` ${c.monthlyHoursAsk}` });
|
|
127
|
+
onLocation();
|
|
128
128
|
const workModes = await promptMultiSelect({
|
|
129
129
|
ask,
|
|
130
130
|
stdinIsTTY,
|
|
@@ -150,26 +150,11 @@ function makeIo({ ask, lang, out, acceptDisclaimer, stdinIsTTY = false }) {
|
|
|
150
150
|
const telephoneNumber = (await ask(` ${c.phoneNumberAsk} `)).trim();
|
|
151
151
|
return { available, monthlyHours, workModes, country, subdivision, timezone, longFullTimeProjects, telephoneCode, telephoneNumber };
|
|
152
152
|
},
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
const
|
|
157
|
-
|
|
158
|
-
let password;
|
|
159
|
-
for (;;) {
|
|
160
|
-
password = await askSecret(c.askPassword);
|
|
161
|
-
const again = await askSecret(c.askPasswordConfirm);
|
|
162
|
-
if (password === again) break;
|
|
163
|
-
writeErr(c.passwordMismatch);
|
|
164
|
-
}
|
|
165
|
-
const newsletterConsent = isYes(await ask(` ${c.askNewsletter} `));
|
|
166
|
-
// freelanceType is NOT asked here anymore: it is derived from the work
|
|
167
|
-
// situation (asked once, after the sources) and sent via professional-details.
|
|
168
|
-
return { name, lastName, email, password, newsletterConsent };
|
|
169
|
-
},
|
|
170
|
-
askGoogleSignUp: async () => {
|
|
171
|
-
// Nothing to ask: Google supplies name/email; freelanceType is derived later.
|
|
172
|
-
return {};
|
|
153
|
+
askTerms: async (c) => {
|
|
154
|
+
out(`\n ${termsText(lang, (label, url) => `${label} (${url})`)}\n`);
|
|
155
|
+
if (!isYes(await ask(` ${c.termsAsk} `))) return { accepted: false };
|
|
156
|
+
const newsletterConsent = isYes(await ask(` ${c.newsletterAsk(legalCopy(lang).newsletter)} `));
|
|
157
|
+
return { accepted: true, newsletterConsent };
|
|
173
158
|
},
|
|
174
159
|
askWorkSituation: async (c) => {
|
|
175
160
|
const situation = await promptSelect({
|
|
@@ -241,13 +226,9 @@ function makeIo({ ask, lang, out, acceptDisclaimer, stdinIsTTY = false }) {
|
|
|
241
226
|
}
|
|
242
227
|
return rows;
|
|
243
228
|
},
|
|
244
|
-
|
|
229
|
+
onAuthUrl: (url, userCode) => {
|
|
245
230
|
const g = getCatalog(lang).login;
|
|
246
|
-
out(`\n ${g.deviceVisit}\n ${url}\n\n ${g.deviceCodeLabel(userCode)}\n\n ${g.deviceWaiting}\n`);
|
|
247
|
-
},
|
|
248
|
-
login: async () => {
|
|
249
|
-
await runLogin(['--lang', lang], { ask });
|
|
250
|
-
return sessionStatus(loadAuthSession()) === 'active';
|
|
231
|
+
out(`\n ${g.deviceTrust}\n\n ${g.deviceVisit}\n ${url}\n\n ${g.deviceCodeLabel(userCode)}\n\n ${g.deviceWaiting}\n`);
|
|
251
232
|
},
|
|
252
233
|
// Registration always measures the whole machine (--all-repos).
|
|
253
234
|
runUsage: async () => { await runAiUsage(['--lang', lang, '--all-repos'], { ask, bypassThrottle: true, inRegistration: true }); },
|
|
@@ -331,7 +312,8 @@ async function run(
|
|
|
331
312
|
const io = makeIo({ ask, lang, out: write, acceptDisclaimer: o.acceptDisclaimer, stdinIsTTY });
|
|
332
313
|
// At a terminal the profile opens in the browser at the end; piped runs, tests and --no-open only print it.
|
|
333
314
|
const openProfile = stdinIsTTY && !o.noOpen ? (openBrowser || openPath) : undefined;
|
|
334
|
-
const
|
|
315
|
+
const openAuth = openBrowser || (deps || o.noOpen ? undefined : openPath);
|
|
316
|
+
const result = await runOnboarding(io, deps || makeDeps({ makeInterviewClient: createInterviewClient }), { method, openBrowser: openAuth, onAuthUrl: io.onAuthUrl, openProfile, funnel: registrationFunnel });
|
|
335
317
|
if (!result.ok) {
|
|
336
318
|
process.stderr.write(`\n ${red(`register: ${result.reason}${result.step ? ` (${result.step})` : ''}`)}\n\n`);
|
|
337
319
|
process.exitCode = 1;
|
package/bin/shakers-cli.js
CHANGED
|
@@ -37,9 +37,10 @@ const { run: runOnboarding } = require('./onboarding');
|
|
|
37
37
|
const { run: runConfig } = require('./config');
|
|
38
38
|
const { run: runUpdate } = require('./update');
|
|
39
39
|
const { checkOnboardingCompleted } = require('../src/onboarding-availability');
|
|
40
|
-
const {
|
|
40
|
+
const { startCommandAudit } = require('../src/command-audit');
|
|
41
41
|
const { readPackageInfo } = require('../src/update-flow');
|
|
42
42
|
const { isSuppressed, maybeNotify, triggerBackgroundCheck } = require('../src/update-notifier');
|
|
43
|
+
const { cliNarration, changedNothing } = require('../src/trust-narration');
|
|
43
44
|
|
|
44
45
|
let VERSION = '';
|
|
45
46
|
try {
|
|
@@ -92,10 +93,11 @@ const HANDLERS = {
|
|
|
92
93
|
login: runLogin,
|
|
93
94
|
logout: runLogout,
|
|
94
95
|
superadmin: runSuperadmin,
|
|
95
|
-
mcp: () => require('../src/mcp-install').runMcpInstall(),
|
|
96
|
+
mcp: (args) => require('../src/mcp-install').runMcpInstall({ args }),
|
|
97
|
+
skill: (args) => require('../src/skill-install').runSkillInstall({ args }),
|
|
96
98
|
};
|
|
97
99
|
|
|
98
|
-
const SESSION_EXEMPT = new Set(['ai-usage', 'register', 'config', 'login', 'logout', 'superadmin', 'mcp', 'share', 'alma', 'update']);
|
|
100
|
+
const SESSION_EXEMPT = new Set(['ai-usage', 'register', 'config', 'login', 'logout', 'superadmin', 'mcp', 'skill', 'share', 'alma', 'update']);
|
|
99
101
|
|
|
100
102
|
// Command FAMILIES — a navigation layer over the flat commands (which all stay
|
|
101
103
|
// valid as aliases). `shakers <family> <sub>` resolves to the flat handler; every
|
|
@@ -178,7 +180,8 @@ function usageText(lang, { hideOnboarding = false } = {}) {
|
|
|
178
180
|
+ ` Público (sin sesión):\n`
|
|
179
181
|
+ ` ai-usage Escanea este proyecto + tu máquina y puntúa tu setup de IA\n`
|
|
180
182
|
+ ` register Registro completo: perfil, precio, uso de IA y entrevista\n`
|
|
181
|
-
+ ` mcp Servidor MCP local (stdio) para apps de IA de escritorio\n
|
|
183
|
+
+ ` mcp Servidor MCP local (stdio) para apps de IA de escritorio\n`
|
|
184
|
+
+ ` skill install Instala la skill shakers-cli en Claude Code y Codex\n\n`
|
|
182
185
|
+ ` Requieren sesión (ejecuta antes: shakers login):\n`
|
|
183
186
|
+ onboardingLine
|
|
184
187
|
+ ` certify Certifica una dimensión de tu rol (entrevista por LiveKit)\n`
|
|
@@ -222,7 +225,8 @@ function usageText(lang, { hideOnboarding = false } = {}) {
|
|
|
222
225
|
+ ` Public (no session):\n`
|
|
223
226
|
+ ` ai-usage Scan this project + machine and score your AI setup\n`
|
|
224
227
|
+ ` register Full registration: profile, pricing, AI usage and interview\n`
|
|
225
|
-
+ ` mcp Local MCP server (stdio) for desktop AI apps\n
|
|
228
|
+
+ ` mcp Local MCP server (stdio) for desktop AI apps\n`
|
|
229
|
+
+ ` skill install Install the shakers-cli skill into Claude Code and Codex\n\n`
|
|
226
230
|
+ ` Require a session (run \`shakers login\` first):\n`
|
|
227
231
|
+ onboardingLine
|
|
228
232
|
+ ` certify Certify a dimension of your role (LiveKit interview)\n`
|
|
@@ -287,15 +291,13 @@ async function main() {
|
|
|
287
291
|
|
|
288
292
|
migrateLegacyConfigDir(process.env);
|
|
289
293
|
sanitizeStaleBakedEndpoints(process.env);
|
|
290
|
-
// Mint a fresh hub JWT from the stored cookie once per command (email sessions only; a no-op for Google/legacy or when no session exists).
|
|
291
294
|
try { await ensureFreshSession(process.env); } catch { /* never blocks a command */ }
|
|
292
295
|
|
|
293
296
|
const token = argv.find((a) => !a.startsWith('-'));
|
|
294
297
|
|
|
295
|
-
// Single dispatch choke-point: audit EXACTLY once before branching, fire-and-forget (never awaited). MCP tool calls run in-process and never reach here.
|
|
296
298
|
const auditCommand =
|
|
297
299
|
!token || token.toLowerCase() === 'help' ? 'help' : token.toLowerCase();
|
|
298
|
-
|
|
300
|
+
startCommandAudit({ command: auditCommand, argv, env: process.env });
|
|
299
301
|
|
|
300
302
|
// Non-blocking update notice: reads a cached version synchronously, registry check runs in a detached worker; stderr-only, suppressed for --json/non-TTY/CI/update/opt-out.
|
|
301
303
|
if (!isSuppressed({ argv, command: auditCommand, env: process.env })) {
|
|
@@ -346,7 +348,10 @@ async function main() {
|
|
|
346
348
|
await remindAccountForAiUsage(lang, args);
|
|
347
349
|
}
|
|
348
350
|
|
|
349
|
-
|
|
351
|
+
const narration = wantsHelp || args.includes('--json') ? { before: null, after: null } : cliNarration(command, args, lang);
|
|
352
|
+
if (narration.before) process.stderr.write(`\n ${narration.before}\n`);
|
|
353
|
+
const outcome = await handler(args);
|
|
354
|
+
if (narration.after && !process.exitCode && !changedNothing(outcome)) process.stderr.write(`\n ${narration.after}\n\n`);
|
|
350
355
|
}
|
|
351
356
|
|
|
352
|
-
module.exports = { main };
|
|
357
|
+
module.exports = { main, HANDLERS };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "shakers-cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.18.0",
|
|
4
4
|
"description": "Shakers AI usage & skills evaluator: a local-first CLI (`shakers` binary) plus a local MCP server. Commands: ai-usage (scan + classify your AI-tool setup, T0-T7), certify (certify Skills from your code), share (branded LinkedIn card).",
|
|
5
5
|
"type": "commonjs",
|
|
6
6
|
"bin": {
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const crypto = require('crypto');
|
|
4
|
+
const fs = require('fs');
|
|
5
|
+
const path = require('path');
|
|
6
|
+
const { getConfigDir } = require('./config-dir');
|
|
7
|
+
|
|
8
|
+
const MARKER_FILE_NAME = 'account-marker.json';
|
|
9
|
+
const MARKER_FILE_VERSION = 1;
|
|
10
|
+
|
|
11
|
+
function accountMarkerPath(env = process.env) {
|
|
12
|
+
return path.join(getConfigDir(env), MARKER_FILE_NAME);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function hasAccountMarker(env = process.env, { fs: fsImpl = fs } = {}) {
|
|
16
|
+
return fsImpl.existsSync(accountMarkerPath(env));
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function recordAccountMarker(kind, env = process.env, { fs: fsImpl = fs } = {}) {
|
|
20
|
+
if (hasAccountMarker(env, { fs: fsImpl })) return;
|
|
21
|
+
const target = accountMarkerPath(env);
|
|
22
|
+
const dir = path.dirname(target);
|
|
23
|
+
const tmp = path.join(dir, `.${MARKER_FILE_NAME}.${process.pid}.${crypto.randomBytes(6).toString('hex')}.tmp`);
|
|
24
|
+
const payload = `${JSON.stringify({ version: MARKER_FILE_VERSION, kind, at: new Date().toISOString() }, null, 2)}\n`;
|
|
25
|
+
try {
|
|
26
|
+
fsImpl.mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
27
|
+
const fd = fsImpl.openSync(tmp, 'wx', 0o600);
|
|
28
|
+
try {
|
|
29
|
+
fsImpl.writeFileSync(fd, payload);
|
|
30
|
+
try { fsImpl.fsyncSync(fd); } catch {}
|
|
31
|
+
} finally {
|
|
32
|
+
fsImpl.closeSync(fd);
|
|
33
|
+
}
|
|
34
|
+
fsImpl.renameSync(tmp, target);
|
|
35
|
+
} catch {
|
|
36
|
+
try { fsImpl.unlinkSync(tmp); } catch {}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
module.exports = { hasAccountMarker, recordAccountMarker };
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { hasAuthSessionFile } = require('./auth-session-store');
|
|
4
|
+
const { loadConsentState } = require('./share-consent-state');
|
|
5
|
+
const { loadConfigFile } = require('./config');
|
|
6
|
+
const { hasAccountMarker, recordAccountMarker } = require('./account-marker');
|
|
7
|
+
|
|
8
|
+
function hasCurrentTrace(env) {
|
|
9
|
+
const consent = loadConsentState(env);
|
|
10
|
+
return hasAuthSessionFile(env)
|
|
11
|
+
|| !!(consent && consent.lastSentAt)
|
|
12
|
+
|| require('./report-store').hasRecordedUpload(env)
|
|
13
|
+
|| !!loadConfigFile(env).superadminSession;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function hasAccountTrace(env = process.env) {
|
|
17
|
+
if (hasAccountMarker(env)) return true;
|
|
18
|
+
if (!hasCurrentTrace(env)) return false;
|
|
19
|
+
recordAccountMarker('existing-trace', env);
|
|
20
|
+
return true;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
module.exports = { hasAccountTrace };
|
package/src/agents-client.js
CHANGED
|
@@ -54,6 +54,7 @@ async function requestListAgents({ accessToken, hubAccessToken } = {}, { endpoin
|
|
|
54
54
|
.map((entry) => ({
|
|
55
55
|
name: entry && typeof entry.name === 'string' ? entry.name : null,
|
|
56
56
|
id: entry && (typeof entry.id === 'string' || typeof entry.id === 'number') ? String(entry.id) : null,
|
|
57
|
+
source: entry && typeof entry.source === 'string' ? entry.source : null,
|
|
57
58
|
}))
|
|
58
59
|
.filter((a) => a.name !== null);
|
|
59
60
|
return { ok: true, agents };
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const path = require('path');
|
|
4
|
+
const { derivePayload } = require('./share-payload');
|
|
5
|
+
const { classify } = require('./maturity');
|
|
6
|
+
const { reviewCopy } = require('./signup-copy');
|
|
7
|
+
const { sanitizeRenderText } = require('./sanitize-network-text');
|
|
8
|
+
|
|
9
|
+
function repoName(repo) {
|
|
10
|
+
return path.basename(String(repo)) || String(repo);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function uploadFacts({ report, maturity, repos = [] }) {
|
|
14
|
+
const payload = derivePayload(report, maturity);
|
|
15
|
+
const sessions = payload.sessions || {};
|
|
16
|
+
const conversationExcerpts =
|
|
17
|
+
(payload.steering ? payload.steering.redactedSteeringExcerpts.length : 0)
|
|
18
|
+
+ (payload.decisions ? payload.decisions.redactedDecisionExcerpts.length : 0);
|
|
19
|
+
const commandExcerpts = Array.isArray(sessions.redactedCommandSample) ? sessions.redactedCommandSample.length : 0;
|
|
20
|
+
return {
|
|
21
|
+
tools: (report.tools || []).filter((t) => t && t.detected).map((t) => t.name || t.id),
|
|
22
|
+
technologies: payload.technologies.filter((t) => typeof t === 'string'),
|
|
23
|
+
sessionCount: sessions.sessionCount || 0,
|
|
24
|
+
activeDaysPerWeek: typeof sessions.activeDaysPerWeek === 'number' ? sessions.activeDaysPerWeek : null,
|
|
25
|
+
commitCount: payload.gitActivity ? payload.gitActivity.commitCount : 0,
|
|
26
|
+
repos: repos.map(repoName),
|
|
27
|
+
agents: payload.agents.map((a) => a.name),
|
|
28
|
+
agentDefinitions: payload.agents.filter((a) => a.definition).length,
|
|
29
|
+
agentSummaries: payload.agentSynthesis.filter((a) => a.whatItDoes || a.symbolicName).length,
|
|
30
|
+
conversationExcerpts,
|
|
31
|
+
commandExcerpts,
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function keepNamed(list, excluded) {
|
|
36
|
+
return Array.isArray(list) ? list.filter((x) => !(x && excluded.has(x.name))) : list;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function withoutExcluded(report, { agents = [], excerpts = true } = {}) {
|
|
40
|
+
const excluded = new Set(agents);
|
|
41
|
+
const next = structuredClone(report);
|
|
42
|
+
if (excluded.size > 0) {
|
|
43
|
+
next.agents = keepNamed(next.agents, excluded)
|
|
44
|
+
.map((a) => (a && excluded.has(a.parent) ? { ...a, parent: null } : a));
|
|
45
|
+
next.agentDefinitions = keepNamed(next.agentDefinitions, excluded);
|
|
46
|
+
next.agentDescriptions = keepNamed(next.agentDescriptions, excluded);
|
|
47
|
+
if (next.agentEvaluation) next.agentEvaluation.evaluations = keepNamed(next.agentEvaluation.evaluations, excluded);
|
|
48
|
+
if (next.agentSynthesis) next.agentSynthesis.agents = keepNamed(next.agentSynthesis.agents, excluded);
|
|
49
|
+
if (next.detectionEvidence) next.detectionEvidence.agents = keepNamed(next.detectionEvidence.agents, excluded);
|
|
50
|
+
if (next.agentUsage && next.agentUsage.byAgent) {
|
|
51
|
+
for (const name of excluded) delete next.agentUsage.byAgent[name];
|
|
52
|
+
}
|
|
53
|
+
if (next.agentCounts) next.agentCounts.agents = Array.isArray(next.agents) ? next.agents.length : 0;
|
|
54
|
+
}
|
|
55
|
+
if (!excerpts) {
|
|
56
|
+
if (next.steering) next.steering.redactedSteeringExcerpts = [];
|
|
57
|
+
if (next.decisions) next.decisions.redactedDecisionExcerpts = [];
|
|
58
|
+
if (next.sessions) next.sessions.redactedCommandSample = [];
|
|
59
|
+
}
|
|
60
|
+
return { report: next, maturity: classify(next) };
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function joined(list) {
|
|
64
|
+
return list.join(', ');
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function summaryText(lang, facts, { sent = false } = {}) {
|
|
68
|
+
const c = reviewCopy(lang);
|
|
69
|
+
return [
|
|
70
|
+
sent ? c.sentTitle : c.title,
|
|
71
|
+
facts.tools.length ? c.tools(joined(facts.tools)) : null,
|
|
72
|
+
facts.technologies.length ? c.technologies(joined(facts.technologies)) : null,
|
|
73
|
+
c.activity(facts.sessionCount, facts.activeDaysPerWeek),
|
|
74
|
+
facts.repos.length ? c.repos(joined(facts.repos), facts.commitCount) : null,
|
|
75
|
+
facts.agents.length ? c.agents(joined(facts.agents), facts.agentDefinitions, facts.agentSummaries) : null,
|
|
76
|
+
facts.conversationExcerpts + facts.commandExcerpts > 0 ? c.excerpts(facts.conversationExcerpts, facts.commandExcerpts) : c.noExcerpts,
|
|
77
|
+
c.notSent,
|
|
78
|
+
].filter(Boolean).map((line, i) => (i === 0 ? line : `- ${line}`)).join('\n');
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function explanationText(lang) {
|
|
82
|
+
const c = reviewCopy(lang);
|
|
83
|
+
return [c.explanationTitle, ...c.explanation, c.afterwards].join('\n\n');
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// `explained`: the talent already read what the two axes mean and how to disagree (the review in the sign-up), so they are not repeated.
|
|
87
|
+
function resultText(lang, preview, { explained = false } = {}) {
|
|
88
|
+
const c = reviewCopy(lang);
|
|
89
|
+
const after = explained ? [] : [c.afterwards];
|
|
90
|
+
if (!preview) return [c.resultPending, ...after].join('\n\n');
|
|
91
|
+
const m = preview.matrix || {};
|
|
92
|
+
const lines = [
|
|
93
|
+
c.resultTitle,
|
|
94
|
+
m.setupLevel ? c.setupLevel(c.levels[m.setupLevel] || m.setupLevel) : null,
|
|
95
|
+
preview.setup && preview.setup.explanation,
|
|
96
|
+
m.usageLevel ? c.usageLevel(c.levels[m.usageLevel] || m.usageLevel) : null,
|
|
97
|
+
preview.usage && preview.usage.explanation,
|
|
98
|
+
m.cell ? c.cell(m.cell) : null,
|
|
99
|
+
].filter(Boolean).map(sanitizeRenderText);
|
|
100
|
+
return [lines.join('\n'), ...(explained ? [] : [c.explanation[0]]), ...after].join('\n\n');
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
module.exports = { uploadFacts, withoutExcluded, repoName, summaryText, explanationText, resultText };
|