shakers-cli 0.15.0 → 0.17.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +56 -1203
- package/bin/ai-usage.js +32 -58
- package/bin/login.js +10 -10
- package/bin/mcp.js +17 -12
- package/bin/mcpb.js +1 -1
- package/bin/onboarding.js +3 -2
- package/bin/register.js +19 -5
- package/bin/report-html.js +2 -2
- package/bin/shakers-cli.js +352 -0
- package/bin/shakers.js +20 -344
- package/bin/share.js +1 -1
- package/bin/superadmin.js +5 -5
- package/bin/version.js +3 -0
- package/package.json +12 -3
- package/src/account-marker.js +40 -0
- package/src/account-trace.js +23 -0
- package/src/agent-category.js +1 -1
- package/src/agent-evaluation.js +3 -3
- package/src/agent-org-chart.js +5 -5
- package/src/agent-synthesis.js +3 -3
- package/src/agent-usage.js +1 -1
- package/src/agents-client.js +4 -4
- package/src/ai-usage-review.js +101 -0
- package/src/ai.js +1 -1
- package/src/ansi.js +1 -1
- package/src/auth-client.js +18 -4
- package/src/auth-method-picker.js +1 -1
- package/src/auth-methods.js +1 -1
- package/src/auth-session-store.js +18 -8
- package/src/authorship-mismatch.js +1 -1
- package/src/authorship.js +2 -2
- package/src/automations-detector.js +1 -1
- package/src/backend-request.js +5 -3
- package/src/brand-ansi.js +1 -1
- package/src/browser-tools-detector.js +1 -1
- package/src/certify-disclaimer.js +1 -1
- package/src/certify-remediation-prompt.js +1 -1
- package/src/cli-args.js +2 -2
- package/src/command-audit.js +8 -5
- package/src/config-dir.js +1 -1
- package/src/config.js +114 -48
- package/src/consent-flow.js +7 -6
- package/src/consent-skip.js +1 -1
- package/src/detection-evidence.js +2 -2
- package/src/detection-noise.js +1 -1
- package/src/detectors.js +1 -1
- package/src/device-login.js +12 -1
- package/src/email-verification.js +3 -3
- package/src/env-paths.js +1 -1
- package/src/find-projects-client.js +12 -0
- package/src/find-projects-flow.js +1 -13
- package/src/git-activity.js +1 -1
- package/src/graph-certs.js +3 -3
- package/src/graph-scan.js +9 -9
- package/src/i18n.js +148 -138
- package/src/interactive-select.js +1 -1
- package/src/interview-client.js +6 -1
- package/src/interview-transcript.js +1 -1
- package/src/legal-notice.js +1 -1
- package/src/maturity.js +6 -6
- package/src/mcp-ai-usage-result-tool.js +34 -12
- package/src/mcp-ai-usage-tool.js +282 -163
- package/src/mcp-auth-form.js +7 -3
- package/src/mcp-detector.js +3 -3
- package/src/mcp-device-login-tools.js +2 -1
- package/src/mcp-find-projects-tools.js +1 -2
- package/src/mcp-install.js +279 -44
- package/src/mcp-instructions.js +1 -1
- package/src/mcp-onboarding-tools.js +4 -4
- package/src/mcp-prompts.js +4 -4
- package/src/mcp-register-tools.js +1 -0
- package/src/mcp-role-tools.js +24 -1
- package/src/mcp-server.js +5 -1
- package/src/mcp-signup-client.js +2 -2
- package/src/mcp-signup-tools.js +39 -9
- package/src/memory-structure-detector.js +1 -1
- package/src/model-call-record.js +2 -2
- package/src/onboarding-flow.js +63 -42
- package/src/onboarding-funnel.js +153 -20
- package/src/open-file.js +1 -1
- package/src/osc-link.js +1 -1
- package/src/portfolio-client.js +11 -1
- package/src/portfolio-git-info.js +1 -1
- package/src/profile-flow.js +1 -1
- package/src/profile-switch.js +5 -12
- package/src/render-certification.js +10 -10
- package/src/render-html.js +6 -6
- package/src/render-report-text.js +1 -1
- package/src/render-sheet.js +10 -10
- package/src/render-terminal.js +6 -6
- package/src/repo-scope.js +132 -0
- package/src/report-gating.js +5 -3
- package/src/report-store.js +13 -8
- package/src/roadmap-content.js +5 -5
- package/src/roadmap-personalization.js +3 -3
- package/src/roadmap-prompt.js +2 -2
- package/src/roles-flow.js +8 -5
- package/src/scan-exclusions.js +1 -1
- package/src/scanner.js +14 -14
- package/src/secret-prompt.js +1 -1
- package/src/session-gate.js +1 -1
- package/src/session-refresh.js +33 -3
- package/src/session-scan.js +1 -1
- package/src/shakers-wordmark.js +1 -1
- package/src/share-card.js +2 -2
- package/src/share-certification.js +4 -4
- package/src/share-consent-state.js +9 -9
- package/src/share-payload.js +7 -7
- package/src/share.js +7 -7
- package/src/signup-client.js +1 -1
- package/src/signup-copy.js +177 -2
- package/src/skills-client.js +1 -1
- package/src/start-add-agents.js +2 -2
- package/src/start-add-portfolio.js +3 -13
- package/src/start-add-skills.js +2 -2
- package/src/stdin-ask.js +16 -4
- package/src/strip-internal-comments.js +1 -1
- package/src/tech-detector.js +2 -2
- package/src/templates/report-sheet.html +11 -13
- package/src/terminal-agents.js +8 -8
- package/src/terminal-format.js +3 -3
- package/src/terminal-sections.js +11 -11
- package/src/tier-engine.js +3 -3
- package/src/tier-projection.js +2 -2
- package/src/transcript-turns.js +1 -1
- package/src/typewriter.js +1 -1
- package/src/usage-model-calls.js +6 -6
- package/src/usage-oneshot-commands.js +5 -5
- package/src/usage-repo-scope.js +11 -129
- package/src/usage-run-steps.js +82 -181
- package/src/usage-scan.js +235 -0
- package/src/work-streams.js +1 -1
package/src/usage-run-steps.js
CHANGED
|
@@ -1,60 +1,32 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
const { scan, scanForRoots } = require('./scanner');
|
|
4
|
-
const { classify } = require('./maturity');
|
|
5
3
|
const { renderTerminal } = require('./render-terminal');
|
|
6
|
-
const { reportGateForIdentity } = require('./report-gating');
|
|
7
4
|
const { revealText } = require('./typewriter');
|
|
8
5
|
const { persistFootprint } = require('./report-store');
|
|
9
6
|
const {
|
|
10
7
|
loadConsentState,
|
|
11
8
|
getConsentDecision,
|
|
12
|
-
recordConsent,
|
|
13
|
-
isValidEmail,
|
|
14
|
-
normalizeEmail,
|
|
15
9
|
autoShare,
|
|
16
10
|
consentPath,
|
|
11
|
+
isThrottled,
|
|
17
12
|
} = require('./share');
|
|
18
13
|
const { runConsentPrompt } = require('./consent-flow');
|
|
19
14
|
const { createStdinAsk } = require('./stdin-ask');
|
|
20
|
-
const {
|
|
21
|
-
parseAgentDescriptions,
|
|
22
|
-
parseAgentOrgChartForRoots,
|
|
23
|
-
parseAgentDescriptionsForRoots,
|
|
24
|
-
parseAgentDefinitionsForRoots,
|
|
25
|
-
} = require('./agent-org-chart');
|
|
26
|
-
const { collectAgentUsage } = require('./agent-usage');
|
|
27
|
-
const { collectSessionSignals } = require('./session-signals');
|
|
28
|
-
const {
|
|
29
|
-
collectScopedGitActivity,
|
|
30
|
-
collectScopedWorkStreams,
|
|
31
|
-
collectScopedDetectionEvidence,
|
|
32
|
-
} = require('./scope-signals');
|
|
33
|
-
const { detectAuthorshipMismatch } = require('./authorship-mismatch');
|
|
34
|
-
const { MODEL_CALL, REASON, skipped, attachModelCall } = require('./model-call-record');
|
|
35
|
-
const {
|
|
36
|
-
getSynthesisEndpoint,
|
|
37
|
-
getRoadmapEndpoint,
|
|
38
|
-
getAgentEvaluationEndpoint,
|
|
39
|
-
getAiProfileEndpoint,
|
|
40
|
-
} = require('./config');
|
|
15
|
+
const { getAiProfileEndpoint } = require('./config');
|
|
41
16
|
// "My work with AI" preview, read from the hub ai-profile (same as the front).
|
|
42
17
|
const { resolveAiProfilePreview } = require('./ai-profile-preview');
|
|
43
18
|
const { renderAiProfileTerminal } = require('./render-ai-fluency');
|
|
44
19
|
const { ensureFreshSession } = require('./session-refresh');
|
|
45
20
|
const { loadAuthSession, sessionStatus } = require('./auth-session-store');
|
|
46
21
|
const { c: termColors } = require('./terminal-format');
|
|
47
|
-
const {
|
|
22
|
+
const { withSpinner } = require('./terminal-progress');
|
|
48
23
|
const { computeConsentSkip } = require('./consent-skip');
|
|
49
|
-
const {
|
|
50
|
-
const {
|
|
51
|
-
maybeSynthesizeAgents,
|
|
52
|
-
maybeEvaluateAgents,
|
|
53
|
-
maybePersonalizeRoadmap,
|
|
54
|
-
} = require('./usage-model-calls');
|
|
24
|
+
const { collectIdentityAndUsage, storedAiConsent, reanchorReusedGrant } = require('./usage-scan');
|
|
55
25
|
const { doBuildNextLevel } = require('./usage-oneshot-commands');
|
|
26
|
+
const { uploadFacts, withoutExcluded, summaryText, explanationText } = require('./ai-usage-review');
|
|
27
|
+
const { reviewCopy } = require('./signup-copy');
|
|
28
|
+
const { askYesNo } = require('./consent-flow');
|
|
56
29
|
|
|
57
|
-
// Automatic, silent PERSISTING once consent is `granted` (ADR-007, gating revised by ADR-011: consent controls persistence only, never what's shown).
|
|
58
30
|
async function maybeAutoShare(report, maturity, root, catalog, { bypassThrottle = false } = {}) {
|
|
59
31
|
try {
|
|
60
32
|
const result = await autoShare(report, maturity, { root, bypassThrottle });
|
|
@@ -82,35 +54,17 @@ function sendStatusColor(reason) {
|
|
|
82
54
|
return SEND_WARN_REASONS.has(reason) ? termColors.warning : termColors.danger;
|
|
83
55
|
}
|
|
84
56
|
|
|
85
|
-
function performScan({ opts, catalog }) {
|
|
86
|
-
const root = opts.root;
|
|
87
|
-
const report = withStaticStatus(catalog.cli.scanningLabel, () => scan({ root }));
|
|
88
|
-
// `let`, not `const`: this is the cwd tier.
|
|
89
|
-
const maturity = classify(report);
|
|
90
|
-
|
|
91
|
-
if (Array.isArray(report.agents) && report.agents.length > 0) {
|
|
92
|
-
report.agentDescriptions = parseAgentDescriptions(root || process.cwd());
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
return { report, maturity, root, rootDir: root || process.cwd() };
|
|
96
|
-
}
|
|
97
|
-
|
|
98
57
|
// The identity gate + local (no-network, no-LLM) agent-usage signal.
|
|
99
58
|
function prepareIdentityAndUsage({ report, catalog, quiet = false }) {
|
|
100
|
-
const
|
|
101
|
-
const gate = reportGateForIdentity();
|
|
102
|
-
|
|
59
|
+
const identity = collectIdentityAndUsage({ report });
|
|
103
60
|
// --json owns stdout: one parseable document, and the MCP's JSON-RPC channel.
|
|
104
|
-
if (gate.showFrameworkIntro && !quiet) process.stdout.write(`\n ${catalog.cli.frameworkIntroUsage}\n`);
|
|
105
|
-
|
|
106
|
-
// Local, no network, no LLM: how often each detected agent was invoked in the local Claude Code history.
|
|
107
|
-
if (hasAgents) report.agentUsage = collectAgentUsage(report.agents);
|
|
108
|
-
|
|
109
|
-
return { gate, hasAgents };
|
|
61
|
+
if (identity.gate.showFrameworkIntro && !quiet) process.stdout.write(`\n ${catalog.cli.frameworkIntroUsage}\n`);
|
|
62
|
+
return identity;
|
|
110
63
|
}
|
|
111
64
|
|
|
112
65
|
async function resolveAiConsent({ opts, catalog, gate, injectedAsk }) {
|
|
113
|
-
// CONSENT GATE, MOVED TO BEFORE ANY AI/EGRESS CALL (
|
|
66
|
+
// CONSENT GATE, MOVED TO BEFORE ANY AI/EGRESS CALL (explicit product decision, not a bug fix).
|
|
67
|
+
if (opts.json) return storedAiConsent({ gate });
|
|
114
68
|
const state = loadConsentState();
|
|
115
69
|
const decision = getConsentDecision(state);
|
|
116
70
|
const consentSkip = computeConsentSkip({
|
|
@@ -122,143 +76,90 @@ async function resolveAiConsent({ opts, catalog, gate, injectedAsk }) {
|
|
|
122
76
|
});
|
|
123
77
|
|
|
124
78
|
let aiConsentGranted;
|
|
125
|
-
// A REUSED grant (skip
|
|
79
|
+
// A REUSED grant (skip) carries a stale consent email; a fresh prompt re-anchors itself.
|
|
126
80
|
let reusedStoredConsent = false;
|
|
127
|
-
if (
|
|
128
|
-
|
|
129
|
-
|
|
81
|
+
if (consentSkip.message) {
|
|
82
|
+
process.stdout.write(`\n ${consentSkip.message}\n`);
|
|
83
|
+
}
|
|
84
|
+
if (consentSkip.skip) {
|
|
85
|
+
// A TERMINAL decision already exists (a prior run's explicit grant or
|
|
86
|
+
// denial) — never re-asked.
|
|
87
|
+
aiConsentGranted = decision === 'granted';
|
|
130
88
|
reusedStoredConsent = true;
|
|
131
89
|
} else {
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
} else {
|
|
141
|
-
const ask = injectedAsk || createStdinAsk();
|
|
142
|
-
let result;
|
|
143
|
-
try {
|
|
144
|
-
result = await runConsentPrompt({
|
|
145
|
-
ask, catalog, loggedIn: gate.loggedIn, sessionEmail: gate.email, profile: gate.profile,
|
|
146
|
-
});
|
|
147
|
-
} finally {
|
|
148
|
-
if (!injectedAsk) ask.close();
|
|
149
|
-
}
|
|
150
|
-
aiConsentGranted = result === 'granted';
|
|
151
|
-
}
|
|
152
|
-
if (!aiConsentGranted) {
|
|
153
|
-
// No early return (reverted 8867450's wall): the local report still renders further down, same as ADR-011 always promised.
|
|
154
|
-
process.stdout.write(`\n ${termColors.warning}${catalog.consent.noReportWithoutConsent}${termColors.reset}\n\n`);
|
|
90
|
+
const ask = injectedAsk || createStdinAsk();
|
|
91
|
+
let result;
|
|
92
|
+
try {
|
|
93
|
+
result = await runConsentPrompt({
|
|
94
|
+
ask, catalog, loggedIn: gate.loggedIn, sessionEmail: gate.email, profile: gate.profile,
|
|
95
|
+
});
|
|
96
|
+
} finally {
|
|
97
|
+
if (!injectedAsk) ask.close();
|
|
155
98
|
}
|
|
99
|
+
aiConsentGranted = result === 'granted';
|
|
156
100
|
}
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
recordConsent('granted', gate.email, { verified: true });
|
|
101
|
+
if (!aiConsentGranted) {
|
|
102
|
+
// No early return: the local report still renders further down.
|
|
103
|
+
process.stdout.write(`\n ${termColors.warning}${catalog.consent.noReportWithoutConsent}${termColors.reset}\n\n`);
|
|
161
104
|
}
|
|
105
|
+
if (reusedStoredConsent && aiConsentGranted) reanchorReusedGrant({ gate, state });
|
|
162
106
|
return aiConsentGranted;
|
|
163
107
|
}
|
|
164
108
|
|
|
165
|
-
function
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
report.workStreams = collectScopedWorkStreams(scope.toplevels);
|
|
173
|
-
report.steering = sessionSignals.steering;
|
|
174
|
-
report.decisions = sessionSignals.decisions;
|
|
175
|
-
report.detectionEvidence = collectScopedDetectionEvidence(
|
|
176
|
-
scope.toplevels,
|
|
177
|
-
consentEmail,
|
|
178
|
-
report,
|
|
179
|
-
);
|
|
180
|
-
|
|
181
|
-
const scoped = scanForRoots(agentRoots, { root });
|
|
182
|
-
report.tools = scoped.tools;
|
|
183
|
-
report.summary = scoped.summary;
|
|
184
|
-
report.technologies = scoped.technologies;
|
|
185
|
-
report.mcp = scoped.mcp;
|
|
186
|
-
report.memory = scoped.memory;
|
|
187
|
-
report.automations = scoped.automations;
|
|
188
|
-
report.browserTools = scoped.browserTools;
|
|
189
|
-
report.agentCounts = scoped.agentCounts;
|
|
190
|
-
report.environment = scoped.environment;
|
|
191
|
-
|
|
192
|
-
report.agents = parseAgentOrgChartForRoots(agentRoots);
|
|
193
|
-
report.agentCounts.agents = report.agents.length;
|
|
194
|
-
if (report.agents.length > 0) {
|
|
195
|
-
report.agentDescriptions = parseAgentDescriptionsForRoots(agentRoots);
|
|
196
|
-
report.agentDefinitions = parseAgentDefinitionsForRoots(agentRoots);
|
|
197
|
-
report.agentUsage = collectAgentUsage(report.agents);
|
|
198
|
-
}
|
|
199
|
-
});
|
|
109
|
+
function matchNames(raw, known) {
|
|
110
|
+
const wanted = String(raw || '').split(',').map((x) => x.trim()).filter(Boolean);
|
|
111
|
+
const byLower = new Map(known.map((name) => [name.toLowerCase(), name]));
|
|
112
|
+
return {
|
|
113
|
+
matched: wanted.map((w) => byLower.get(w.toLowerCase())).filter(Boolean),
|
|
114
|
+
unknown: wanted.filter((w) => !byLower.has(w.toLowerCase())),
|
|
115
|
+
};
|
|
200
116
|
}
|
|
201
117
|
|
|
202
|
-
async function
|
|
203
|
-
|
|
204
|
-
})
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
const willAttemptEvaluation = aiConsentGranted && hasAgents && !!getAgentEvaluationEndpoint();
|
|
208
|
-
const roadmapEntryForTier = maturity.tierKey ? getRoadmapEntry(maturity.tierKey, lang) : null;
|
|
209
|
-
const willAttemptRoadmapPersonalization =
|
|
210
|
-
aiConsentGranted
|
|
211
|
-
&& gate.showRoadmap
|
|
212
|
-
&& !!roadmapEntryForTier && !roadmapEntryForTier.maxTier
|
|
213
|
-
&& !!getRoadmapEndpoint();
|
|
214
|
-
|
|
215
|
-
// THE THREE MODEL CALLS RUN ONE AFTER ANOTHER, NOT IN PARALLEL (issue 116).
|
|
216
|
-
const runPhase = (willAttempt, label, call) => (willAttempt ? withSpinner(label, call) : call());
|
|
217
|
-
|
|
218
|
-
// talents-ai-score, ADR-051: `aiConsentGranted` is `false` whenever consent for this run was declined, denied in the past, or (on `--json`) simply never granted.
|
|
219
|
-
let synthesis = null;
|
|
220
|
-
let evaluation = null;
|
|
221
|
-
let roadmapPersonalization = null;
|
|
222
|
-
if (aiConsentGranted && !opts.noAi) {
|
|
223
|
-
synthesis = await runPhase(
|
|
224
|
-
willAttemptSynthesis,
|
|
225
|
-
catalog.cli.synthesizingLabel,
|
|
226
|
-
() => maybeSynthesizeAgents(report, rootDir),
|
|
227
|
-
);
|
|
228
|
-
evaluation = hasAgents
|
|
229
|
-
? await runPhase(
|
|
230
|
-
willAttemptEvaluation,
|
|
231
|
-
catalog.cli.evaluatingAgentsLabel,
|
|
232
|
-
() => maybeEvaluateAgents(report, agentRoots, lang),
|
|
233
|
-
)
|
|
234
|
-
: null;
|
|
235
|
-
// issue 123 / ADR-044: `runPhase` runs `call()` whether or not a spinner is shown, so gating the roadmap CANNOT be done by flipping `willAttempt` — that only hides the spinner.
|
|
236
|
-
roadmapPersonalization = gate.showRoadmap
|
|
237
|
-
? await runPhase(
|
|
238
|
-
willAttemptRoadmapPersonalization,
|
|
239
|
-
catalog.cli.personalizingRoadmapLabel,
|
|
240
|
-
() => maybePersonalizeRoadmap(report, maturity, lang),
|
|
241
|
-
)
|
|
242
|
-
: null;
|
|
243
|
-
} else {
|
|
244
|
-
const skipReason = aiConsentGranted ? REASON.NOT_APPLICABLE : REASON.NO_CONSENT;
|
|
245
|
-
attachModelCall(report, MODEL_CALL.SYNTHESIS, skipped(skipReason));
|
|
246
|
-
attachModelCall(report, MODEL_CALL.EVALUATION, skipped(skipReason));
|
|
247
|
-
attachModelCall(report, MODEL_CALL.ROADMAP, skipped(skipReason));
|
|
118
|
+
async function askExcludedAgents(ask, c, known, out) {
|
|
119
|
+
for (let attempt = 0; attempt < 3; attempt += 1) {
|
|
120
|
+
const { matched, unknown } = matchNames(await ask(c.cliExcludeAgents), known);
|
|
121
|
+
if (unknown.length === 0) return matched;
|
|
122
|
+
out.write(` ${c.cliUnknownAgents(unknown.join(', '), known.join(', '))}\n`);
|
|
248
123
|
}
|
|
124
|
+
return null;
|
|
125
|
+
}
|
|
249
126
|
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
if (
|
|
127
|
+
async function reviewUpload({ report, maturity, repos = [], lang, catalog, injectedAsk = null, out = process.stdout, bypassThrottle = false }) {
|
|
128
|
+
const state = loadConsentState();
|
|
129
|
+
if (getConsentDecision(state) !== 'granted' || !state || state.emailVerified === false || !state.email || (!bypassThrottle && isThrottled(state))) {
|
|
130
|
+
return { approved: true, report, maturity };
|
|
131
|
+
}
|
|
132
|
+
const c = reviewCopy(lang);
|
|
133
|
+
const facts = uploadFacts({ report, maturity, repos });
|
|
134
|
+
out.write(`\n${summaryText(lang, facts)}\n\n${explanationText(lang)}\n\n`);
|
|
135
|
+
if (facts.repos.length > 1) out.write(`${c.cliRepoHint}\n\n`);
|
|
136
|
+
const ask = injectedAsk || createStdinAsk();
|
|
137
|
+
try {
|
|
138
|
+
const approved = await askYesNo(ask, c.cliQuestion, catalog, { defaultNo: true });
|
|
139
|
+
if (!approved) {
|
|
140
|
+
out.write(` ${c.declined}\n`);
|
|
141
|
+
return { approved: false };
|
|
142
|
+
}
|
|
143
|
+
const agents = facts.agents.length > 0 ? await askExcludedAgents(ask, c, facts.agents, out) : [];
|
|
144
|
+
if (agents === null) {
|
|
145
|
+
out.write(` ${c.declined}\n`);
|
|
146
|
+
return { approved: false };
|
|
147
|
+
}
|
|
148
|
+
const hasExcerpts = facts.conversationExcerpts + facts.commandExcerpts > 0;
|
|
149
|
+
const excerpts = hasExcerpts ? (await askYesNo(ask, c.cliIncludeExcerpts, catalog, { defaultYes: true })) === true : true;
|
|
150
|
+
return { approved: true, ...withoutExcluded(report, { agents, excerpts }) };
|
|
151
|
+
} finally {
|
|
152
|
+
if (!injectedAsk) ask.close();
|
|
153
|
+
}
|
|
253
154
|
}
|
|
254
155
|
|
|
255
|
-
async function renderAndFinalize({ report, maturity, lang, opts, root, gate, catalog, bypassThrottle = false }) {
|
|
156
|
+
async function renderAndFinalize({ report, maturity, lang, opts, root, gate, catalog, bypassThrottle = false, repos = [], injectedAsk = null }) {
|
|
256
157
|
if (opts.json) {
|
|
257
158
|
process.stdout.write(JSON.stringify({ report, maturity }, null, 2) + '\n');
|
|
258
159
|
return;
|
|
259
160
|
}
|
|
260
161
|
|
|
261
|
-
//
|
|
162
|
+
// Revealed by SECTIONS instead of landing in one write.
|
|
262
163
|
if (gate.sessionStatus === 'expired') {
|
|
263
164
|
process.stdout.write(`\n ${termColors.warning}${catalog.terminal.sessionExpiredNotice}${termColors.reset}\n`);
|
|
264
165
|
}
|
|
@@ -276,14 +177,16 @@ async function renderAndFinalize({ report, maturity, lang, opts, root, gate, cat
|
|
|
276
177
|
}
|
|
277
178
|
}
|
|
278
179
|
|
|
279
|
-
// "Construir el siguiente nivel ahora"
|
|
180
|
+
// "Construir el siguiente nivel ahora": optional, only when
|
|
280
181
|
// explicitly requested — never part of a normal run otherwise.
|
|
281
182
|
if (opts.buildNextLevel) {
|
|
282
183
|
doBuildNextLevel(root, maturity, opts.force, catalog);
|
|
283
184
|
}
|
|
284
185
|
|
|
285
|
-
|
|
286
|
-
|
|
186
|
+
const reviewed = await reviewUpload({ report, maturity, repos, lang, catalog, injectedAsk, bypassThrottle });
|
|
187
|
+
if (!reviewed.approved) return;
|
|
188
|
+
const shareResult = await maybeAutoShare(reviewed.report, reviewed.maturity, root, catalog, { bypassThrottle });
|
|
189
|
+
if (shareResult && shareResult.ok) process.stdout.write(`\n ${reviewCopy(lang).afterwards}\n`);
|
|
287
190
|
|
|
288
191
|
// "My work with AI" preview, best-effort after the send; never blocks/breaks the run.
|
|
289
192
|
await maybeRenderAiProfile({ report, maturity, lang, root, save: opts.save, submitOk: !!(shareResult && shareResult.ok), catalog });
|
|
@@ -343,11 +246,9 @@ async function maybeRenderAiProfile({ report, maturity, lang, root, save, submit
|
|
|
343
246
|
}
|
|
344
247
|
|
|
345
248
|
module.exports = {
|
|
346
|
-
|
|
249
|
+
reviewUpload,
|
|
347
250
|
prepareIdentityAndUsage,
|
|
348
251
|
resolveAiConsent,
|
|
349
|
-
runAggregation,
|
|
350
|
-
runModelCalls,
|
|
351
252
|
renderAndFinalize,
|
|
352
253
|
shouldRenderAiProfile,
|
|
353
254
|
maybeRenderAiProfile,
|
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { scan, scanForRoots } = require('./scanner');
|
|
4
|
+
const { classify } = require('./maturity');
|
|
5
|
+
const { reportGateForIdentity } = require('./report-gating');
|
|
6
|
+
const {
|
|
7
|
+
loadConsentState,
|
|
8
|
+
getConsentDecision,
|
|
9
|
+
recordConsent,
|
|
10
|
+
isValidEmail,
|
|
11
|
+
normalizeEmail,
|
|
12
|
+
} = require('./share');
|
|
13
|
+
const {
|
|
14
|
+
parseAgentDescriptions,
|
|
15
|
+
parseAgentOrgChartForRoots,
|
|
16
|
+
parseAgentDescriptionsForRoots,
|
|
17
|
+
parseAgentDefinitionsForRoots,
|
|
18
|
+
} = require('./agent-org-chart');
|
|
19
|
+
const { collectAgentUsage } = require('./agent-usage');
|
|
20
|
+
const { collectSessionSignals } = require('./session-signals');
|
|
21
|
+
const {
|
|
22
|
+
collectScopedGitActivity,
|
|
23
|
+
collectScopedWorkStreams,
|
|
24
|
+
collectScopedDetectionEvidence,
|
|
25
|
+
} = require('./scope-signals');
|
|
26
|
+
const { detectAuthorshipMismatch } = require('./authorship-mismatch');
|
|
27
|
+
const { MODEL_CALL, REASON, skipped, attachModelCall } = require('./model-call-record');
|
|
28
|
+
const {
|
|
29
|
+
getSynthesisEndpoint,
|
|
30
|
+
getRoadmapEndpoint,
|
|
31
|
+
getAgentEvaluationEndpoint,
|
|
32
|
+
} = require('./config');
|
|
33
|
+
const { withStaticStatus, withSpinner } = require('./terminal-progress');
|
|
34
|
+
const { getRoadmapEntry } = require('./roadmap-content');
|
|
35
|
+
const {
|
|
36
|
+
maybeSynthesizeAgents,
|
|
37
|
+
maybeEvaluateAgents,
|
|
38
|
+
maybePersonalizeRoadmap,
|
|
39
|
+
} = require('./usage-model-calls');
|
|
40
|
+
const { parseArgs } = require('./cli-args');
|
|
41
|
+
const { detectReportLang, getCatalog } = require('./i18n');
|
|
42
|
+
const { prefetchGitLogs } = require('./git-log-prefetch');
|
|
43
|
+
const { GIT_ACTIVITY_LOG_ARGS } = require('./git-activity');
|
|
44
|
+
const { WORK_STREAMS_LOG_ARGS } = require('./work-streams');
|
|
45
|
+
const { AUTHORSHIP_LOG_ARGS } = require('./authorship');
|
|
46
|
+
const { resolveRepoScope } = require('./repo-scope');
|
|
47
|
+
|
|
48
|
+
function performScan({ opts, catalog }) {
|
|
49
|
+
const root = opts.root;
|
|
50
|
+
const report = withStaticStatus(catalog.cli.scanningLabel, () => scan({ root }));
|
|
51
|
+
// `let`, not `const`: this is the cwd tier.
|
|
52
|
+
const maturity = classify(report);
|
|
53
|
+
|
|
54
|
+
if (Array.isArray(report.agents) && report.agents.length > 0) {
|
|
55
|
+
report.agentDescriptions = parseAgentDescriptions(root || process.cwd());
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return { report, maturity, root, rootDir: root || process.cwd() };
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function collectIdentityAndUsage({ report }) {
|
|
62
|
+
const hasAgents = Array.isArray(report.agents) && report.agents.length > 0;
|
|
63
|
+
const gate = reportGateForIdentity();
|
|
64
|
+
if (hasAgents) report.agentUsage = collectAgentUsage(report.agents);
|
|
65
|
+
return { gate, hasAgents };
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Re-anchor a reused grant to the session's login-proven email: else a stale consent email ingests the report under another account and this session's AI profile never populates.
|
|
69
|
+
function reanchorReusedGrant({ gate, state }) {
|
|
70
|
+
if (gate.loggedIn && isValidEmail(gate.email) && state && state.email !== normalizeEmail(gate.email)) {
|
|
71
|
+
recordConsent('granted', gate.email, { verified: true });
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// `--json` never had an interactive prompt — its whole point is a single parseable stdout document, and it does not gain one now.
|
|
76
|
+
function storedAiConsent({ gate }) {
|
|
77
|
+
const state = loadConsentState();
|
|
78
|
+
const granted = getConsentDecision(state) === 'granted' && (!state || state.emailVerified !== false);
|
|
79
|
+
if (granted) reanchorReusedGrant({ gate, state });
|
|
80
|
+
return granted;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function runAggregation({ report, scope, consentEmail, agentRoots, root, catalog }) {
|
|
84
|
+
withStaticStatus(catalog.cli.aggregatingLabel, () => {
|
|
85
|
+
report.gitActivity = collectScopedGitActivity(scope.toplevels, consentEmail);
|
|
86
|
+
// Display-only transparency warning (never feeds scoring, see authorship-mismatch.js).
|
|
87
|
+
report.authorshipMismatch = detectAuthorshipMismatch(scope.toplevels, consentEmail, report.gitActivity);
|
|
88
|
+
const sessionSignals = collectSessionSignals(process.env, scope.selectedCwds);
|
|
89
|
+
report.sessions = sessionSignals.sessions;
|
|
90
|
+
report.workStreams = collectScopedWorkStreams(scope.toplevels);
|
|
91
|
+
report.steering = sessionSignals.steering;
|
|
92
|
+
report.decisions = sessionSignals.decisions;
|
|
93
|
+
report.detectionEvidence = collectScopedDetectionEvidence(
|
|
94
|
+
scope.toplevels,
|
|
95
|
+
consentEmail,
|
|
96
|
+
report,
|
|
97
|
+
);
|
|
98
|
+
|
|
99
|
+
const scoped = scanForRoots(agentRoots, { root });
|
|
100
|
+
report.tools = scoped.tools;
|
|
101
|
+
report.summary = scoped.summary;
|
|
102
|
+
report.technologies = scoped.technologies;
|
|
103
|
+
report.mcp = scoped.mcp;
|
|
104
|
+
report.memory = scoped.memory;
|
|
105
|
+
report.automations = scoped.automations;
|
|
106
|
+
report.browserTools = scoped.browserTools;
|
|
107
|
+
report.agentCounts = scoped.agentCounts;
|
|
108
|
+
report.environment = scoped.environment;
|
|
109
|
+
|
|
110
|
+
report.agents = parseAgentOrgChartForRoots(agentRoots);
|
|
111
|
+
report.agentCounts.agents = report.agents.length;
|
|
112
|
+
if (report.agents.length > 0) {
|
|
113
|
+
report.agentDescriptions = parseAgentDescriptionsForRoots(agentRoots);
|
|
114
|
+
report.agentDefinitions = parseAgentDefinitionsForRoots(agentRoots);
|
|
115
|
+
report.agentUsage = collectAgentUsage(report.agents);
|
|
116
|
+
}
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
async function runModelCalls({
|
|
121
|
+
report, maturity, aiConsentGranted, opts, gate, hasAgents, agentRoots, rootDir, lang, catalog,
|
|
122
|
+
}) {
|
|
123
|
+
// Which of the three will genuinely be ATTEMPTED.
|
|
124
|
+
const willAttemptSynthesis = aiConsentGranted && hasAgents && !!getSynthesisEndpoint();
|
|
125
|
+
const willAttemptEvaluation = aiConsentGranted && hasAgents && !!getAgentEvaluationEndpoint();
|
|
126
|
+
const roadmapEntryForTier = maturity.tierKey ? getRoadmapEntry(maturity.tierKey, lang) : null;
|
|
127
|
+
const roadmapRequested = opts.roadmap === true && gate.showRoadmap;
|
|
128
|
+
const willAttemptRoadmapPersonalization =
|
|
129
|
+
aiConsentGranted
|
|
130
|
+
&& roadmapRequested
|
|
131
|
+
&& !!roadmapEntryForTier && !roadmapEntryForTier.maxTier
|
|
132
|
+
&& !!getRoadmapEndpoint();
|
|
133
|
+
|
|
134
|
+
// THE THREE MODEL CALLS RUN ONE AFTER ANOTHER, NOT IN PARALLEL.
|
|
135
|
+
const runPhase = (willAttempt, label, call) => (willAttempt ? withSpinner(label, call) : call());
|
|
136
|
+
|
|
137
|
+
// `aiConsentGranted` is `false` whenever consent for this run was declined, denied in the past, or (on `--json`) simply never granted.
|
|
138
|
+
let synthesis = null;
|
|
139
|
+
let evaluation = null;
|
|
140
|
+
let roadmapPersonalization = null;
|
|
141
|
+
if (aiConsentGranted && !opts.noAi) {
|
|
142
|
+
synthesis = await runPhase(
|
|
143
|
+
willAttemptSynthesis,
|
|
144
|
+
catalog.cli.synthesizingLabel,
|
|
145
|
+
() => maybeSynthesizeAgents(report, rootDir),
|
|
146
|
+
);
|
|
147
|
+
evaluation = hasAgents
|
|
148
|
+
? await runPhase(
|
|
149
|
+
willAttemptEvaluation,
|
|
150
|
+
catalog.cli.evaluatingAgentsLabel,
|
|
151
|
+
() => maybeEvaluateAgents(report, agentRoots, lang),
|
|
152
|
+
)
|
|
153
|
+
: null;
|
|
154
|
+
// `runPhase` runs `call()` whether or not a spinner is shown, so gating the roadmap CANNOT be done by flipping `willAttempt` — that only hides the spinner.
|
|
155
|
+
roadmapPersonalization = roadmapRequested
|
|
156
|
+
? await runPhase(
|
|
157
|
+
willAttemptRoadmapPersonalization,
|
|
158
|
+
catalog.cli.personalizingRoadmapLabel,
|
|
159
|
+
() => maybePersonalizeRoadmap(report, maturity, lang),
|
|
160
|
+
)
|
|
161
|
+
: null;
|
|
162
|
+
} else {
|
|
163
|
+
const skipReason = aiConsentGranted ? REASON.NOT_APPLICABLE : REASON.NO_CONSENT;
|
|
164
|
+
attachModelCall(report, MODEL_CALL.SYNTHESIS, skipped(skipReason));
|
|
165
|
+
attachModelCall(report, MODEL_CALL.EVALUATION, skipped(skipReason));
|
|
166
|
+
attachModelCall(report, MODEL_CALL.ROADMAP, skipped(skipReason));
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
if (synthesis) report.agentSynthesis = synthesis;
|
|
170
|
+
if (evaluation) report.agentEvaluation = evaluation;
|
|
171
|
+
if (roadmapPersonalization) report.roadmapPersonalization = roadmapPersonalization;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// Steps 1-6 of a run: scan, scope, aggregate, classify; nothing is rendered or sent.
|
|
175
|
+
async function computeUsage({
|
|
176
|
+
opts,
|
|
177
|
+
catalog,
|
|
178
|
+
lang,
|
|
179
|
+
inRegistration = false,
|
|
180
|
+
prepareIdentity = collectIdentityAndUsage,
|
|
181
|
+
resolveConsent = storedAiConsent,
|
|
182
|
+
resolveScope = resolveRepoScope,
|
|
183
|
+
}) {
|
|
184
|
+
// 1. Local scan + classify (always computed and shown).
|
|
185
|
+
const { report, root, rootDir } = performScan({ opts, catalog });
|
|
186
|
+
|
|
187
|
+
// 2. Identity gate + local agent-usage signal.
|
|
188
|
+
const { gate: identityGate } = prepareIdentity({ report, catalog, quiet: opts.json === true });
|
|
189
|
+
const gate = (inRegistration && identityGate.showRoadmap)
|
|
190
|
+
? { ...identityGate, showRoadmap: false }
|
|
191
|
+
: identityGate;
|
|
192
|
+
|
|
193
|
+
// 3. Consent gate before any AI/egress call; a decline still renders.
|
|
194
|
+
const aiConsentGranted = await resolveConsent({ opts, catalog, gate });
|
|
195
|
+
|
|
196
|
+
// 4. Repo scoping (exclude client work).
|
|
197
|
+
const scope = await resolveScope({ opts, cwd: rootDir, catalog });
|
|
198
|
+
const consentEmail = (loadConsentState() || {}).email || null;
|
|
199
|
+
const agentRoots = scope.toplevels;
|
|
200
|
+
|
|
201
|
+
// 5. Aggregate scoped signals + rebuild scan/org-chart over the selection, re-classify.
|
|
202
|
+
await prefetchGitLogs(agentRoots, [GIT_ACTIVITY_LOG_ARGS, WORK_STREAMS_LOG_ARGS, AUTHORSHIP_LOG_ARGS]);
|
|
203
|
+
runAggregation({ report, scope, consentEmail, agentRoots, root, catalog });
|
|
204
|
+
const maturity = classify(report);
|
|
205
|
+
const hasAgents = Array.isArray(report.agents) && report.agents.length > 0;
|
|
206
|
+
report.analyzedRepos =
|
|
207
|
+
scope.mode === 'all'
|
|
208
|
+
? null
|
|
209
|
+
: scope.toplevels.map((tl) => tl.split(/[/\\]/).pop() || tl);
|
|
210
|
+
|
|
211
|
+
// 6. The three ephemeral model calls, serialized and gated by consent.
|
|
212
|
+
await runModelCalls({
|
|
213
|
+
report, maturity, aiConsentGranted, opts, gate, hasAgents, agentRoots, rootDir, lang, catalog,
|
|
214
|
+
});
|
|
215
|
+
return { report, maturity, root, gate, repos: scope.toplevels };
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// The MCP's scan: same steps, returned instead of printed, since stdout is its JSON-RPC channel.
|
|
219
|
+
async function scanUsage(argv) {
|
|
220
|
+
const opts = { ...parseArgs(argv), roadmap: false };
|
|
221
|
+
const lang = opts.lang || detectReportLang();
|
|
222
|
+
const { report, maturity, repos } = await computeUsage({ opts, catalog: getCatalog(lang), lang });
|
|
223
|
+
return { report, maturity, repos };
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
module.exports = {
|
|
227
|
+
performScan,
|
|
228
|
+
collectIdentityAndUsage,
|
|
229
|
+
reanchorReusedGrant,
|
|
230
|
+
storedAiConsent,
|
|
231
|
+
runAggregation,
|
|
232
|
+
runModelCalls,
|
|
233
|
+
computeUsage,
|
|
234
|
+
scanUsage,
|
|
235
|
+
};
|
package/src/work-streams.js
CHANGED
|
@@ -4,7 +4,7 @@ const { execFileSync } = require('child_process');
|
|
|
4
4
|
const { isGitRepo } = require('./git-activity');
|
|
5
5
|
const { takePrefetchedGitLog } = require('./git-log-prefetch');
|
|
6
6
|
|
|
7
|
-
// Deterministic WORK-STREAM structure for the project where `shakers ai-usage` runs
|
|
7
|
+
// Deterministic WORK-STREAM structure for the project where `shakers ai-usage` runs.
|
|
8
8
|
|
|
9
9
|
const GIT_TIMEOUT_MS = 15000;
|
|
10
10
|
const SESSION_GAP_SECONDS = 4 * 3600;
|