shakers-cli 0.17.0 → 0.19.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 +15 -5
- package/bin/add-agent.js +12 -6
- package/bin/ai-usage.js +2 -1
- package/bin/apply.js +66 -0
- package/bin/command-audit-worker.js +19 -0
- package/bin/login.js +4 -8
- package/bin/mcp.js +32 -19
- package/bin/register.js +10 -29
- package/bin/shakers-cli.js +21 -12
- package/package.json +1 -1
- package/src/agents-client.js +1 -0
- package/src/ai-usage-review.js +5 -3
- package/src/apply-client.js +164 -0
- package/src/apply-flow.js +105 -0
- package/src/auth-client.js +7 -107
- package/src/auth-session-store.js +6 -12
- package/src/backend-request.js +1 -24
- package/src/cli-skill.js +77 -0
- package/src/command-audit.js +58 -5
- package/src/config.js +15 -16
- package/src/consent-flow.js +7 -2
- package/src/detect-repos.js +4 -8
- package/src/detected-agents.js +40 -0
- 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 +258 -88
- package/src/mcp-ai-usage-result-tool.js +6 -4
- package/src/mcp-ai-usage-tool.js +73 -47
- package/src/mcp-apply-tools.js +152 -0
- 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 +15 -11
- package/src/mcp-instructions.js +43 -17
- package/src/mcp-onboarding-tools.js +90 -3
- package/src/mcp-prompts.js +6 -4
- package/src/mcp-register-tools.js +25 -14
- package/src/mcp-role-tools.js +35 -17
- 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 +237 -126
- package/src/onboarding-flow.js +30 -125
- package/src/opencode-sessions.js +92 -0
- package/src/profile-agents.js +26 -0
- 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-payload.js +4 -1
- package/src/share.js +2 -2
- package/src/signup-client.js +12 -39
- package/src/signup-copy.js +506 -68
- package/src/skill-install.js +102 -0
- package/src/start-add-agents.js +106 -3
- package/src/tier-engine.js +14 -2
- package/src/trust-narration.js +32 -0
- package/src/usage-run-steps.js +16 -4
- package/src/usage-scan.js +3 -0
- package/src/brand-svg.js +0 -9
- package/src/inter-fonts.js +0 -6
- package/src/mcp-auth-form.js +0 -484
package/README.md
CHANGED
|
@@ -69,11 +69,21 @@ Run `shakers` with no command to see every command.
|
|
|
69
69
|
can leave out agents and text excerpts there. `--consent-revoke` stops future uploads
|
|
70
70
|
from this computer; it does not delete what was already uploaded. To ask for that, write
|
|
71
71
|
to talent@shakersworks.com.
|
|
72
|
-
- What is
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
your
|
|
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.
|
|
77
87
|
- Certification sends secret-scrubbed samples of your code and your interview answers to
|
|
78
88
|
Shakers to assess them.
|
|
79
89
|
- Read the full policy at <https://www.shakersworks.com/en/politica-de-privacidad>.
|
package/bin/add-agent.js
CHANGED
|
@@ -11,19 +11,21 @@ const VALID_LANGS = new Set(['es', 'en']);
|
|
|
11
11
|
|
|
12
12
|
const COPY = {
|
|
13
13
|
es: {
|
|
14
|
-
help: 'add-agent — añade a tu perfil de Talento un agente de tu inventario descubierto. Uso: add-agent [--lang es|en] [--accept-disclaimer]',
|
|
14
|
+
help: 'add-agent — añade a tu perfil de Talento un agente de tu inventario descubierto, o todos los nuevos con --all. Uso: add-agent [--all [--yes]] [--lang es|en] [--accept-disclaimer]',
|
|
15
15
|
gate: '"add-agent" requiere una sesión activa.\n Falta: iniciar sesión. Ejecuta primero: shakers login',
|
|
16
16
|
missingAccept: 'Modo no interactivo: falta --accept-disclaimer.\n add-agent es un flujo interactivo (elegir agente, editar campos) y su borrado por IA envía datos.\n Reejecuta con --accept-disclaimer, aportando las respuestas por stdin.',
|
|
17
|
+
missingYes: 'Modo no interactivo: falta --yes.\n add-agent --all añade a tu perfil todos los agentes detectados que aún no tienes.\n Reejecuta con --all --yes para confirmarlo sin terminal.',
|
|
17
18
|
},
|
|
18
19
|
en: {
|
|
19
|
-
help: 'add-agent — add an agent from your discovered inventory to your Talent profile. Usage: add-agent [--lang es|en] [--accept-disclaimer]',
|
|
20
|
+
help: 'add-agent — add an agent from your discovered inventory to your Talent profile, or every new one with --all. Usage: add-agent [--all [--yes]] [--lang es|en] [--accept-disclaimer]',
|
|
20
21
|
gate: '"add-agent" requires an active session.\n Missing: sign-in. Run `shakers login` first.',
|
|
21
22
|
missingAccept: 'Non-interactive mode: --accept-disclaimer is missing.\n add-agent is an interactive flow (pick an agent, edit fields) and its AI draft sends data.\n Re-run with --accept-disclaimer, feeding the answers over stdin.',
|
|
23
|
+
missingYes: 'Non-interactive mode: --yes is missing.\n add-agent --all adds every detected agent you do not have yet to your profile.\n Re-run with --all --yes to confirm it without a terminal.',
|
|
22
24
|
},
|
|
23
25
|
};
|
|
24
26
|
|
|
25
27
|
function parseArgs(argv) {
|
|
26
|
-
const o = { root: null, lang: null, help: false, acceptDisclaimer: false };
|
|
28
|
+
const o = { root: null, lang: null, help: false, acceptDisclaimer: false, all: false, yes: false };
|
|
27
29
|
for (let i = 0; i < argv.length; i++) {
|
|
28
30
|
const a = argv[i];
|
|
29
31
|
if (a === '--root') o.root = argv[++i];
|
|
@@ -31,6 +33,8 @@ function parseArgs(argv) {
|
|
|
31
33
|
else if (a === '--lang') o.lang = VALID_LANGS.has(argv[++i]) ? argv[i] : null;
|
|
32
34
|
else if (a.startsWith('--lang=')) { const v = a.slice(7); o.lang = VALID_LANGS.has(v) ? v : null; }
|
|
33
35
|
else if (a === '--accept-disclaimer') o.acceptDisclaimer = true;
|
|
36
|
+
else if (a === '--all') o.all = true;
|
|
37
|
+
else if (a === '--yes' || a === '-y') o.yes = true;
|
|
34
38
|
else if (a === '--help' || a === '-h') o.help = true;
|
|
35
39
|
}
|
|
36
40
|
return o;
|
|
@@ -59,21 +63,23 @@ async function run(
|
|
|
59
63
|
|
|
60
64
|
const stdinIsTTY = ttyOverride !== undefined ? ttyOverride : !!process.stdin.isTTY;
|
|
61
65
|
|
|
62
|
-
if (!stdinIsTTY && !o.acceptDisclaimer) {
|
|
63
|
-
process.stderr.write(`\n ${c.missingAccept}\n\n`);
|
|
66
|
+
if (!stdinIsTTY && (o.all ? !o.yes : !o.acceptDisclaimer)) {
|
|
67
|
+
process.stderr.write(`\n ${o.all ? c.missingYes : c.missingAccept}\n\n`);
|
|
64
68
|
process.exitCode = 1;
|
|
65
69
|
return;
|
|
66
70
|
}
|
|
67
71
|
|
|
68
72
|
const ask = injectedAsk || createStdinAsk();
|
|
69
73
|
try {
|
|
70
|
-
await runAddAgents({
|
|
74
|
+
return await runAddAgents({
|
|
71
75
|
ask,
|
|
72
76
|
catalog,
|
|
73
77
|
root: o.root || process.cwd(),
|
|
74
78
|
session,
|
|
75
79
|
stdinIsTTY,
|
|
76
80
|
preAccepted: o.acceptDisclaimer,
|
|
81
|
+
all: o.all,
|
|
82
|
+
yes: o.yes,
|
|
77
83
|
deps: { runLogin },
|
|
78
84
|
});
|
|
79
85
|
} finally {
|
package/bin/ai-usage.js
CHANGED
|
@@ -17,6 +17,7 @@ const {
|
|
|
17
17
|
resolveAiConsent,
|
|
18
18
|
renderAndFinalize,
|
|
19
19
|
} = require('../src/usage-run-steps');
|
|
20
|
+
const { run: runLogin } = require('./login');
|
|
20
21
|
|
|
21
22
|
// Shared by the dispatcher and register flow: an injected `ask` is caller-owned, `bypassThrottle` is register-only.
|
|
22
23
|
async function run(argv = process.argv.slice(2), { ask: injectedAsk = null, bypassThrottle = false, inRegistration = false } = {}) {
|
|
@@ -59,7 +60,7 @@ async function run(argv = process.argv.slice(2), { ask: injectedAsk = null, bypa
|
|
|
59
60
|
},
|
|
60
61
|
});
|
|
61
62
|
|
|
62
|
-
await renderAndFinalize({ report, maturity, lang, opts, root, gate, catalog, bypassThrottle, repos, injectedAsk: ask });
|
|
63
|
+
await renderAndFinalize({ report, maturity, lang, opts, root, gate, catalog, bypassThrottle, repos, injectedAsk: ask, offerAgents: inRegistration ? null : { runLogin } });
|
|
63
64
|
} finally {
|
|
64
65
|
if (!injectedAsk) ask.close();
|
|
65
66
|
}
|
package/bin/apply.js
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
const { detectReportLang, getCatalog } = require('../src/i18n');
|
|
5
|
+
const { loadAuthSession, sessionStatus } = require('../src/auth-session-store');
|
|
6
|
+
const { createStdinAsk } = require('../src/stdin-ask');
|
|
7
|
+
const { makeIo } = require('./register');
|
|
8
|
+
const { runApply, makeApplyDeps } = require('../src/apply-flow');
|
|
9
|
+
|
|
10
|
+
function parseApplyArgs(argv = []) {
|
|
11
|
+
const o = { lang: null, help: false, id: null };
|
|
12
|
+
for (let i = 0; i < argv.length; i++) {
|
|
13
|
+
const a = argv[i];
|
|
14
|
+
if (a === '--help' || a === '-h') o.help = true;
|
|
15
|
+
else if (a === '--lang') { const v = argv[++i]; o.lang = v === 'es' || v === 'en' ? v : null; }
|
|
16
|
+
else if (a.startsWith('--lang=')) { const v = a.slice(7); o.lang = v === 'es' || v === 'en' ? v : null; }
|
|
17
|
+
else if (a === '--id') o.id = argv[++i] != null ? argv[i] : null;
|
|
18
|
+
else if (a.startsWith('--id=')) o.id = a.slice('--id='.length);
|
|
19
|
+
else if (!a.startsWith('-') && o.id == null) o.id = a;
|
|
20
|
+
}
|
|
21
|
+
return o;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
async function run(
|
|
25
|
+
argv = process.argv.slice(2),
|
|
26
|
+
{ ask: injectedAsk = null, stdinIsTTY: ttyOverride = undefined, deps = null, session: injectedSession = null, out = null, input, output } = {},
|
|
27
|
+
) {
|
|
28
|
+
const opts = parseApplyArgs(argv);
|
|
29
|
+
const lang = opts.lang || detectReportLang();
|
|
30
|
+
const catalog = getCatalog(lang);
|
|
31
|
+
const fp = catalog.apply;
|
|
32
|
+
const write = out || ((s) => process.stdout.write(s));
|
|
33
|
+
|
|
34
|
+
if (opts.help) {
|
|
35
|
+
write(fp.help + '\n');
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const session = injectedSession || loadAuthSession();
|
|
40
|
+
if (sessionStatus(session) !== 'active') {
|
|
41
|
+
process.stderr.write(`\n ${fp.loginRequired}\n\n`);
|
|
42
|
+
process.exitCode = 1;
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const stdinIsTTY = ttyOverride !== undefined ? ttyOverride : !!process.stdin.isTTY;
|
|
47
|
+
const ask = injectedAsk || createStdinAsk();
|
|
48
|
+
try {
|
|
49
|
+
const io = makeIo({ ask, lang, out: write, stdinIsTTY });
|
|
50
|
+
const result = await runApply({
|
|
51
|
+
io, ask, session, catalog,
|
|
52
|
+
opts: { id: opts.id, stdinIsTTY, input, output },
|
|
53
|
+
deps: deps || makeApplyDeps(),
|
|
54
|
+
});
|
|
55
|
+
if (result && result.ok === false) process.exitCode = 1;
|
|
56
|
+
return result;
|
|
57
|
+
} finally {
|
|
58
|
+
if (!injectedAsk) ask.close();
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
module.exports = { run, parseApplyArgs };
|
|
63
|
+
|
|
64
|
+
if (require.main === module) {
|
|
65
|
+
run();
|
|
66
|
+
}
|
|
@@ -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
|
@@ -8,6 +8,7 @@ const { makeCertifyDimensionTools } = require('../src/mcp-certify-dimension-tool
|
|
|
8
8
|
const { makeFindProjectsTools } = require('../src/mcp-find-projects-tools');
|
|
9
9
|
const { makeShowProjectTools } = require('../src/mcp-show-project-tools');
|
|
10
10
|
const { makeSavedPositionsTools } = require('../src/mcp-saved-positions-tools');
|
|
11
|
+
const { makeApplyTools } = require('../src/mcp-apply-tools');
|
|
11
12
|
const { makeInvitationsTools } = require('../src/mcp-invitations-tools');
|
|
12
13
|
const { makeApplicationsTools } = require('../src/mcp-applications-tools');
|
|
13
14
|
const { makeAvailabilityTools } = require('../src/mcp-availability-tools');
|
|
@@ -23,13 +24,20 @@ const { makeRoleTools } = require('../src/mcp-role-tools');
|
|
|
23
24
|
const { makeDeviceLoginTools } = require('../src/mcp-device-login-tools');
|
|
24
25
|
const { makeRegisterTools } = require('../src/mcp-register-tools');
|
|
25
26
|
const { makeSignupTools } = require('../src/mcp-signup-tools');
|
|
27
|
+
const { makeSignupTool, outsideSignup, narratedTool, narrationLanguage, ABSORBED_TOOLS } = require('../src/mcp-signup-flow');
|
|
26
28
|
const { detectFlowLang } = require('../src/i18n');
|
|
27
29
|
const { makeReportTools } = require('../src/mcp-report-tools');
|
|
28
30
|
const { makePrompts } = require('../src/mcp-prompts');
|
|
29
31
|
const { buildServerInstructions } = require('../src/mcp-instructions');
|
|
32
|
+
const { mcpNarration } = require('../src/trust-narration');
|
|
30
33
|
const { ensureFreshSession } = require('../src/session-refresh');
|
|
34
|
+
const { emitCommandAudit } = require('../src/command-audit');
|
|
31
35
|
const { createFunnel } = require('../src/onboarding-funnel');
|
|
32
36
|
|
|
37
|
+
function narrateTool(lang) {
|
|
38
|
+
return (name, { failed = false, args } = {}) => mcpNarration(narratedTool(name, args), narrationLanguage(name, args, lang), { failed });
|
|
39
|
+
}
|
|
40
|
+
|
|
33
41
|
let VERSION = '';
|
|
34
42
|
try {
|
|
35
43
|
VERSION = require('../package.json').version || '';
|
|
@@ -43,19 +51,17 @@ const HELP = `\n shakers mcp — local MCP server (stdio)\n\n`
|
|
|
43
51
|
+ ` Usage: shakers mcp\n\n`
|
|
44
52
|
+ ` This command speaks JSON-RPC on stdio; it is meant to be launched by an\n`
|
|
45
53
|
+ ` MCP client, not run by hand.\n`
|
|
46
|
-
+ ` Tools:
|
|
47
|
-
+ `
|
|
48
|
-
+ `
|
|
49
|
-
+ `
|
|
50
|
-
+ `
|
|
51
|
-
+ `
|
|
52
|
-
+ `
|
|
53
|
-
+ `
|
|
54
|
-
+ `
|
|
55
|
-
+ `
|
|
56
|
-
+ `
|
|
57
|
-
+ ` save_project, unsave_project, list_invitations,\n`
|
|
58
|
-
+ ` list_applications, get_availability,\n`
|
|
54
|
+
+ ` Tools: signup (the whole sign-up, step by step), ai_usage, ai_usage_upload,\n`
|
|
55
|
+
+ ` ai_usage_result, logout, social_signin_start, social_signin_poll,\n`
|
|
56
|
+
+ ` list_addable, add_skill, add_agent, add_detected_agents,\n`
|
|
57
|
+
+ ` add_project, open_web,\n`
|
|
58
|
+
+ ` onboarding_interview_start, onboarding_interview_turn,\n`
|
|
59
|
+
+ ` onboarding_interview_complete, repeat_onboarding_interview,\n`
|
|
60
|
+
+ ` onboarding_finish, list_available_roles, add_role, list_my_roles,\n`
|
|
61
|
+
+ ` set_main_role, report, share, list_certifiable_dimensions,\n`
|
|
62
|
+
+ ` find_projects, show_project, save_project, unsave_project,\n`
|
|
63
|
+
+ ` prepare_application, apply_to_project,\n`
|
|
64
|
+
+ ` list_invitations, list_applications, get_availability,\n`
|
|
59
65
|
+ ` list_certifications, get_profile, get_rate, get_languages,\n`
|
|
60
66
|
+ ` list_socials, list_experiences, list_portfolios, alma_chat, alma_decision.\n\n`;
|
|
61
67
|
|
|
@@ -68,8 +74,11 @@ const LOCAL_ONLY_TOOL_NAMES = new Set(['ai_usage', 'ai_usage_upload', 'ai_usage_
|
|
|
68
74
|
function buildServer({ localOnly = false, funnel = createFunnel({ surface: 'mcp' }), makeInterviewClient = null } = {}) {
|
|
69
75
|
const lang = detectFlowLang();
|
|
70
76
|
const aiUsage = makeAiUsageTools();
|
|
71
|
-
const allTools = [aiUsage.usage, aiUsage.upload, makeAiUsageResultTool(), ...makeOnboardingTools(), ...makeRoleTools({ lang, funnel }), ...makeDeviceLoginTools(), ...makeSignupTools({ lang, funnel }), ...makeRegisterTools({ lang, funnel, flowDeps: { makeInterviewClient } }), ...makeReportTools(), ...makeCertifyDimensionTools(), ...makeFindProjectsTools(), ...makeShowProjectTools(), ...makeSavedPositionsTools(), ...makeInvitationsTools(), ...makeApplicationsTools(), ...makeAvailabilityTools(), ...makeCertificationsTools(), ...makeProfileTools(), ...makeRateTools(), ...makeLangTools(), ...makeSocialsTools(), ...makePortfolioTools(), ...makeAlmaTools()];
|
|
72
|
-
|
|
77
|
+
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(), ...makeApplyTools(), ...makeInvitationsTools(), ...makeApplicationsTools(), ...makeAvailabilityTools(), ...makeCertificationsTools(), ...makeProfileTools(), ...makeRateTools(), ...makeLangTools(), ...makeSocialsTools(), ...makePortfolioTools(), ...makeAlmaTools()];
|
|
78
|
+
// The sign-up runs through one tool (`signup`), so Claude Desktop asks the talent once instead of once per step.
|
|
79
|
+
const tools = localOnly
|
|
80
|
+
? allTools.filter((t) => LOCAL_ONLY_TOOL_NAMES.has(t.name))
|
|
81
|
+
: [makeSignupTool(allTools), ...allTools.filter((t) => !ABSORBED_TOOLS.has(t.name)).map(outsideSignup)];
|
|
73
82
|
// Slash-command prompts, kept to the tools actually registered in this mode.
|
|
74
83
|
const prompts = makePrompts(new Set(tools.map((t) => t.name)), lang);
|
|
75
84
|
return createMcpServer({
|
|
@@ -77,10 +86,14 @@ function buildServer({ localOnly = false, funnel = createFunnel({ surface: 'mcp'
|
|
|
77
86
|
prompts,
|
|
78
87
|
serverInfo: { name: localOnly ? 'Shakers (local scan)' : 'Shakers', version: VERSION || '0.0.0' },
|
|
79
88
|
instructions: localOnly ? null : (client) => buildServerInstructions(client),
|
|
80
|
-
// Long-lived server: refresh the email session's hub JWT from the stored
|
|
81
|
-
// cookie before each tool call so a stale (~15-min) JWT never reaches a tool.
|
|
82
89
|
beforeCall: () => ensureFreshSession(process.env),
|
|
83
|
-
afterCall: () => {
|
|
90
|
+
afterCall: (name) => {
|
|
91
|
+
funnel.flush();
|
|
92
|
+
ensureFreshSession(process.env)
|
|
93
|
+
.catch(() => null)
|
|
94
|
+
.then(() => emitCommandAudit({ command: `mcp:${name}`, env: process.env }));
|
|
95
|
+
},
|
|
96
|
+
narrate: narrateTool(lang),
|
|
84
97
|
onInitialize: ({ clientInfo }) => funnel.setMcpClient(clientInfo.name),
|
|
85
98
|
});
|
|
86
99
|
}
|
|
@@ -122,7 +135,7 @@ async function run(argv = process.argv.slice(2), { makeInterviewClient = null }
|
|
|
122
135
|
await funnel.settle();
|
|
123
136
|
}
|
|
124
137
|
|
|
125
|
-
module.exports = { run, buildServer, buildServerInstructions, prepareProcess };
|
|
138
|
+
module.exports = { run, buildServer, buildServerInstructions, prepareProcess, narrateTool };
|
|
126
139
|
|
|
127
140
|
if (require.main === module) {
|
|
128
141
|
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}`;
|
|
@@ -151,26 +150,11 @@ function makeIo({ ask, lang, out, acceptDisclaimer, stdinIsTTY = false }) {
|
|
|
151
150
|
const telephoneNumber = (await ask(` ${c.phoneNumberAsk} `)).trim();
|
|
152
151
|
return { available, monthlyHours, workModes, country, subdivision, timezone, longFullTimeProjects, telephoneCode, telephoneNumber };
|
|
153
152
|
},
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
const
|
|
158
|
-
|
|
159
|
-
let password;
|
|
160
|
-
for (;;) {
|
|
161
|
-
password = await askSecret(c.askPassword);
|
|
162
|
-
const again = await askSecret(c.askPasswordConfirm);
|
|
163
|
-
if (password === again) break;
|
|
164
|
-
writeErr(c.passwordMismatch);
|
|
165
|
-
}
|
|
166
|
-
const newsletterConsent = isYes(await ask(` ${c.askNewsletter} `));
|
|
167
|
-
// freelanceType is NOT asked here anymore: it is derived from the work
|
|
168
|
-
// situation (asked once, after the sources) and sent via professional-details.
|
|
169
|
-
return { name, lastName, email, password, newsletterConsent };
|
|
170
|
-
},
|
|
171
|
-
askGoogleSignUp: async () => {
|
|
172
|
-
// Nothing to ask: Google supplies name/email; freelanceType is derived later.
|
|
173
|
-
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 };
|
|
174
158
|
},
|
|
175
159
|
askWorkSituation: async (c) => {
|
|
176
160
|
const situation = await promptSelect({
|
|
@@ -242,13 +226,9 @@ function makeIo({ ask, lang, out, acceptDisclaimer, stdinIsTTY = false }) {
|
|
|
242
226
|
}
|
|
243
227
|
return rows;
|
|
244
228
|
},
|
|
245
|
-
|
|
229
|
+
onAuthUrl: (url, userCode) => {
|
|
246
230
|
const g = getCatalog(lang).login;
|
|
247
|
-
out(`\n ${g.deviceVisit}\n ${url}\n\n ${g.deviceCodeLabel(userCode)}\n\n ${g.deviceWaiting}\n`);
|
|
248
|
-
},
|
|
249
|
-
login: async () => {
|
|
250
|
-
await runLogin(['--lang', lang], { ask });
|
|
251
|
-
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`);
|
|
252
232
|
},
|
|
253
233
|
// Registration always measures the whole machine (--all-repos).
|
|
254
234
|
runUsage: async () => { await runAiUsage(['--lang', lang, '--all-repos'], { ask, bypassThrottle: true, inRegistration: true }); },
|
|
@@ -332,7 +312,8 @@ async function run(
|
|
|
332
312
|
const io = makeIo({ ask, lang, out: write, acceptDisclaimer: o.acceptDisclaimer, stdinIsTTY });
|
|
333
313
|
// At a terminal the profile opens in the browser at the end; piped runs, tests and --no-open only print it.
|
|
334
314
|
const openProfile = stdinIsTTY && !o.noOpen ? (openBrowser || openPath) : undefined;
|
|
335
|
-
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 });
|
|
336
317
|
if (!result.ok) {
|
|
337
318
|
process.stderr.write(`\n ${red(`register: ${result.reason}${result.step ? ` (${result.step})` : ''}`)}\n\n`);
|
|
338
319
|
process.exitCode = 1;
|
package/bin/shakers-cli.js
CHANGED
|
@@ -21,6 +21,7 @@ const { run: runChangeRole } = require('./change-role');
|
|
|
21
21
|
const { run: runFindProjects } = require('./find-projects');
|
|
22
22
|
const { run: runShowProject } = require('./show-project');
|
|
23
23
|
const { run: runSave, runUnsave } = require('./save');
|
|
24
|
+
const { run: runApply } = require('./apply');
|
|
24
25
|
const { run: runInvitations } = require('./invitations');
|
|
25
26
|
const { run: runApplications } = require('./applications');
|
|
26
27
|
const { run: runAvailability } = require('./availability');
|
|
@@ -37,9 +38,10 @@ const { run: runOnboarding } = require('./onboarding');
|
|
|
37
38
|
const { run: runConfig } = require('./config');
|
|
38
39
|
const { run: runUpdate } = require('./update');
|
|
39
40
|
const { checkOnboardingCompleted } = require('../src/onboarding-availability');
|
|
40
|
-
const {
|
|
41
|
+
const { startCommandAudit } = require('../src/command-audit');
|
|
41
42
|
const { readPackageInfo } = require('../src/update-flow');
|
|
42
43
|
const { isSuppressed, maybeNotify, triggerBackgroundCheck } = require('../src/update-notifier');
|
|
44
|
+
const { cliNarration, changedNothing } = require('../src/trust-narration');
|
|
43
45
|
|
|
44
46
|
let VERSION = '';
|
|
45
47
|
try {
|
|
@@ -73,6 +75,7 @@ const HANDLERS = {
|
|
|
73
75
|
'show-project': runShowProject,
|
|
74
76
|
'save-project': runSave,
|
|
75
77
|
'unsave-project': runUnsave,
|
|
78
|
+
apply: runApply,
|
|
76
79
|
invitations: runInvitations,
|
|
77
80
|
applications: runApplications,
|
|
78
81
|
availability: runAvailability,
|
|
@@ -93,9 +96,10 @@ const HANDLERS = {
|
|
|
93
96
|
logout: runLogout,
|
|
94
97
|
superadmin: runSuperadmin,
|
|
95
98
|
mcp: (args) => require('../src/mcp-install').runMcpInstall({ args }),
|
|
99
|
+
skill: (args) => require('../src/skill-install').runSkillInstall({ args }),
|
|
96
100
|
};
|
|
97
101
|
|
|
98
|
-
const SESSION_EXEMPT = new Set(['ai-usage', 'register', 'config', 'login', 'logout', 'superadmin', 'mcp', 'share', 'alma', 'update']);
|
|
102
|
+
const SESSION_EXEMPT = new Set(['ai-usage', 'register', 'config', 'login', 'logout', 'superadmin', 'mcp', 'skill', 'share', 'alma', 'update']);
|
|
99
103
|
|
|
100
104
|
// Command FAMILIES — a navigation layer over the flat commands (which all stay
|
|
101
105
|
// valid as aliases). `shakers <family> <sub>` resolves to the flat handler; every
|
|
@@ -109,7 +113,7 @@ const FAMILIES = {
|
|
|
109
113
|
certifications: 'certifications',
|
|
110
114
|
},
|
|
111
115
|
projects: {
|
|
112
|
-
find: 'find-projects', show: 'show-project', save: 'save-project', unsave: 'unsave-project',
|
|
116
|
+
find: 'find-projects', show: 'show-project', save: 'save-project', unsave: 'unsave-project', apply: 'apply',
|
|
113
117
|
invitations: 'invitations', applications: 'applications',
|
|
114
118
|
},
|
|
115
119
|
usage: { run: 'ai-usage', report: 'report', share: 'share' },
|
|
@@ -178,7 +182,8 @@ function usageText(lang, { hideOnboarding = false } = {}) {
|
|
|
178
182
|
+ ` Público (sin sesión):\n`
|
|
179
183
|
+ ` ai-usage Escanea este proyecto + tu máquina y puntúa tu setup de IA\n`
|
|
180
184
|
+ ` register Registro completo: perfil, precio, uso de IA y entrevista\n`
|
|
181
|
-
+ ` mcp Servidor MCP local (stdio) para apps de IA de escritorio\n
|
|
185
|
+
+ ` mcp Servidor MCP local (stdio) para apps de IA de escritorio\n`
|
|
186
|
+
+ ` skill install Instala la skill shakers-cli en Claude Code y Codex\n\n`
|
|
182
187
|
+ ` Requieren sesión (ejecuta antes: shakers login):\n`
|
|
183
188
|
+ onboardingLine
|
|
184
189
|
+ ` certify Certifica una dimensión de tu rol (entrevista por LiveKit)\n`
|
|
@@ -190,6 +195,7 @@ function usageText(lang, { hideOnboarding = false } = {}) {
|
|
|
190
195
|
+ ` find-projects Lista los proyectos/posiciones disponibles para ti\n`
|
|
191
196
|
+ ` show-project Detalle de un proyecto/posición por id\n`
|
|
192
197
|
+ ` save-project/unsave-project Guarda o quita un proyecto de tus guardados\n`
|
|
198
|
+
+ ` apply Aplica a un proyecto/posición con tu mensaje y respuestas\n`
|
|
193
199
|
+ ` invitations Tus invitaciones a proyectos\n`
|
|
194
200
|
+ ` applications Proyectos a los que has aplicado + estado\n`
|
|
195
201
|
+ ` availability Ver o fijar tu disponibilidad\n`
|
|
@@ -211,7 +217,7 @@ function usageText(lang, { hideOnboarding = false } = {}) {
|
|
|
211
217
|
+ ` Familias (agrupan los comandos de arriba; los nombres planos siguen valiendo como alias):\n`
|
|
212
218
|
+ ` add <skill|agent|project|role> · role <add|change>\n`
|
|
213
219
|
+ ` profile <show|rate|languages|socials|experiences|portfolios|availability|certifications>\n`
|
|
214
|
-
+ ` projects <find|show|save|unsave|invitations|applications>\n`
|
|
220
|
+
+ ` projects <find|show|save|unsave|apply|invitations|applications>\n`
|
|
215
221
|
+ ` usage <run|report|share> · certs <list|certify> · account <login|logout|register|config|update>\n`
|
|
216
222
|
+ ` Ejecuta \`shakers <familia>\` (sin subcomando) para ver sus subcomandos.\n\n`
|
|
217
223
|
+ ` Ejecuta shakers <comando> --help para las opciones de cada comando.\n\n`;
|
|
@@ -222,7 +228,8 @@ function usageText(lang, { hideOnboarding = false } = {}) {
|
|
|
222
228
|
+ ` Public (no session):\n`
|
|
223
229
|
+ ` ai-usage Scan this project + machine and score your AI setup\n`
|
|
224
230
|
+ ` register Full registration: profile, pricing, AI usage and interview\n`
|
|
225
|
-
+ ` mcp Local MCP server (stdio) for desktop AI apps\n
|
|
231
|
+
+ ` mcp Local MCP server (stdio) for desktop AI apps\n`
|
|
232
|
+
+ ` skill install Install the shakers-cli skill into Claude Code and Codex\n\n`
|
|
226
233
|
+ ` Require a session (run \`shakers login\` first):\n`
|
|
227
234
|
+ onboardingLine
|
|
228
235
|
+ ` certify Certify a dimension of your role (LiveKit interview)\n`
|
|
@@ -234,6 +241,7 @@ function usageText(lang, { hideOnboarding = false } = {}) {
|
|
|
234
241
|
+ ` find-projects List the Projects/Positions available to you\n`
|
|
235
242
|
+ ` show-project Detail of a project/position by id\n`
|
|
236
243
|
+ ` save-project/unsave-project Save or remove a project from your saved list\n`
|
|
244
|
+
+ ` apply Apply to a project/position with your message and answers\n`
|
|
237
245
|
+ ` invitations Your project invitations\n`
|
|
238
246
|
+ ` applications Projects you applied to + status\n`
|
|
239
247
|
+ ` availability View or set your availability\n`
|
|
@@ -255,7 +263,7 @@ function usageText(lang, { hideOnboarding = false } = {}) {
|
|
|
255
263
|
+ ` Families (group the commands above; the flat names still work as aliases):\n`
|
|
256
264
|
+ ` add <skill|agent|project|role> · role <add|change>\n`
|
|
257
265
|
+ ` profile <show|rate|languages|socials|experiences|portfolios|availability|certifications>\n`
|
|
258
|
-
+ ` projects <find|show|save|unsave|invitations|applications>\n`
|
|
266
|
+
+ ` projects <find|show|save|unsave|apply|invitations|applications>\n`
|
|
259
267
|
+ ` usage <run|report|share> · certs <list|certify> · account <login|logout|register|config|update>\n`
|
|
260
268
|
+ ` Run \`shakers <family>\` (no subcommand) to list its subcommands.\n\n`
|
|
261
269
|
+ ` Run \`shakers <command> --help\` for a command's options.\n\n`;
|
|
@@ -287,15 +295,13 @@ async function main() {
|
|
|
287
295
|
|
|
288
296
|
migrateLegacyConfigDir(process.env);
|
|
289
297
|
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
298
|
try { await ensureFreshSession(process.env); } catch { /* never blocks a command */ }
|
|
292
299
|
|
|
293
300
|
const token = argv.find((a) => !a.startsWith('-'));
|
|
294
301
|
|
|
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
302
|
const auditCommand =
|
|
297
303
|
!token || token.toLowerCase() === 'help' ? 'help' : token.toLowerCase();
|
|
298
|
-
|
|
304
|
+
startCommandAudit({ command: auditCommand, argv, env: process.env });
|
|
299
305
|
|
|
300
306
|
// 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
307
|
if (!isSuppressed({ argv, command: auditCommand, env: process.env })) {
|
|
@@ -346,7 +352,10 @@ async function main() {
|
|
|
346
352
|
await remindAccountForAiUsage(lang, args);
|
|
347
353
|
}
|
|
348
354
|
|
|
349
|
-
|
|
355
|
+
const narration = wantsHelp || args.includes('--json') ? { before: null, after: null } : cliNarration(command, args, lang);
|
|
356
|
+
if (narration.before) process.stderr.write(`\n ${narration.before}\n`);
|
|
357
|
+
const outcome = await handler(args);
|
|
358
|
+
if (narration.after && !process.exitCode && !changedNothing(outcome)) process.stderr.write(`\n ${narration.after}\n\n`);
|
|
350
359
|
}
|
|
351
360
|
|
|
352
|
-
module.exports = { main };
|
|
361
|
+
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.19.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": {
|
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 };
|
package/src/ai-usage-review.js
CHANGED
|
@@ -83,9 +83,11 @@ function explanationText(lang) {
|
|
|
83
83
|
return [c.explanationTitle, ...c.explanation, c.afterwards].join('\n\n');
|
|
84
84
|
}
|
|
85
85
|
|
|
86
|
-
|
|
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 } = {}) {
|
|
87
88
|
const c = reviewCopy(lang);
|
|
88
|
-
|
|
89
|
+
const after = explained ? [] : [c.afterwards];
|
|
90
|
+
if (!preview) return [c.resultPending, ...after].join('\n\n');
|
|
89
91
|
const m = preview.matrix || {};
|
|
90
92
|
const lines = [
|
|
91
93
|
c.resultTitle,
|
|
@@ -95,7 +97,7 @@ function resultText(lang, preview) {
|
|
|
95
97
|
preview.usage && preview.usage.explanation,
|
|
96
98
|
m.cell ? c.cell(m.cell) : null,
|
|
97
99
|
].filter(Boolean).map(sanitizeRenderText);
|
|
98
|
-
return [lines.join('\n'), c.explanation[0],
|
|
100
|
+
return [lines.join('\n'), ...(explained ? [] : [c.explanation[0]]), ...after].join('\n\n');
|
|
99
101
|
}
|
|
100
102
|
|
|
101
103
|
module.exports = { uploadFacts, withoutExcluded, repoName, summaryText, explanationText, resultText };
|