shakers-cli 0.16.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 +11 -6
- package/bin/ai-usage.js +31 -12
- package/bin/mcp.js +5 -4
- package/bin/register.js +2 -1
- package/bin/shakers-cli.js +1 -1
- package/package.json +1 -1
- package/src/account-marker.js +40 -0
- package/src/account-trace.js +23 -0
- package/src/ai-usage-review.js +101 -0
- package/src/auth-session-store.js +7 -0
- package/src/backend-request.js +2 -0
- package/src/config.js +2 -0
- package/src/consent-flow.js +3 -2
- package/src/i18n.js +24 -14
- package/src/mcp-ai-usage-result-tool.js +34 -12
- package/src/mcp-ai-usage-tool.js +281 -162
- package/src/mcp-auth-form.js +6 -2
- package/src/mcp-install.js +279 -44
- package/src/mcp-instructions.js +1 -1
- package/src/mcp-prompts.js +2 -2
- package/src/mcp-role-tools.js +24 -1
- package/src/mcp-signup-tools.js +18 -12
- package/src/onboarding-flow.js +47 -26
- package/src/onboarding-funnel.js +46 -6
- 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/share-consent-state.js +2 -2
- package/src/signup-copy.js +176 -1
- package/src/stdin-ask.js +13 -1
- package/src/usage-run-steps.js +57 -4
- package/src/usage-scan.js +8 -8
package/README.md
CHANGED
|
@@ -64,14 +64,19 @@ 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 saved is mostly derived data (counts, categories, tier, detected technologies).
|
|
73
|
+
Two kinds of text travel too, and the review shows how many: the definition text of your
|
|
74
|
+
AI agents and short excerpts of what you ask the AI (200 characters at most), both with
|
|
75
|
+
secrets, paths and emails removed. Never your code, your full conversations, the names of
|
|
76
|
+
your repositories, environment variables or credentials.
|
|
72
77
|
- Certification sends secret-scrubbed samples of your code and your interview answers to
|
|
73
78
|
Shakers to assess them.
|
|
74
|
-
- Read the full policy at <https://shakersworks.com/
|
|
79
|
+
- Read the full policy at <https://www.shakersworks.com/en/politica-de-privacidad>.
|
|
75
80
|
|
|
76
81
|
## License
|
|
77
82
|
|
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 };
|
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');
|
|
@@ -43,7 +43,7 @@ const HELP = `\n shakers mcp — local MCP server (stdio)\n\n`
|
|
|
43
43
|
+ ` Usage: shakers mcp\n\n`
|
|
44
44
|
+ ` This command speaks JSON-RPC on stdio; it is meant to be launched by an\n`
|
|
45
45
|
+ ` MCP client, not run by hand.\n`
|
|
46
|
-
+ ` Tools: ai_usage, ai_usage_result, logout, social_signin_start, social_signin_poll,\n`
|
|
46
|
+
+ ` Tools: ai_usage, ai_usage_upload, ai_usage_result, logout, social_signin_start, social_signin_poll,\n`
|
|
47
47
|
+ ` list_addable, add_skill, add_agent,\n`
|
|
48
48
|
+ ` add_project, signup_start, signup_email, signup_draft, signup_create_account,\n`
|
|
49
49
|
+ ` signup_status, update_existing_profile, import_profile, save_profile_details, open_web,\n`
|
|
@@ -61,13 +61,14 @@ const HELP = `\n shakers mcp — local MCP server (stdio)\n\n`
|
|
|
61
61
|
|
|
62
62
|
// The LOCAL (filesystem/machine) tools — the only ones the .mcpb bundle exposes; the
|
|
63
63
|
// 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']);
|
|
64
|
+
const LOCAL_ONLY_TOOL_NAMES = new Set(['ai_usage', 'ai_usage_upload', 'ai_usage_result', 'read_cv', 'suggest_register_context']);
|
|
65
65
|
|
|
66
66
|
// `localOnly` builds the scan-only server for the Claude .mcpb bundle, reusing the SAME
|
|
67
67
|
// tool registry (just filtered) — no duplicate registration.
|
|
68
68
|
function buildServer({ localOnly = false, funnel = createFunnel({ surface: 'mcp' }), makeInterviewClient = null } = {}) {
|
|
69
69
|
const lang = detectFlowLang();
|
|
70
|
-
const
|
|
70
|
+
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()];
|
|
71
72
|
const tools = localOnly ? allTools.filter((t) => LOCAL_ONLY_TOOL_NAMES.has(t.name)) : allTools;
|
|
72
73
|
// Slash-command prompts, kept to the tools actually registered in this mode.
|
|
73
74
|
const prompts = makePrompts(new Set(tools.map((t) => t.name)), lang);
|
package/bin/register.js
CHANGED
|
@@ -122,9 +122,10 @@ function makeIo({ ask, lang, out, acceptDisclaimer, stdinIsTTY = false }) {
|
|
|
122
122
|
}
|
|
123
123
|
return { fullTimeSelected: true, fullTimeAmount, fullTimeCurrency, partTimeSelected, partTimeAmount, partTimeCurrency };
|
|
124
124
|
},
|
|
125
|
-
askAvailability: async (c) => {
|
|
125
|
+
askAvailability: async (c, { onLocation = () => {} } = {}) => {
|
|
126
126
|
const available = isYes(await ask(` ${c.availableAsk} `));
|
|
127
127
|
const monthlyHours = await promptSelect({ ask, stdinIsTTY, out: selectOut, items: MONTHLY_HOURS, header: ` ${c.monthlyHoursAsk}` });
|
|
128
|
+
onLocation();
|
|
128
129
|
const workModes = await promptMultiSelect({
|
|
129
130
|
ask,
|
|
130
131
|
stdinIsTTY,
|
package/bin/shakers-cli.js
CHANGED
|
@@ -92,7 +92,7 @@ const HANDLERS = {
|
|
|
92
92
|
login: runLogin,
|
|
93
93
|
logout: runLogout,
|
|
94
94
|
superadmin: runSuperadmin,
|
|
95
|
-
mcp: () => require('../src/mcp-install').runMcpInstall(),
|
|
95
|
+
mcp: (args) => require('../src/mcp-install').runMcpInstall({ args }),
|
|
96
96
|
};
|
|
97
97
|
|
|
98
98
|
const SESSION_EXEMPT = new Set(['ai-usage', 'register', 'config', 'login', 'logout', 'superadmin', 'mcp', 'share', 'alma', 'update']);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "shakers-cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.17.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 };
|
|
@@ -0,0 +1,101 @@
|
|
|
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
|
+
function resultText(lang, preview) {
|
|
87
|
+
const c = reviewCopy(lang);
|
|
88
|
+
if (!preview) return [c.resultPending, c.afterwards].join('\n\n');
|
|
89
|
+
const m = preview.matrix || {};
|
|
90
|
+
const lines = [
|
|
91
|
+
c.resultTitle,
|
|
92
|
+
m.setupLevel ? c.setupLevel(c.levels[m.setupLevel] || m.setupLevel) : null,
|
|
93
|
+
preview.setup && preview.setup.explanation,
|
|
94
|
+
m.usageLevel ? c.usageLevel(c.levels[m.usageLevel] || m.usageLevel) : null,
|
|
95
|
+
preview.usage && preview.usage.explanation,
|
|
96
|
+
m.cell ? c.cell(m.cell) : null,
|
|
97
|
+
].filter(Boolean).map(sanitizeRenderText);
|
|
98
|
+
return [lines.join('\n'), c.explanation[0], c.afterwards].join('\n\n');
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
module.exports = { uploadFacts, withoutExcluded, repoName, summaryText, explanationText, resultText };
|
|
@@ -4,6 +4,7 @@ const crypto = require('crypto');
|
|
|
4
4
|
const fs = require('fs');
|
|
5
5
|
const path = require('path');
|
|
6
6
|
const { getConfigDir } = require('./config-dir');
|
|
7
|
+
const { recordAccountMarker } = require('./account-marker');
|
|
7
8
|
|
|
8
9
|
// `~/.config/shakers/auth-session.json` — the logged-in Talent's session.
|
|
9
10
|
|
|
@@ -141,6 +142,7 @@ function saveAuthSession(session, env = process.env, { fs: fsImpl = fs } = {}) {
|
|
|
141
142
|
try { fsImpl.unlinkSync(tmp); } catch { /* never existed */ }
|
|
142
143
|
throw e;
|
|
143
144
|
}
|
|
145
|
+
recordAccountMarker(cookie ? 'email' : sessionToken ? 'device' : 'token', env, { fs: fsImpl });
|
|
144
146
|
return target;
|
|
145
147
|
}
|
|
146
148
|
|
|
@@ -174,6 +176,10 @@ function loadAuthSession(env = process.env, { fs: fsImpl = fs } = {}) {
|
|
|
174
176
|
};
|
|
175
177
|
}
|
|
176
178
|
|
|
179
|
+
function hasAuthSessionFile(env = process.env, { fs: fsImpl = fs } = {}) {
|
|
180
|
+
return fsImpl.existsSync(authSessionPath(env));
|
|
181
|
+
}
|
|
182
|
+
|
|
177
183
|
// Removes the file. Idempotent and never throws — `logout` on no session is a
|
|
178
184
|
// no-op, not a failure.
|
|
179
185
|
function clearAuthSession(env = process.env, { fs: fsImpl = fs } = {}) {
|
|
@@ -207,6 +213,7 @@ module.exports = {
|
|
|
207
213
|
authSessionPath,
|
|
208
214
|
saveAuthSession,
|
|
209
215
|
loadAuthSession,
|
|
216
|
+
hasAuthSessionFile,
|
|
210
217
|
clearAuthSession,
|
|
211
218
|
sessionStatus,
|
|
212
219
|
isLoggedIn,
|
package/src/backend-request.js
CHANGED
|
@@ -71,6 +71,7 @@ function postJsonWithTimeout(url, body, timeoutMs, method = 'POST', idempotencyK
|
|
|
71
71
|
...sanitizeExtraHeaders(extraHeaders),
|
|
72
72
|
},
|
|
73
73
|
timeout: timeoutMs,
|
|
74
|
+
agent: false,
|
|
74
75
|
},
|
|
75
76
|
(res) => {
|
|
76
77
|
let raw = '';
|
|
@@ -151,6 +152,7 @@ function postMultipartWithTimeout(url, fields, timeoutMs, method = 'POST', extra
|
|
|
151
152
|
...sanitizeExtraHeaders(extraHeaders),
|
|
152
153
|
},
|
|
153
154
|
timeout: timeoutMs,
|
|
155
|
+
agent: false,
|
|
154
156
|
},
|
|
155
157
|
(res) => {
|
|
156
158
|
let raw = '';
|
package/src/config.js
CHANGED
|
@@ -4,6 +4,7 @@ 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
9
|
// Persistent CLI config file.
|
|
9
10
|
function configDir(env = process.env) {
|
|
@@ -815,6 +816,7 @@ function saveSuperadminSession(session, env = process.env) {
|
|
|
815
816
|
expiresAt: session.expiresAt || null,
|
|
816
817
|
};
|
|
817
818
|
saveConfigFile(config, env);
|
|
819
|
+
recordAccountMarker('superadmin', env);
|
|
818
820
|
}
|
|
819
821
|
|
|
820
822
|
function clearSuperadminSession(env = process.env) {
|
package/src/consent-flow.js
CHANGED
|
@@ -15,10 +15,11 @@ const MAX_ATTEMPTS = 5;
|
|
|
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`);
|
|
@@ -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/i18n.js
CHANGED
|
@@ -450,7 +450,7 @@ const catalogs = {
|
|
|
450
450
|
// Shown only when the 4 prose gaps below were actually replaced by a validated, project-adapted response — never on fallback to the curated content.
|
|
451
451
|
roadmapPersonalizedNotice: 'Contenido adaptado a tu proyecto.',
|
|
452
452
|
// Los dos avisos que faltaban, hermanos del `agentsEvalMissing` que añadió la 106.
|
|
453
|
-
roadmapNotPersonalizedNotice: 'Este roadmap es el genérico de tu tier: esta vez no se ha podido adaptar a tu proyecto. Vuelve a ejecutar `usage` para intentarlo otra vez.',
|
|
453
|
+
roadmapNotPersonalizedNotice: 'Este roadmap es el genérico de tu tier: esta vez no se ha podido adaptar a tu proyecto. Vuelve a ejecutar `usage --roadmap` para intentarlo otra vez.',
|
|
454
454
|
// El antes y el después del roadmap.
|
|
455
455
|
roadmapNowLabel: 'De dónde partes',
|
|
456
456
|
roadmapProjectionLabel: 'Dónde te deja esta ruta',
|
|
@@ -558,6 +558,7 @@ const catalogs = {
|
|
|
558
558
|
// Roadmap personalization: reuses the
|
|
559
559
|
// same spinner mechanism as synthesizingLabel above.
|
|
560
560
|
personalizingRoadmapLabel: 'Personalizando roadmap…',
|
|
561
|
+
roadmapNotForAccounts: 'El roadmap no está disponible para cuentas de Shakers: tu nivel sale de certificarte.',
|
|
561
562
|
// Agent definition-quality evaluation (ephemeral LLM call).
|
|
562
563
|
evaluatingAgentsLabel: 'Evaluando la calidad de tus agentes…',
|
|
563
564
|
// "Construir el siguiente nivel ahora": now a SECONDARY, opt-in alternative — the copyable implementation prompt (below) is the PRIMARY "how do I implement this" path.
|
|
@@ -575,7 +576,7 @@ const catalogs = {
|
|
|
575
576
|
+ ' --repo Evalúa solo el repo actual\n'
|
|
576
577
|
+ ' --scope machine|repo Igual que --machine / --repo (salta el selector)\n'
|
|
577
578
|
+ ' --all-repos Alias de --machine\n'
|
|
578
|
-
+ ' --roadmap Muestra los pasos para subir tu nivel de uso de IA (
|
|
579
|
+
+ ' --roadmap Muestra los pasos para subir tu nivel de uso de IA (solo sin cuenta de Shakers)\n'
|
|
579
580
|
+ ' --build-next-level Genera el starter del siguiente tier (alternativa secundaria)\n'
|
|
580
581
|
+ ' --force Junto a --build-next-level, sobrescribe un fichero existente\n'
|
|
581
582
|
+ ' --lang es|en Fuerza el idioma (informe + prompt) en vez de detectarlo del sistema\n'
|
|
@@ -647,7 +648,7 @@ const catalogs = {
|
|
|
647
648
|
// Esta pregunta se pide ahora ANTES de cualquier llamada de IA/egress (agent-synthesis, agent-evaluation, personalización de roadmap), no después.
|
|
648
649
|
noReportWithoutConsent: 'Sin tu consentimiento, este informe no incluirá síntesis, evaluación ni roadmap generados por IA esta vez (el resto del informe se muestra igual). Vuelve a ejecutar `usage` y acepta para incluirlos, o usa --consent-reset si quieres que se te pregunte de nuevo.',
|
|
649
650
|
deniedSaved: 'Entendido, no se guardará nada. Puedes cambiar de opinión más adelante volviendo a ejecutar el comando.',
|
|
650
|
-
grantedSaved: (email) => `Gracias.
|
|
651
|
+
grantedSaved: (email) => `Gracias. Antes de subir nada a Shakers te enseñaré lo que se enviaría y solo lo subiré si lo apruebas (correo: ${email}, máx. 1 vez por hora).`,
|
|
651
652
|
// Un talento logueado NUNCA ve el prompt de email (su cuenta ya prueba su identidad).
|
|
652
653
|
noSessionEmail: 'No hay un correo asociado a tu sesión todavía, así que no se puede guardar el informe esta vez. Se te preguntará de nuevo la próxima ejecución.',
|
|
653
654
|
skipAlreadyDecided: (decision, path) =>
|
|
@@ -972,12 +973,13 @@ const catalogs = {
|
|
|
972
973
|
},
|
|
973
974
|
mcpInstall: {
|
|
974
975
|
title: 'Conectando el MCP de Shakers',
|
|
975
|
-
clients: { claudeDesktop: 'Claude Desktop', claudeCode: 'Claude Code', cursor: 'Cursor', geminiCli: 'Gemini CLI', codex: 'Codex
|
|
976
|
+
clients: { claudeDesktop: 'Claude Desktop', claudeCode: 'Claude Code', cursor: 'Cursor', geminiCli: 'Gemini CLI', codex: 'Codex / ChatGPT', vscode: 'VS Code', windsurf: 'Windsurf' },
|
|
976
977
|
status: {
|
|
977
978
|
configured: 'conectado',
|
|
978
979
|
unchanged: 'ya estaba conectado',
|
|
979
980
|
'not-found': 'no instalado en esta máquina',
|
|
980
|
-
'invalid-
|
|
981
|
+
'invalid-config': 'no puedo editar su configuración con seguridad; no la he tocado',
|
|
982
|
+
manual: 'ejecuta este comando en tu terminal:',
|
|
981
983
|
failed: 'no se pudo conectar',
|
|
982
984
|
},
|
|
983
985
|
removedLegacy: 'Sustituye a la entrada antigua "shakers-ai-usage".',
|
|
@@ -985,7 +987,10 @@ const catalogs = {
|
|
|
985
987
|
confirm: '¿Conecto Shakers a ellas para que puedas usarlo desde tu IA? [S/n]',
|
|
986
988
|
declined: 'No he tocado nada. Para conectarlo más tarde, vuelve a ejecutar el instalador.',
|
|
987
989
|
noTerminal: 'No hay una terminal donde preguntarte, así que no he tocado nada. Vuelve a ejecutar el instalador en tu terminal para conectarlo.',
|
|
988
|
-
restart:
|
|
990
|
+
restart: (apps) => `Reinicia ${apps} (o recarga sus servidores MCP) para que aparezcan las herramientas de Shakers.`,
|
|
991
|
+
alreadyConnected: 'Shakers ya estaba conectado. Si no ves sus herramientas, reinicia la app o recarga sus servidores MCP.',
|
|
992
|
+
yesHint: 'Para conectarlas sin preguntar (por ejemplo, desde un asistente de IA): shakers mcp install --yes',
|
|
993
|
+
help: 'mcp install — conecta Shakers a las apps de IA de esta máquina (Claude Desktop, Claude Code, Cursor, Gemini CLI, Codex / ChatGPT, VS Code, Windsurf).\n\nUso:\n shakers mcp install pregunta en la terminal y conecta las apps encontradas\n shakers mcp install --yes conecta sin preguntar (-y; para asistentes de IA y scripts)\n shakers mcp install --json imprime el resultado como un único objeto JSON\n\nSin --yes y sin terminal no toca nada. SHAKERS_MCP_INSTALL=1 equivale a --yes.\nAl terminar, reinicia las apps que indique (o recarga sus servidores MCP) para que aparezcan las herramientas de Shakers.',
|
|
989
994
|
noneFound: 'No he encontrado ninguna app de IA compatible en esta máquina.',
|
|
990
995
|
failedAll: 'No he podido conectar Shakers a tus apps de IA. Vuelve a ejecutar el instalador para reintentarlo.',
|
|
991
996
|
},
|
|
@@ -1224,7 +1229,7 @@ const catalogs = {
|
|
|
1224
1229
|
needInput: 'Se necesitan email y contraseña para iniciar sesión.',
|
|
1225
1230
|
success: 'Sesión iniciada. Tu informe ya no incluye el roadmap ni las sugerencias de mejora: tu nivel sale de certificarte.',
|
|
1226
1231
|
expiresAt: (iso) => `La sesión caduca: ${iso}. Cuando expire te lo diré y podrás volver a entrar con \`login\`.`,
|
|
1227
|
-
loggedOut: 'Sesión cerrada. Vuelves a ver
|
|
1232
|
+
loggedOut: 'Sesión cerrada. Vuelves a ver las sugerencias de mejora en tu informe.',
|
|
1228
1233
|
notLoggedIn: 'No había ninguna sesión iniciada. No hay nada que cerrar.',
|
|
1229
1234
|
errorInvalidCredentials: 'Credenciales incorrectas: revisa tu email y contraseña e inténtalo otra vez.',
|
|
1230
1235
|
errorNoEmailPassword: 'Esta cuenta existe pero no tiene contraseña de email: la registraste con Google o LinkedIn. Si fue con Google, usa `login --google`. Si fue con LinkedIn, entra desde la web para añadir una contraseña por ahora.',
|
|
@@ -2094,7 +2099,7 @@ const catalogs = {
|
|
|
2094
2099
|
roadmapPersonalizedNotice: 'Content adapted to your project.',
|
|
2095
2100
|
// See the Spanish catalog for why these say what was lost
|
|
2096
2101
|
// rather than what went wrong, and when they are allowed to show.
|
|
2097
|
-
roadmapNotPersonalizedNotice: 'This roadmap is the generic one for your tier: it could not be adapted to your project this run. Run `usage` again to retry.',
|
|
2102
|
+
roadmapNotPersonalizedNotice: 'This roadmap is the generic one for your tier: it could not be adapted to your project this run. Run `usage --roadmap` again to retry.',
|
|
2098
2103
|
// See the Spanish catalog for why the tag sits next to the
|
|
2099
2104
|
// heading instead of in a footnote.
|
|
2100
2105
|
roadmapNowLabel: 'Where you start from',
|
|
@@ -2198,6 +2203,7 @@ const catalogs = {
|
|
|
2198
2203
|
reposScopeAll: 'Evaluating all AI usage on this machine.',
|
|
2199
2204
|
synthesizingLabel: 'Synthesizing agents with AI…',
|
|
2200
2205
|
personalizingRoadmapLabel: 'Personalizing roadmap…',
|
|
2206
|
+
roadmapNotForAccounts: 'The roadmap is not available for Shakers accounts: your level comes from certifying.',
|
|
2201
2207
|
// Agent definition-quality evaluation (ephemeral LLM call).
|
|
2202
2208
|
evaluatingAgentsLabel: 'Evaluating your agents’ quality…',
|
|
2203
2209
|
buildNextLevelHint: 'Alternatively, run `usage --build-next-level` to generate the starter file directly in your project.',
|
|
@@ -2214,7 +2220,7 @@ const catalogs = {
|
|
|
2214
2220
|
+ ' --repo Evaluate the current repo only\n'
|
|
2215
2221
|
+ ' --scope machine|repo Same as --machine / --repo (skips the picker)\n'
|
|
2216
2222
|
+ ' --all-repos Alias of --machine\n'
|
|
2217
|
-
+ ' --roadmap Show the steps to raise your AI-usage level (
|
|
2223
|
+
+ ' --roadmap Show the steps to raise your AI-usage level (only without a Shakers account)\n'
|
|
2218
2224
|
+ ' --build-next-level Generate the next tier starter (secondary alternative)\n'
|
|
2219
2225
|
+ ' --force With --build-next-level, overwrite an existing file\n'
|
|
2220
2226
|
+ ' --lang es|en Force the language (report + prompt) instead of OS detection\n'
|
|
@@ -2287,7 +2293,7 @@ const catalogs = {
|
|
|
2287
2293
|
// This question is now asked BEFORE any AI/egress call (agent-synthesis, agent-evaluation, roadmap personalization), not after.
|
|
2288
2294
|
noReportWithoutConsent: 'Without your consent, this report will not include AI-generated synthesis, evaluation, or roadmap this time (the rest of the report is shown as usual). Run `usage` again and accept to include them, or use --consent-reset if you want to be asked again.',
|
|
2289
2295
|
deniedSaved: 'Understood, nothing will be saved. You can change your mind later by running the command again.',
|
|
2290
|
-
grantedSaved: (email) => `Thanks.
|
|
2296
|
+
grantedSaved: (email) => `Thanks. Before anything is uploaded to Shakers I will show you what would be sent, and I will upload it only if you approve it (email: ${email}, at most once an hour).`,
|
|
2291
2297
|
// A logged-in Talent is NEVER asked for an email (their account already proves identity).
|
|
2292
2298
|
noSessionEmail: "There's no email on file for your session yet, so this report can't be saved this time. You'll be asked again next run.",
|
|
2293
2299
|
skipAlreadyDecided: (decision, path) =>
|
|
@@ -2609,12 +2615,13 @@ const catalogs = {
|
|
|
2609
2615
|
},
|
|
2610
2616
|
mcpInstall: {
|
|
2611
2617
|
title: 'Connecting the Shakers MCP',
|
|
2612
|
-
clients: { claudeDesktop: 'Claude Desktop', claudeCode: 'Claude Code', cursor: 'Cursor', geminiCli: 'Gemini CLI', codex: 'Codex
|
|
2618
|
+
clients: { claudeDesktop: 'Claude Desktop', claudeCode: 'Claude Code', cursor: 'Cursor', geminiCli: 'Gemini CLI', codex: 'Codex / ChatGPT', vscode: 'VS Code', windsurf: 'Windsurf' },
|
|
2613
2619
|
status: {
|
|
2614
2620
|
configured: 'connected',
|
|
2615
2621
|
unchanged: 'already connected',
|
|
2616
2622
|
'not-found': 'not installed on this machine',
|
|
2617
|
-
'invalid-
|
|
2623
|
+
'invalid-config': 'its config cannot be edited safely; left untouched',
|
|
2624
|
+
manual: 'run this command in your terminal:',
|
|
2618
2625
|
failed: 'could not connect',
|
|
2619
2626
|
},
|
|
2620
2627
|
removedLegacy: 'Replaces the old "shakers-ai-usage" entry.',
|
|
@@ -2622,7 +2629,10 @@ const catalogs = {
|
|
|
2622
2629
|
confirm: 'Connect Shakers to them so you can use it from your AI? [Y/n]',
|
|
2623
2630
|
declined: 'Nothing touched. To connect it later, run the installer again.',
|
|
2624
2631
|
noTerminal: 'There is no terminal to ask you on, so nothing was touched. Run the installer again in your terminal to connect it.',
|
|
2625
|
-
restart:
|
|
2632
|
+
restart: (apps) => `Restart ${apps} (or reload their MCP servers) so the Shakers tools appear.`,
|
|
2633
|
+
alreadyConnected: 'Shakers was already connected. If its tools do not appear, restart the app or reload its MCP servers.',
|
|
2634
|
+
yesHint: 'To connect them without being asked (for example from an AI assistant): shakers mcp install --yes',
|
|
2635
|
+
help: 'mcp install — connect Shakers to the AI apps on this machine (Claude Desktop, Claude Code, Cursor, Gemini CLI, Codex / ChatGPT, VS Code, Windsurf).\n\nUsage:\n shakers mcp install ask on the terminal, then connect every app found\n shakers mcp install --yes connect without asking (-y; for AI assistants and scripts)\n shakers mcp install --json print the result as one JSON object\n\nWithout --yes and without a terminal, nothing is touched. SHAKERS_MCP_INSTALL=1 works like --yes.\nWhen it finishes, restart the apps it names (or reload their MCP servers) so the Shakers tools appear.',
|
|
2626
2636
|
noneFound: 'Found no supported AI app on this machine.',
|
|
2627
2637
|
failedAll: 'Could not connect Shakers to your AI apps. Re-run this installer to retry.',
|
|
2628
2638
|
},
|
|
@@ -2857,7 +2867,7 @@ const catalogs = {
|
|
|
2857
2867
|
needInput: 'Email and password are required to log in.',
|
|
2858
2868
|
success: 'Logged in. Your report no longer includes the roadmap or the improvement suggestions: your level comes from certifying.',
|
|
2859
2869
|
expiresAt: (iso) => `The session expires: ${iso}. When it does I will tell you, and you can log in again with \`login\`.`,
|
|
2860
|
-
loggedOut: 'Logged out.
|
|
2870
|
+
loggedOut: 'Logged out. Your report shows the improvement suggestions again.',
|
|
2861
2871
|
notLoggedIn: 'No session was active. Nothing to log out of.',
|
|
2862
2872
|
errorInvalidCredentials: 'Wrong credentials: check your email and password and try again.',
|
|
2863
2873
|
errorNoEmailPassword: 'This account exists but has no email password: you registered with Google or LinkedIn. If it was Google, use `login --google`. If it was LinkedIn, sign in on the web to add a password for now.',
|
|
@@ -52,6 +52,8 @@ async function pollClassifiedInventory({
|
|
|
52
52
|
}
|
|
53
53
|
}
|
|
54
54
|
|
|
55
|
+
const RESULT_MESSAGE = 'Print relayVerbatim word for word: the result in "My work with AI", what it means and how to ask for a human review.';
|
|
56
|
+
const PROFILE_PENDING_MESSAGE = 'The profile result is not ready yet: call ai_usage_result again in a few minutes.';
|
|
55
57
|
const READY_MESSAGE = 'Shakers finished classifying the agents.';
|
|
56
58
|
const PENDING_MESSAGE = 'Shakers is still classifying the agents — this is normal, call ai_usage_result again shortly to keep waiting.';
|
|
57
59
|
|
|
@@ -65,6 +67,12 @@ function makeAiUsageResultTool(deps = {}) {
|
|
|
65
67
|
now = () => Date.now(),
|
|
66
68
|
intervalMs = POLL_INTERVAL_MS,
|
|
67
69
|
capMs = POLL_CAP_MS,
|
|
70
|
+
resolveAiProfilePreview = require('./ai-profile-preview').resolveAiProfilePreview,
|
|
71
|
+
getAiProfileEndpoint = require('./config').getAiProfileEndpoint,
|
|
72
|
+
ensureFreshSession = require('./session-refresh').ensureFreshSession,
|
|
73
|
+
aiProfileWaitMs = 30000,
|
|
74
|
+
resultText = require('./ai-usage-review').resultText,
|
|
75
|
+
lang = () => require('./signup-language').signupLanguage(require('./i18n').detectFlowLang()),
|
|
68
76
|
} = deps;
|
|
69
77
|
|
|
70
78
|
async function handler() {
|
|
@@ -77,27 +85,41 @@ function makeAiUsageResultTool(deps = {}) {
|
|
|
77
85
|
throw new Error('discovered-inventory endpoint is not configured (set SHAKERS_CLI_INGEST_ENDPOINT).');
|
|
78
86
|
}
|
|
79
87
|
|
|
80
|
-
const result = await
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
88
|
+
const [result, aiProfile] = await Promise.all([
|
|
89
|
+
pollClassifiedInventory({
|
|
90
|
+
accessToken: session.accessToken,
|
|
91
|
+
endpoint,
|
|
92
|
+
requestDiscoveredInventory,
|
|
93
|
+
sleep,
|
|
94
|
+
now,
|
|
95
|
+
intervalMs,
|
|
96
|
+
capMs,
|
|
97
|
+
}),
|
|
98
|
+
resolveAiProfilePreview(
|
|
99
|
+
{ session, endpoint: getAiProfileEndpoint(), timeoutMs: aiProfileWaitMs },
|
|
100
|
+
{
|
|
101
|
+
refreshToken: async () => {
|
|
102
|
+
const fresh = await ensureFreshSession(process.env);
|
|
103
|
+
return fresh ? { accessToken: fresh.accessToken, hubAccessToken: fresh.hubAccessToken } : null;
|
|
104
|
+
},
|
|
105
|
+
},
|
|
106
|
+
).catch(() => ({ status: 'unavailable', preview: null })),
|
|
107
|
+
]);
|
|
108
|
+
const profileReady = aiProfile.status === 'ready';
|
|
89
109
|
return {
|
|
90
110
|
...result,
|
|
91
|
-
|
|
111
|
+
aiProfile,
|
|
112
|
+
relayVerbatim: resultText(lang(), profileReady ? aiProfile.preview : null),
|
|
113
|
+
message: `${RESULT_MESSAGE} ${result.status === 'ready' ? READY_MESSAGE : PENDING_MESSAGE}${profileReady ? '' : ` ${PROFILE_PENDING_MESSAGE}`}`,
|
|
92
114
|
};
|
|
93
115
|
}
|
|
94
116
|
|
|
95
117
|
return {
|
|
96
118
|
name: 'ai_usage_result',
|
|
97
119
|
title: 'Results of your AI analysis',
|
|
98
|
-
description: 'Fetches from Shakers the
|
|
120
|
+
description: 'Fetches from Shakers the result of the AI analysis you approved, what it means, and the description of each AI agent found. It reads nothing on this computer.',
|
|
99
121
|
guidance:
|
|
100
|
-
"Retrieve the enriched Shakers classification for the talent's agents
|
|
122
|
+
"Retrieve the result after an approved upload: relayVerbatim is the 'My work with AI' result with its levels and explanation in plain words, and how to ask for a human review; print it word for word. It also returns the enriched Shakers classification for the talent's agents. Shakers classifies agents and writes their descriptions server-side (no client LLM), so this reads the result: per agent { name, tools, model, category, role, level, code, whatItDoes }. Session-keyed, no arguments. Server-side classification can take a few minutes, so this does a bounded internal poll (~90s max, never a long-held call): returns { status: 'ready', agents } once every agent has been classified, or { status: 'pending', agents } while Shakers is still processing (call it again to keep waiting — 'pending' is normal progress, not an error). Present agents to the talent by NAME and category/role only; the `code` field is an internal handle for add_agent, never show it. Requires an active session.",
|
|
101
123
|
inputSchema: RESULT_INPUT_SCHEMA,
|
|
102
124
|
handler,
|
|
103
125
|
};
|