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
|
@@ -3,7 +3,8 @@
|
|
|
3
3
|
const FLOOR_METHOD = 'floor';
|
|
4
4
|
const FLOOR_CODE = 'other-1';
|
|
5
5
|
const POLL_INTERVAL_MS = 5000;
|
|
6
|
-
|
|
6
|
+
// With the last inventory request (its own timeout) this answers within ~45 s, under Claude Desktop's ~60 s cut.
|
|
7
|
+
const POLL_CAP_MS = 30000;
|
|
7
8
|
const INVENTORY_TIMEOUT_MS = 15000;
|
|
8
9
|
|
|
9
10
|
const RESULT_INPUT_SCHEMA = { type: 'object', properties: {} };
|
|
@@ -75,7 +76,8 @@ function makeAiUsageResultTool(deps = {}) {
|
|
|
75
76
|
lang = () => require('./signup-language').signupLanguage(require('./i18n').detectFlowLang()),
|
|
76
77
|
} = deps;
|
|
77
78
|
|
|
78
|
-
|
|
79
|
+
// `explained` comes only from the sign-up, whose review already explained the axes and how to disagree.
|
|
80
|
+
async function handler(args = {}) {
|
|
79
81
|
const session = loadAuthSession();
|
|
80
82
|
if (sessionStatus(session) !== 'active') {
|
|
81
83
|
throw new Error('no active Shakers session — sign in with social_signin_start (or run `shakers login`) first.');
|
|
@@ -109,7 +111,7 @@ function makeAiUsageResultTool(deps = {}) {
|
|
|
109
111
|
return {
|
|
110
112
|
...result,
|
|
111
113
|
aiProfile,
|
|
112
|
-
relayVerbatim: resultText(lang(), profileReady ? aiProfile.preview : null),
|
|
114
|
+
relayVerbatim: resultText(lang(), profileReady ? aiProfile.preview : null, { explained: args.explained === true }),
|
|
113
115
|
message: `${RESULT_MESSAGE} ${result.status === 'ready' ? READY_MESSAGE : PENDING_MESSAGE}${profileReady ? '' : ` ${PROFILE_PENDING_MESSAGE}`}`,
|
|
114
116
|
};
|
|
115
117
|
}
|
|
@@ -119,7 +121,7 @@ function makeAiUsageResultTool(deps = {}) {
|
|
|
119
121
|
title: 'Results of your AI analysis',
|
|
120
122
|
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.',
|
|
121
123
|
guidance:
|
|
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 (~
|
|
124
|
+
"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 (~30 s, 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.",
|
|
123
125
|
inputSchema: RESULT_INPUT_SCHEMA,
|
|
124
126
|
handler,
|
|
125
127
|
};
|
package/src/mcp-ai-usage-tool.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
const fs = require('fs');
|
|
4
4
|
const { execFileSync } = require('child_process');
|
|
5
5
|
const { isRefusedWalkRoot } = require('./scan-exclusions');
|
|
6
|
-
const { askChoice, chatChoice, queueNotice } = require('./mcp-choice');
|
|
6
|
+
const { askChoice, chatChoice, forChat, handOver, narrationOf, queueNotice, settle, withNextSay } = require('./mcp-choice');
|
|
7
7
|
|
|
8
8
|
function isExistingDir(p) {
|
|
9
9
|
try {
|
|
@@ -76,6 +76,7 @@ function buildArgv({ root, repoScope, lang }) {
|
|
|
76
76
|
}
|
|
77
77
|
|
|
78
78
|
const VERBATIM = 'Print relayVerbatim word for word now, as its own block, with nothing of your own added to it.';
|
|
79
|
+
const CONSENT_QUESTION = 'ai_usage:consent';
|
|
79
80
|
|
|
80
81
|
const UPLOAD_INPUT_SCHEMA = {
|
|
81
82
|
type: 'object',
|
|
@@ -103,6 +104,7 @@ function makeAiUsageTools(deps = {}) {
|
|
|
103
104
|
const {
|
|
104
105
|
scanUsage = (argv) => require('./usage-scan').scanUsage(argv),
|
|
105
106
|
autoShare = require('./share').autoShare,
|
|
107
|
+
logCommandRun = require('./command-audit').logCommandRun,
|
|
106
108
|
recordConsent = require('./share').recordConsent,
|
|
107
109
|
sendThrottled = () => require('./share').isThrottled(require('./share').loadConsentState()),
|
|
108
110
|
isValidEmail = require('./share').isValidEmail,
|
|
@@ -121,6 +123,8 @@ function makeAiUsageTools(deps = {}) {
|
|
|
121
123
|
renderFullReportText = require('./render-report-text').renderFullReportText,
|
|
122
124
|
detectFlowLang = require('./i18n').detectFlowLang,
|
|
123
125
|
aiFluencyWaitMs = 30000,
|
|
126
|
+
signupInventoryWaitMs = 20000,
|
|
127
|
+
signupProfileWaitMs = 25000,
|
|
124
128
|
signupPhase = () => require('./mcp-signup-tools').signupPhase(),
|
|
125
129
|
usageDisclaimers = (lang) => require('./onboarding-flow').usageDisclaimers(lang),
|
|
126
130
|
signupCopy = require('./signup-copy').signupCopy,
|
|
@@ -132,6 +136,8 @@ function makeAiUsageTools(deps = {}) {
|
|
|
132
136
|
let job = null;
|
|
133
137
|
// The talent's answer in the consent dialog, so a later call cannot contradict it.
|
|
134
138
|
let dialogConsent = null;
|
|
139
|
+
// In the sign-up a consent counts only after its question and the two disclaimers went to the chat.
|
|
140
|
+
let consentAsked = false;
|
|
135
141
|
let pending = null;
|
|
136
142
|
|
|
137
143
|
function startSignupScan(argv, root, lang) {
|
|
@@ -206,20 +212,19 @@ function makeAiUsageTools(deps = {}) {
|
|
|
206
212
|
const facts = reviewFacts(review);
|
|
207
213
|
const texts = [reviewTexts.summaryText(review.lang, facts), reviewTexts.explanationText(review.lang)];
|
|
208
214
|
if (ctx && ctx.elicitation && typeof ctx.elicit === 'function') {
|
|
209
|
-
const reply = await ctx.elicit({ message: [...texts, c.question].join('\n\n'), requestedSchema: reviewSchema(c, facts) });
|
|
215
|
+
const reply = await ctx.elicit({ message: [...narrationOf(ctx), ...texts, c.question].join('\n\n'), requestedSchema: reviewSchema(c, facts) });
|
|
210
216
|
const content = reply && reply.action === 'accept' && reply.content ? reply.content : null;
|
|
211
217
|
if (content && content.decision === c.yes) return upload(review, exclusionsFromForm(review, facts, content));
|
|
212
218
|
if ((content && content.decision === c.no) || (reply && reply.action === 'decline')) return declineUpload(review);
|
|
213
219
|
}
|
|
214
220
|
review.askedInChat = true;
|
|
215
|
-
const
|
|
221
|
+
const { choiceMessage, ...shown } = chatChoice(c.question, [c.yes, c.no], [...texts, c.excludeHint]);
|
|
216
222
|
return {
|
|
217
223
|
ok: false,
|
|
218
224
|
reason: 'upload-approval-required',
|
|
219
|
-
|
|
220
|
-
options: chat.options,
|
|
225
|
+
...shown,
|
|
221
226
|
excludable: { repos: facts.repos, agents: facts.agents, excerpts: facts.conversationExcerpts + facts.commandExcerpts > 0 },
|
|
222
|
-
message: `Nothing has been sent. ${
|
|
227
|
+
message: `Nothing has been sent. ${choiceMessage} Then call ai_usage_upload with confirm:true only if they pick "${c.yes}" (adding exclude with the repos and agents they want left out, exactly as listed in excludable, and excerpts:true if they want no text excerpts), or confirm:false if they pick "${c.no}". Only the talent decides: never answer for them.`,
|
|
223
228
|
};
|
|
224
229
|
}
|
|
225
230
|
|
|
@@ -246,7 +251,8 @@ function makeAiUsageTools(deps = {}) {
|
|
|
246
251
|
let send;
|
|
247
252
|
try {
|
|
248
253
|
recordConsent('granted', session.email, { verified: true });
|
|
249
|
-
|
|
254
|
+
const cliCommandId = await logCommandRun({ command: 'mcp:ai_usage_upload:send' });
|
|
255
|
+
send = await autoShare(trimmed.report, trimmed.maturity, { root: review.root, bypassThrottle: review.registration, cliCommandId });
|
|
250
256
|
} catch (error) {
|
|
251
257
|
send = { ok: false, reason: 'upload-failed', message: error && error.message ? error.message : String(error) };
|
|
252
258
|
}
|
|
@@ -257,36 +263,32 @@ function makeAiUsageTools(deps = {}) {
|
|
|
257
263
|
const excluded = exclude.repos.length > 0 || exclude.agents.length > 0 || !exclude.excerpts;
|
|
258
264
|
const confirmation = excluded ? [c.uploaded, sent] : [c.uploaded];
|
|
259
265
|
review.outcome = { ok: true, uploaded: true, send, sentReport: trimmed, sent };
|
|
260
|
-
if (review.registration)
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
}
|
|
266
|
-
return Object.assign(review.outcome,
|
|
266
|
+
if (!review.registration) return Object.assign(review.outcome, await afterUpload(review, trimmed, confirmation));
|
|
267
|
+
// The sign-up shows the same result as outside it, at the same moment, on the next question: the review already
|
|
268
|
+
// explained the two axes and how to disagree, so the result does not repeat them.
|
|
269
|
+
const result = await afterUpload(review, trimmed, confirmation, { inventoryMs: signupInventoryWaitMs, profileMs: signupProfileWaitMs, explained: true });
|
|
270
|
+
queueNotice([result.relayVerbatim, result.text].filter(Boolean).join('\n\n'));
|
|
271
|
+
const { relayVerbatim: _shown, message: _rule, ...rest } = result;
|
|
272
|
+
return Object.assign(review.outcome, rest, {
|
|
273
|
+
message: `Uploaded. Say nothing about it: the result of the analysis, which the talent reads word for word, comes with the next question.${result.next ? ' Shakers is still evaluating part of it (normal, not an error): do not wait for it.' : ''}`,
|
|
274
|
+
});
|
|
267
275
|
}
|
|
268
276
|
|
|
269
|
-
|
|
270
|
-
|
|
277
|
+
// The classified agents and the profile result are awaited together, so the answer stays under the client's cut.
|
|
278
|
+
async function afterUpload(review, { report, maturity }, confirmation, { inventoryMs = inlineWaitMs, profileMs = aiFluencyWaitMs, explained = false } = {}) {
|
|
271
279
|
const hasAgents = Array.isArray(report.agents) && report.agents.length > 0;
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
if (hasAgents) {
|
|
280
|
+
const classify = async () => {
|
|
281
|
+
if (!hasAgents) return null;
|
|
275
282
|
const session = loadAuthSession();
|
|
276
283
|
const endpoint = getUsageDiscoveredInventoryEndpoint();
|
|
277
|
-
if (sessionStatus(session)
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
let aiProfile = { status: 'unavailable', preview: null };
|
|
286
|
-
const usageSession = loadAuthSession();
|
|
287
|
-
if (sessionStatus(usageSession) === 'active') {
|
|
288
|
-
aiProfile = await resolveAiProfilePreview(
|
|
289
|
-
{ session: usageSession, endpoint: getAiProfileEndpoint(), timeoutMs: aiFluencyWaitMs },
|
|
284
|
+
if (sessionStatus(session) !== 'active' || !endpoint) return null;
|
|
285
|
+
return pollClassifiedInventory({ accessToken: session.accessToken, endpoint, requestDiscoveredInventory, capMs: inventoryMs });
|
|
286
|
+
};
|
|
287
|
+
const profile = async () => {
|
|
288
|
+
const usageSession = loadAuthSession();
|
|
289
|
+
if (sessionStatus(usageSession) !== 'active') return { status: 'unavailable', preview: null };
|
|
290
|
+
return resolveAiProfilePreview(
|
|
291
|
+
{ session: usageSession, endpoint: getAiProfileEndpoint(), timeoutMs: profileMs },
|
|
290
292
|
{
|
|
291
293
|
refreshToken: async () => {
|
|
292
294
|
const fresh = await ensureFreshSession(process.env);
|
|
@@ -294,7 +296,10 @@ function makeAiUsageTools(deps = {}) {
|
|
|
294
296
|
},
|
|
295
297
|
},
|
|
296
298
|
);
|
|
297
|
-
}
|
|
299
|
+
};
|
|
300
|
+
const [polled, aiProfile] = await Promise.all([classify(), profile()]);
|
|
301
|
+
const enrichedAgents = polled && polled.status === 'ready' ? polled.agents : null;
|
|
302
|
+
const enrichment = !hasAgents ? 'none' : enrichedAgents ? 'ready' : 'pending';
|
|
298
303
|
const displayAgents = enrichedAgents
|
|
299
304
|
? enrichedAgents.map((a) => ({ name: a.name, category: a.category, role: a.role }))
|
|
300
305
|
: (report.agents || []).map((a) => ({ name: a.name }));
|
|
@@ -306,9 +311,9 @@ function makeAiUsageTools(deps = {}) {
|
|
|
306
311
|
agents: enrichedAgents,
|
|
307
312
|
text,
|
|
308
313
|
aiProfile,
|
|
309
|
-
relayVerbatim: [...confirmation, reviewTexts.resultText(review.lang, aiProfile.status === 'ready' ? aiProfile.preview : null)].join('\n\n'),
|
|
314
|
+
relayVerbatim: [...confirmation, reviewTexts.resultText(review.lang, aiProfile.status === 'ready' ? aiProfile.preview : null, { explained })].join('\n\n'),
|
|
310
315
|
next: enrichment === 'pending' || aiProfile.status !== 'ready' ? 'ai_usage_result' : null,
|
|
311
|
-
message: `Uploaded. ${VERBATIM} Then show \`text\`, the full report, verbatim.${enrichment === 'pending' || aiProfile.status !== 'ready' ? ' Shakers is still evaluating (normal, not an error): call ai_usage_result in a few minutes for the result and its explanation.' : ''}`,
|
|
316
|
+
message: `Uploaded. ${VERBATIM} Then show \`text\`, the full report, verbatim.${enrichment === 'pending' || aiProfile.status !== 'ready' ? ' Shakers is still evaluating (normal, not an error): call ai_usage_result in a few minutes for the result and its explanation.' : ''}${hasAgents && !explained ? ' Then call add_detected_agents to offer adding the detected agents to the profile.' : ''}`,
|
|
312
317
|
display: {
|
|
313
318
|
tier: { key: maturity.tierKey, name: maturity.tierName, setupLevel: maturity.name, score: maturity.score },
|
|
314
319
|
aiProfile: {
|
|
@@ -336,27 +341,47 @@ function makeAiUsageTools(deps = {}) {
|
|
|
336
341
|
return { ok: false, reason: 'consent-declined', relayVerbatim: signupCopy(lang).aiUsageSkipped, message: 'Nothing was scanned. Show relayVerbatim once, word for word.' };
|
|
337
342
|
}
|
|
338
343
|
|
|
344
|
+
// The yes of a dialog in the sign-up: the disclaimers come in a second dialog, so they are read before anything is scanned.
|
|
345
|
+
async function confirmInDialog(ctx, c, texts) {
|
|
346
|
+
const again = await askChoice(ctx, { texts, question: c.aiUsageStart, options: [c.aiUsageYes, c.aiUsageSkip] });
|
|
347
|
+
return again.answer || null;
|
|
348
|
+
}
|
|
349
|
+
|
|
339
350
|
async function askConsent(lang, ctx) {
|
|
340
351
|
const c = signupCopy(lang);
|
|
341
|
-
const
|
|
352
|
+
const disclaimers = Object.values(usageDisclaimers(lang));
|
|
353
|
+
// In the sign-up the question goes first and short; its disclaimers are read only after a yes, before any scan.
|
|
354
|
+
const signup = signupPhase() === 'account';
|
|
355
|
+
const question = signup ? c.aiUsageAsk : c.aiUsageQuestion;
|
|
356
|
+
const texts = signup ? [] : disclaimers;
|
|
357
|
+
const options = [c.aiUsageYes, c.aiUsageSkip];
|
|
342
358
|
const asked = ctx && ctx.elicitation
|
|
343
|
-
? await askChoice(ctx, { texts, question
|
|
344
|
-
: { chat: chatChoice(
|
|
345
|
-
|
|
359
|
+
? await askChoice(ctx, { texts, question, options })
|
|
360
|
+
: { chat: chatChoice(question, options, texts) };
|
|
361
|
+
const answer = signup && asked.answer === c.aiUsageYes ? await confirmInDialog(ctx, c, disclaimers) : asked.answer;
|
|
362
|
+
if (answer === c.aiUsageSkip) {
|
|
346
363
|
dialogConsent = 'denied';
|
|
347
364
|
return declined(lang);
|
|
348
365
|
}
|
|
349
|
-
if (
|
|
366
|
+
if (answer === c.aiUsageYes) {
|
|
350
367
|
dialogConsent = 'granted';
|
|
351
368
|
return { ok: true, reason: 'consent-granted', consent: { granted: true }, message: 'The talent said yes in the dialog: do not ask again. Call ai_usage with consent.granted:true (and their email) to run it.' };
|
|
352
369
|
}
|
|
353
|
-
|
|
370
|
+
consentAsked = true;
|
|
371
|
+
const chat = asked.chat || chatChoice(question, options, texts);
|
|
372
|
+
const reply = {
|
|
354
373
|
ok: false,
|
|
355
374
|
reason: 'consent-required',
|
|
356
375
|
...(asked.dismissed ? { dismissed: asked.dismissed } : {}),
|
|
357
|
-
...
|
|
358
|
-
message: `Nothing was scanned. ${
|
|
376
|
+
...forChat(chat),
|
|
377
|
+
message: `Nothing was scanned. ${chat.choiceMessage} Then call ai_usage again with consent.granted:true only after they pick the first option, or consent.granted:false if they skip it.`,
|
|
359
378
|
};
|
|
379
|
+
if (!signup) return reply;
|
|
380
|
+
// A yes hands over the disclaimers, written before the scan starts; a skip hands over its fixed line, which the next question would carry.
|
|
381
|
+
return withNextSay(reply, [
|
|
382
|
+
handOver(CONSENT_QUESTION, { step: 'ai_usage', ifAnswer: [c.aiUsageYes], texts: disclaimers }),
|
|
383
|
+
handOver(CONSENT_QUESTION, { step: 'interview', ifAnswer: [c.aiUsageSkip], texts: [c.aiUsageSkipped] }),
|
|
384
|
+
]);
|
|
360
385
|
}
|
|
361
386
|
|
|
362
387
|
async function handler(args = {}, ctx = {}) {
|
|
@@ -369,7 +394,8 @@ function makeAiUsageTools(deps = {}) {
|
|
|
369
394
|
if (phase === 'waiting') {
|
|
370
395
|
return { ok: false, reason: 'account-pending', message: 'Nothing was scanned. Ask about AI usage only once signup_status says the account exists.' };
|
|
371
396
|
}
|
|
372
|
-
const consent = args.consent || {};
|
|
397
|
+
const consent = phase === 'account' && !consentAsked && dialogConsent === null ? {} : args.consent || {};
|
|
398
|
+
if (typeof consent.granted === 'boolean') settle(CONSENT_QUESTION, consent.granted ? signupCopy(textLang).aiUsageYes : signupCopy(textLang).aiUsageSkip);
|
|
373
399
|
if (consent.granted === false) return declined(textLang);
|
|
374
400
|
if (consent.granted === true && dialogConsent === 'denied') {
|
|
375
401
|
return { ok: false, reason: 'consent-declined', message: 'The talent skipped the AI-usage step in the dialog: nothing is scanned. Carry on without it. If they later ask for the scan themselves, call ai_usage without consent to ask them again.' };
|
|
@@ -488,9 +514,9 @@ function makeAiUsageTools(deps = {}) {
|
|
|
488
514
|
const usage = {
|
|
489
515
|
name: 'ai_usage',
|
|
490
516
|
title: 'Analyse how you work with AI',
|
|
491
|
-
description:
|
|
517
|
+
description: "Asks you first and, only if you say yes, looks at which AI tools you use on this computer, your conversations with AI assistants and the history of your code projects. It shows you exactly what would go to your Shakers profile (fragments of those conversations, sample commands and your agents' definitions, with anything that looks like a secret removed) and uploads it only if you approve it; an AI model from an external provider (OpenAI, Google or Anthropic) analyses that text for Shakers to compute your evaluation and then discards it, keeping only the result (levels, scores, counts and the names of your tools and agents).",
|
|
492
518
|
guidance:
|
|
493
|
-
"Measure how a Shakers talent uses AI in their work: scans this machine and repository for AI-tool usage (subagents, MCP servers, sessions, git activity) and scores their AI-maturity tier (T0-T7).
|
|
519
|
+
"Measure how a Shakers talent uses AI in their work: scans this machine and repository for AI-tool usage (subagents, MCP servers, sessions, git activity) and scores their AI-maturity tier (T0-T7). The scan runs on the talent's own machine; an approved upload sends derived signals plus secret-scrubbed text (conversation fragments, sample commands, agent definitions), which an AI model from an external provider (OpenAI, Google or Anthropic) analyses for Shakers and discards, keeping only the result. Before any scan, call it without consent: it asks the AI-usage question with both disclaimers in the same block (a dialog, or `say` to print word for word); only an explicit yes allows consent.granted:true, and skipping is consent.granted:false. The yes only scans: nothing is uploaded until the talent approves the review. Outside the sign-up, with a signed-in, email-verified session, the scan is followed by that review in the same call: a form when the client has one, otherwise `say` (what would be sent, what Shakers does with it, and the question) to print word for word, and then ai_usage_upload with the talent's answer. Without a verified session nothing can be sent and `text` is the report only. In the sign-up it is the step right after the account exists: on yes the scan runs in the background, then ai_usage_upload asks. After an upload, show relayVerbatim and `text` (the 'My work with AI' report, Markdown, no ANSI; keep the matrix grid fenced) verbatim; if next is ai_usage_result, call it in a few minutes. It runs no LLM on the machine. IDs and codes (agent `code`, skillId, catalogId) are internal handles for the add_* tools only — never show them.",
|
|
494
520
|
inputSchema: AI_USAGE_INPUT_SCHEMA,
|
|
495
521
|
handler,
|
|
496
522
|
};
|
|
@@ -500,7 +526,7 @@ function makeAiUsageTools(deps = {}) {
|
|
|
500
526
|
title: 'Review and upload your AI analysis',
|
|
501
527
|
description: 'Shows you exactly what your AI analysis would send to your Shakers profile and what Shakers does with it, lets you leave out repositories, agents or text excerpts, and uploads it only if you approve.',
|
|
502
528
|
guidance:
|
|
503
|
-
"The only way an AI-usage analysis reaches Shakers. Call it without confirm: it shows the review (a form the talent fills in when the client has one; otherwise
|
|
529
|
+
"The only way an AI-usage analysis reaches Shakers. Call it without confirm: it shows the review (a form the talent fills in when the client has one; otherwise a question to ask as its message says, plus `excludable`). In the chat, call it again with confirm:true only when the talent picked the upload option, with exclude for what they asked to leave out, or confirm:false when they picked not to; never decide for them, and confirm counts only after the review was shown in the chat. In the sign-up pass waitSeconds 30: reason scan-running means carry on and call it again after the interview step. On an upload print relayVerbatim word for word. Nothing is sent twice.",
|
|
504
530
|
inputSchema: UPLOAD_INPUT_SCHEMA,
|
|
505
531
|
handler: uploadHandler,
|
|
506
532
|
};
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const ID_PROPERTY = { type: 'string', description: 'Position id (UUID or legacy Mongo id) from find_projects or show_project. An internal handle: never show it to the talent.' };
|
|
4
|
+
|
|
5
|
+
const PREPARE_APPLICATION_SCHEMA = {
|
|
6
|
+
type: 'object',
|
|
7
|
+
properties: { id: ID_PROPERTY },
|
|
8
|
+
required: ['id'],
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
const APPLY_TO_PROJECT_SCHEMA = {
|
|
12
|
+
type: 'object',
|
|
13
|
+
properties: {
|
|
14
|
+
id: ID_PROPERTY,
|
|
15
|
+
message: { type: 'string', description: "The talent's own message for the team, in their words, answering mainMotivationQuestion. Never write it for them." },
|
|
16
|
+
answers: {
|
|
17
|
+
type: 'array',
|
|
18
|
+
description: 'The answers the talent gave, one per question they answered; leave out the ones they skipped.',
|
|
19
|
+
items: {
|
|
20
|
+
type: 'object',
|
|
21
|
+
properties: {
|
|
22
|
+
question: { type: 'integer', description: 'The question number from prepare_application.' },
|
|
23
|
+
answer: { type: 'string', description: 'For OPEN, the talent\'s text. For MULTIPLE_CHOICE, exactly one of that question\'s options.' },
|
|
24
|
+
},
|
|
25
|
+
required: ['question', 'answer'],
|
|
26
|
+
},
|
|
27
|
+
},
|
|
28
|
+
confirm: { type: 'boolean', description: 'true ONLY after the talent explicitly said yes to the summary this tool returned for these exact arguments.' },
|
|
29
|
+
},
|
|
30
|
+
required: ['id'],
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
function parseAnswers(raw, questions) {
|
|
34
|
+
const answers = [];
|
|
35
|
+
const invalid = [];
|
|
36
|
+
for (const entry of Array.isArray(raw) ? raw : []) {
|
|
37
|
+
const q = entry && Number.isInteger(entry.question) ? questions[entry.question - 1] : null;
|
|
38
|
+
const said = entry && typeof entry.answer === 'string' ? entry.answer.trim() : '';
|
|
39
|
+
if (!q) { invalid.push({ question: entry && entry.question, reason: 'unknown-question' }); continue; }
|
|
40
|
+
if (!said) continue;
|
|
41
|
+
if (q.type === 'MULTIPLE_CHOICE') {
|
|
42
|
+
const option = q.options.find((o) => o.toLowerCase() === said.toLowerCase());
|
|
43
|
+
if (!option) { invalid.push({ question: entry.question, reason: 'not-an-option', options: q.options }); continue; }
|
|
44
|
+
answers.push({ question: q.text, answer: [option] });
|
|
45
|
+
} else {
|
|
46
|
+
answers.push({ question: q.text, answer: said });
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return { answers, invalid };
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function makeApplyTools(deps = {}) {
|
|
53
|
+
const {
|
|
54
|
+
loadApplication = (opts) => require('./apply-client').loadApplication({}, opts),
|
|
55
|
+
submitApplication = (opts) => require('./apply-client').submitApplication({}, opts),
|
|
56
|
+
applicationSummary = require('./apply-client').applicationSummary,
|
|
57
|
+
loadAuthSession = require('./auth-session-store').loadAuthSession,
|
|
58
|
+
sessionStatus = require('./auth-session-store').sessionStatus,
|
|
59
|
+
applyCopy = () => require('./signup-copy').applyCopy(require('./signup-language').signupLanguage(require('./i18n').detectFlowLang())),
|
|
60
|
+
} = deps;
|
|
61
|
+
|
|
62
|
+
const shownSummaries = new Set();
|
|
63
|
+
|
|
64
|
+
const requireLoginSession = () => {
|
|
65
|
+
const session = loadAuthSession();
|
|
66
|
+
if (sessionStatus(session) !== 'active') {
|
|
67
|
+
throw new Error('no active Shakers session — sign in with social_signin_start (or run `shakers login`) first (this tool is session-gated).');
|
|
68
|
+
}
|
|
69
|
+
return session;
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
async function readApplication(args) {
|
|
73
|
+
const session = requireLoginSession();
|
|
74
|
+
const positionId = typeof args.id === 'string' ? args.id.trim() : '';
|
|
75
|
+
if (!positionId) return { session, result: { ok: false, reason: 'no-id' } };
|
|
76
|
+
const app = await loadApplication({ hubAccessToken: session.hubAccessToken, positionId });
|
|
77
|
+
if (!app.ok) return { session, result: { ok: false, reason: app.reason } };
|
|
78
|
+
if (!app.open) return { session, result: { ok: false, reason: 'not-open', relayVerbatim: applyCopy().notOpen, message: 'Nothing was sent. Print relayVerbatim word for word.' } };
|
|
79
|
+
return { session, positionId, app };
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
async function prepareApplication(args = {}) {
|
|
83
|
+
const { result, app } = await readApplication(args);
|
|
84
|
+
if (result) return result;
|
|
85
|
+
const t = applyCopy();
|
|
86
|
+
return {
|
|
87
|
+
ok: true,
|
|
88
|
+
title: app.title,
|
|
89
|
+
company: app.company,
|
|
90
|
+
mainMotivationQuestion: app.mainMotivationQuestion || t.mainMotivationFallback,
|
|
91
|
+
questions: app.questions.map((q, i) => ({ number: i + 1, ...q })),
|
|
92
|
+
reviewedByShakers: !!app.doubleCheck && app.doubleCheck !== 'NO',
|
|
93
|
+
message: 'Nothing has been sent. Ask the talent the mainMotivationQuestion and then each question, one at a time, in their language; for MULTIPLE_CHOICE list its options numbered. Every answer is optional. Use only what the talent writes: never invent, complete or improve the message or the answers. Then call apply_to_project with them.',
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
async function applyToProject(args = {}) {
|
|
98
|
+
const { session, positionId, app, result } = await readApplication(args);
|
|
99
|
+
if (result) return result;
|
|
100
|
+
const t = applyCopy();
|
|
101
|
+
const { answers, invalid } = parseAnswers(args.answers, app.questions);
|
|
102
|
+
if (invalid.length) {
|
|
103
|
+
return { ok: false, reason: 'invalid-answers', invalid, message: 'Nothing was sent. Some answers do not match the questions from prepare_application: ask the talent again for those and call apply_to_project with the question number and, for MULTIPLE_CHOICE, one of its options exactly.' };
|
|
104
|
+
}
|
|
105
|
+
const message = typeof args.message === 'string' ? args.message.trim() : '';
|
|
106
|
+
const key = JSON.stringify([positionId, message, answers]);
|
|
107
|
+
if (args.confirm !== true || !shownSummaries.has(key)) {
|
|
108
|
+
shownSummaries.add(key);
|
|
109
|
+
return {
|
|
110
|
+
ok: false,
|
|
111
|
+
reason: 'confirmation-required',
|
|
112
|
+
say: [...applicationSummary(t, app, { message, answers }), t.confirmQuestion].join('\n\n'),
|
|
113
|
+
message: "Nothing has been sent. Print say word for word and wait for the talent. Call apply_to_project again with exactly the same arguments and confirm:true only if the talent explicitly says yes to sending it; if they want changes, collect them and call it without confirm. Only the talent decides: never confirm on their behalf, and never invent the message or the answers.",
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
shownSummaries.delete(key);
|
|
117
|
+
const mainMotivationQuestion = app.mainMotivationQuestion || t.mainMotivationFallback;
|
|
118
|
+
const res = await submitApplication({ hubAccessToken: session.hubAccessToken, positionId, message, answers, mainMotivationQuestion });
|
|
119
|
+
if (!res.ok) {
|
|
120
|
+
if (res.reason === 'already-applied') return { ok: false, reason: res.reason, applicationStatus: res.applicationStatus, relayVerbatim: t.alreadyApplied(res.applicationStatus), message: 'Print relayVerbatim word for word.' };
|
|
121
|
+
if (res.reason === 'position-unavailable') return { ok: false, reason: res.reason, relayVerbatim: t.notOpen, message: 'Print relayVerbatim word for word.' };
|
|
122
|
+
return { ok: false, reason: res.reason, message: 'The application was not sent. Tell the talent it could not be sent and that they can try again later.' };
|
|
123
|
+
}
|
|
124
|
+
return {
|
|
125
|
+
ok: true,
|
|
126
|
+
status: res.status,
|
|
127
|
+
relayVerbatim: res.status === 'PENDING' ? t.sentPending : t.sent,
|
|
128
|
+
message: 'Print relayVerbatim word for word.',
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return [
|
|
133
|
+
{
|
|
134
|
+
name: 'prepare_application',
|
|
135
|
+
title: 'See what applying to a project asks',
|
|
136
|
+
description: 'Reads the questions you are asked when you apply to a Shakers project. It sends nothing.',
|
|
137
|
+
guidance: "Read-only first step to apply to a project/Position as an already-registered talent: returns its title, company, the main motivation question and the extra questions (OPEN or MULTIPLE_CHOICE with options), numbered. If reason is not-open, the talent already applied or the project is closed: print relayVerbatim and stop. Then ask the talent each question in the chat and call apply_to_project with their own words. Never invent or complete answers. Requires an active session and the position id.",
|
|
138
|
+
inputSchema: PREPARE_APPLICATION_SCHEMA,
|
|
139
|
+
handler: prepareApplication,
|
|
140
|
+
},
|
|
141
|
+
{
|
|
142
|
+
name: 'apply_to_project',
|
|
143
|
+
title: 'Apply to a project',
|
|
144
|
+
description: 'Shows you exactly what your application to a Shakers project will send and sends it only after you say yes.',
|
|
145
|
+
guidance: "Apply to a project/Position for the talent with their message and their answers to the prepare_application questions. Only the message and the answers are sent: never budget, phone, location or availability. Call it first without confirm: it returns `say`, the literal summary and the question, to print word for word. Only when the talent explicitly answers yes, call it again with exactly the same arguments and confirm:true; a confirm without that summary shown counts for nothing. Never confirm on the talent's behalf and never invent, complete or improve their message or answers. Print relayVerbatim word for word. Requires an active session and the position id, which is an internal handle never shown to the talent.",
|
|
146
|
+
inputSchema: APPLY_TO_PROJECT_SCHEMA,
|
|
147
|
+
handler: applyToProject,
|
|
148
|
+
},
|
|
149
|
+
];
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
module.exports = { makeApplyTools, parseAnswers, PREPARE_APPLICATION_SCHEMA, APPLY_TO_PROJECT_SCHEMA };
|