shakers-cli 0.14.2 → 0.16.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 +51 -1203
- package/bin/ai-usage.js +11 -56
- package/bin/login.js +10 -10
- package/bin/mcp.js +13 -9
- package/bin/mcpb.js +1 -1
- package/bin/onboarding.js +3 -2
- package/bin/register.js +17 -4
- 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/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.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 +11 -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 +20 -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 +112 -48
- package/src/consent-flow.js +4 -4
- 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 +124 -124
- 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-tool.js +1 -1
- package/src/mcp-auth-form.js +1 -1
- 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-onboarding-tools.js +4 -4
- package/src/mcp-prompts.js +2 -2
- package/src/mcp-register-tools.js +1 -0
- package/src/mcp-server.js +5 -1
- package/src/mcp-signup-client.js +2 -2
- package/src/mcp-signup-tools.js +26 -2
- package/src/memory-structure-detector.js +1 -1
- package/src/model-call-record.js +2 -2
- package/src/onboarding-flow.js +16 -16
- package/src/onboarding-funnel.js +110 -17
- 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-sheet.js +10 -10
- package/src/render-terminal.js +6 -6
- package/src/repo-scope.js +132 -0
- package/src/report-gating.js +1 -1
- package/src/report-store.js +6 -6
- package/src/roadmap-content.js +5 -5
- package/src/roadmap-personalization.js +3 -3
- package/src/roadmap-prompt.js +2 -2
- 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 +7 -7
- 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 +1 -1
- 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 +3 -3
- 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 +32 -184
- package/src/usage-scan.js +235 -0
- package/src/work-streams.js +1 -1
package/src/usage-repo-scope.js
CHANGED
|
@@ -1,69 +1,12 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
// Repo-scoping for the `ai-usage` command, extracted from bin/ai-usage.js
|
|
3
|
+
// Repo-scoping for the `ai-usage` command, extracted from bin/ai-usage.js.
|
|
4
4
|
|
|
5
|
-
const
|
|
6
|
-
const {
|
|
7
|
-
detectRepos,
|
|
8
|
-
repoIdentityForCwd,
|
|
9
|
-
matchReposByFlag,
|
|
10
|
-
scopeFromSelection,
|
|
11
|
-
indexOfCurrentRepo,
|
|
12
|
-
} = require('./detect-repos');
|
|
5
|
+
const core = require('./repo-scope');
|
|
13
6
|
const { promptSelect } = require('./prompt-select');
|
|
14
7
|
const { createStdinAsk } = require('./stdin-ask');
|
|
15
8
|
|
|
16
|
-
const
|
|
17
|
-
const SCOPE_MACHINE = 'machine';
|
|
18
|
-
const SCOPE_REPO = 'repo';
|
|
19
|
-
|
|
20
|
-
function currentRepoToplevel(cwd, repoIdentityFn = repoIdentityForCwd) {
|
|
21
|
-
try {
|
|
22
|
-
return repoIdentityFn(cwd).toplevel || cwd;
|
|
23
|
-
} catch {
|
|
24
|
-
return cwd;
|
|
25
|
-
}
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
// The real git toplevel of `cwd`, or null when `cwd` is not a repo (or is a refused system root like `/`, the MCP `cwd=/` case).
|
|
29
|
-
function cwdRepoToplevel(cwd, repoIdentityFn = repoIdentityForCwd) {
|
|
30
|
-
if (isRefusedWalkRoot(cwd)) return null;
|
|
31
|
-
try {
|
|
32
|
-
return repoIdentityFn(cwd).toplevel || null;
|
|
33
|
-
} catch {
|
|
34
|
-
return null;
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
// Pure: builds the machine-wide root set.
|
|
39
|
-
function machineWideToplevels(cwdRepoTop, detectedRepos, cap = MAX_MACHINE_WIDE_REPOS) {
|
|
40
|
-
const ordered = [];
|
|
41
|
-
const seen = new Set();
|
|
42
|
-
const push = (t) => {
|
|
43
|
-
if (typeof t !== 'string' || !t) return;
|
|
44
|
-
if (isRefusedWalkRoot(t)) return;
|
|
45
|
-
if (seen.has(t)) return;
|
|
46
|
-
seen.add(t);
|
|
47
|
-
ordered.push(t);
|
|
48
|
-
};
|
|
49
|
-
push(cwdRepoTop);
|
|
50
|
-
for (const r of detectedRepos || []) push(r && r.toplevel);
|
|
51
|
-
const capped = ordered.slice(0, cap);
|
|
52
|
-
return { toplevels: capped, total: ordered.length, capped: ordered.length > capped.length };
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
function cwdOnlyScope(repos, cwdToplevel) {
|
|
56
|
-
const idx = indexOfCurrentRepo(repos, cwdToplevel);
|
|
57
|
-
if (idx === -1) {
|
|
58
|
-
return { selectedCwds: new Set(), toplevels: [cwdToplevel], mode: 'cwd' };
|
|
59
|
-
}
|
|
60
|
-
const { selectedCwds, selectedToplevels } = scopeFromSelection(repos, [idx]);
|
|
61
|
-
return {
|
|
62
|
-
selectedCwds,
|
|
63
|
-
toplevels: selectedToplevels.length ? selectedToplevels : [cwdToplevel],
|
|
64
|
-
mode: 'cwd',
|
|
65
|
-
};
|
|
66
|
-
}
|
|
9
|
+
const { SCOPE_MACHINE, SCOPE_REPO, machineWideToplevels } = core;
|
|
67
10
|
|
|
68
11
|
async function promptScope({ catalog, stdinIsTTY = true, mkAsk = createStdinAsk } = {}) {
|
|
69
12
|
const c = (catalog && catalog.cli) || {};
|
|
@@ -88,75 +31,14 @@ async function promptScope({ catalog, stdinIsTTY = true, mkAsk = createStdinAsk
|
|
|
88
31
|
}
|
|
89
32
|
}
|
|
90
33
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
chooseScope = promptScope,
|
|
100
|
-
isTTY = !!process.stdin.isTTY && !!process.stdout.isTTY,
|
|
101
|
-
}) {
|
|
102
|
-
const cwdToplevel = currentRepoToplevel(cwd, repoIdentityFn);
|
|
103
|
-
const cwdRepoTop = cwdRepoToplevel(cwd, repoIdentityFn);
|
|
104
|
-
const note = (s) => {
|
|
105
|
-
if (!opts.json) process.stdout.write(`\n ${s}\n`);
|
|
106
|
-
};
|
|
107
|
-
|
|
108
|
-
const detect = () => {
|
|
109
|
-
try {
|
|
110
|
-
return detectReposFn(process.env);
|
|
111
|
-
} catch {
|
|
112
|
-
return { repos: [], unassignedSessionCount: 0 };
|
|
113
|
-
}
|
|
114
|
-
};
|
|
115
|
-
|
|
116
|
-
// 1. Explicit repo list always wins.
|
|
117
|
-
if (opts.repos) {
|
|
118
|
-
const { repos, unassignedSessionCount } = detect();
|
|
119
|
-
note(catalog.cli.reposDetectedNote(repos.length, unassignedSessionCount));
|
|
120
|
-
const { indices, unmatched } = matchReposByFlag(repos, opts.repos);
|
|
121
|
-
if (unmatched.length > 0) {
|
|
122
|
-
process.stderr.write(` ${catalog.cli.reposFlagUnmatched(unmatched.join(', '))}\n`);
|
|
123
|
-
}
|
|
124
|
-
if (indices.length > 0) {
|
|
125
|
-
const { selectedCwds, selectedToplevels } = scopeFromSelection(repos, indices);
|
|
126
|
-
return { selectedCwds, toplevels: [...new Set(selectedToplevels)], mode: 'flag' };
|
|
127
|
-
}
|
|
128
|
-
return cwdOnlyScope(repos, cwdToplevel);
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
const wantsMachine = opts.allRepos || opts.machine || opts.scope === SCOPE_MACHINE;
|
|
132
|
-
const wantsRepo = opts.repo || opts.scope === SCOPE_REPO;
|
|
133
|
-
|
|
134
|
-
let choice = null;
|
|
135
|
-
if (wantsMachine) choice = SCOPE_MACHINE;
|
|
136
|
-
else if (wantsRepo) choice = SCOPE_REPO;
|
|
137
|
-
else if (isTTY && !injectedAsk && !opts.json) {
|
|
138
|
-
choice = await chooseScope({ catalog, stdinIsTTY: true });
|
|
139
|
-
}
|
|
140
|
-
if (choice === null) choice = SCOPE_REPO;
|
|
141
|
-
|
|
142
|
-
if (choice === SCOPE_MACHINE) {
|
|
143
|
-
const { repos, unassignedSessionCount } = detect();
|
|
144
|
-
note(catalog.cli.reposDetectedNote(repos.length, unassignedSessionCount));
|
|
145
|
-
const mw = machineWideToplevels(cwdRepoTop, repos);
|
|
146
|
-
if (mw.capped) {
|
|
147
|
-
process.stderr.write(`\n ${catalog.cli.reposScopeCapped(mw.toplevels.length, mw.total)}\n`);
|
|
148
|
-
}
|
|
149
|
-
note(catalog.cli.reposScopeAll);
|
|
150
|
-
return {
|
|
151
|
-
selectedCwds: null,
|
|
152
|
-
toplevels: mw.toplevels.length ? mw.toplevels : [cwdToplevel],
|
|
153
|
-
mode: 'all',
|
|
154
|
-
};
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
// Current repo.
|
|
158
|
-
const { repos } = detect();
|
|
159
|
-
return cwdOnlyScope(repos, cwdToplevel);
|
|
34
|
+
function resolveRepoScope(args) {
|
|
35
|
+
return core.resolveRepoScope({
|
|
36
|
+
...args,
|
|
37
|
+
chooseScope: args.chooseScope || promptScope,
|
|
38
|
+
note: (s) => {
|
|
39
|
+
if (!args.opts.json) process.stdout.write(`\n ${s}\n`);
|
|
40
|
+
},
|
|
41
|
+
});
|
|
160
42
|
}
|
|
161
43
|
|
|
162
44
|
module.exports = { resolveRepoScope, machineWideToplevels, promptScope };
|
package/src/usage-run-steps.js
CHANGED
|
@@ -1,60 +1,29 @@
|
|
|
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,
|
|
17
11
|
} = require('./share');
|
|
18
12
|
const { runConsentPrompt } = require('./consent-flow');
|
|
19
13
|
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');
|
|
14
|
+
const { getAiProfileEndpoint } = require('./config');
|
|
41
15
|
// "My work with AI" preview, read from the hub ai-profile (same as the front).
|
|
42
16
|
const { resolveAiProfilePreview } = require('./ai-profile-preview');
|
|
43
17
|
const { renderAiProfileTerminal } = require('./render-ai-fluency');
|
|
44
18
|
const { ensureFreshSession } = require('./session-refresh');
|
|
45
19
|
const { loadAuthSession, sessionStatus } = require('./auth-session-store');
|
|
46
20
|
const { c: termColors } = require('./terminal-format');
|
|
47
|
-
const {
|
|
21
|
+
const { withSpinner } = require('./terminal-progress');
|
|
48
22
|
const { computeConsentSkip } = require('./consent-skip');
|
|
49
|
-
const {
|
|
50
|
-
const {
|
|
51
|
-
maybeSynthesizeAgents,
|
|
52
|
-
maybeEvaluateAgents,
|
|
53
|
-
maybePersonalizeRoadmap,
|
|
54
|
-
} = require('./usage-model-calls');
|
|
23
|
+
const { collectIdentityAndUsage, storedAiConsent, reanchorReusedGrant } = require('./usage-scan');
|
|
55
24
|
const { doBuildNextLevel } = require('./usage-oneshot-commands');
|
|
56
25
|
|
|
57
|
-
// Automatic, silent PERSISTING once consent is `granted` (
|
|
26
|
+
// Automatic, silent PERSISTING once consent is `granted` (consent controls persistence only, never what's shown).
|
|
58
27
|
async function maybeAutoShare(report, maturity, root, catalog, { bypassThrottle = false } = {}) {
|
|
59
28
|
try {
|
|
60
29
|
const result = await autoShare(report, maturity, { root, bypassThrottle });
|
|
@@ -82,35 +51,17 @@ function sendStatusColor(reason) {
|
|
|
82
51
|
return SEND_WARN_REASONS.has(reason) ? termColors.warning : termColors.danger;
|
|
83
52
|
}
|
|
84
53
|
|
|
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
54
|
// The identity gate + local (no-network, no-LLM) agent-usage signal.
|
|
99
55
|
function prepareIdentityAndUsage({ report, catalog, quiet = false }) {
|
|
100
|
-
const
|
|
101
|
-
const gate = reportGateForIdentity();
|
|
102
|
-
|
|
56
|
+
const identity = collectIdentityAndUsage({ report });
|
|
103
57
|
// --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 };
|
|
58
|
+
if (identity.gate.showFrameworkIntro && !quiet) process.stdout.write(`\n ${catalog.cli.frameworkIntroUsage}\n`);
|
|
59
|
+
return identity;
|
|
110
60
|
}
|
|
111
61
|
|
|
112
62
|
async function resolveAiConsent({ opts, catalog, gate, injectedAsk }) {
|
|
113
|
-
// CONSENT GATE, MOVED TO BEFORE ANY AI/EGRESS CALL (
|
|
63
|
+
// CONSENT GATE, MOVED TO BEFORE ANY AI/EGRESS CALL (explicit product decision, not a bug fix).
|
|
64
|
+
if (opts.json) return storedAiConsent({ gate });
|
|
114
65
|
const state = loadConsentState();
|
|
115
66
|
const decision = getConsentDecision(state);
|
|
116
67
|
const consentSkip = computeConsentSkip({
|
|
@@ -122,143 +73,43 @@ async function resolveAiConsent({ opts, catalog, gate, injectedAsk }) {
|
|
|
122
73
|
});
|
|
123
74
|
|
|
124
75
|
let aiConsentGranted;
|
|
125
|
-
// A REUSED grant (skip
|
|
76
|
+
// A REUSED grant (skip) carries a stale consent email; a fresh prompt re-anchors itself.
|
|
126
77
|
let reusedStoredConsent = false;
|
|
127
|
-
if (
|
|
128
|
-
|
|
129
|
-
|
|
78
|
+
if (consentSkip.message) {
|
|
79
|
+
process.stdout.write(`\n ${consentSkip.message}\n`);
|
|
80
|
+
}
|
|
81
|
+
if (consentSkip.skip) {
|
|
82
|
+
// A TERMINAL decision already exists (a prior run's explicit grant or
|
|
83
|
+
// denial) — never re-asked.
|
|
84
|
+
aiConsentGranted = decision === 'granted';
|
|
130
85
|
reusedStoredConsent = true;
|
|
131
86
|
} 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`);
|
|
87
|
+
const ask = injectedAsk || createStdinAsk();
|
|
88
|
+
let result;
|
|
89
|
+
try {
|
|
90
|
+
result = await runConsentPrompt({
|
|
91
|
+
ask, catalog, loggedIn: gate.loggedIn, sessionEmail: gate.email, profile: gate.profile,
|
|
92
|
+
});
|
|
93
|
+
} finally {
|
|
94
|
+
if (!injectedAsk) ask.close();
|
|
155
95
|
}
|
|
96
|
+
aiConsentGranted = result === 'granted';
|
|
156
97
|
}
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
recordConsent('granted', gate.email, { verified: true });
|
|
98
|
+
if (!aiConsentGranted) {
|
|
99
|
+
// No early return: the local report still renders further down.
|
|
100
|
+
process.stdout.write(`\n ${termColors.warning}${catalog.consent.noReportWithoutConsent}${termColors.reset}\n\n`);
|
|
161
101
|
}
|
|
102
|
+
if (reusedStoredConsent && aiConsentGranted) reanchorReusedGrant({ gate, state });
|
|
162
103
|
return aiConsentGranted;
|
|
163
104
|
}
|
|
164
105
|
|
|
165
|
-
function runAggregation({ report, scope, consentEmail, agentRoots, root, catalog }) {
|
|
166
|
-
withStaticStatus(catalog.cli.aggregatingLabel, () => {
|
|
167
|
-
report.gitActivity = collectScopedGitActivity(scope.toplevels, consentEmail);
|
|
168
|
-
// Display-only transparency warning (never feeds scoring, see authorship-mismatch.js).
|
|
169
|
-
report.authorshipMismatch = detectAuthorshipMismatch(scope.toplevels, consentEmail, report.gitActivity);
|
|
170
|
-
const sessionSignals = collectSessionSignals(process.env, scope.selectedCwds);
|
|
171
|
-
report.sessions = sessionSignals.sessions;
|
|
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
|
-
});
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
async function runModelCalls({
|
|
203
|
-
report, maturity, aiConsentGranted, opts, gate, hasAgents, agentRoots, rootDir, lang, catalog,
|
|
204
|
-
}) {
|
|
205
|
-
// Which of the three will genuinely be ATTEMPTED.
|
|
206
|
-
const willAttemptSynthesis = aiConsentGranted && hasAgents && !!getSynthesisEndpoint();
|
|
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));
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
if (synthesis) report.agentSynthesis = synthesis;
|
|
251
|
-
if (evaluation) report.agentEvaluation = evaluation;
|
|
252
|
-
if (roadmapPersonalization) report.roadmapPersonalization = roadmapPersonalization;
|
|
253
|
-
}
|
|
254
|
-
|
|
255
106
|
async function renderAndFinalize({ report, maturity, lang, opts, root, gate, catalog, bypassThrottle = false }) {
|
|
256
107
|
if (opts.json) {
|
|
257
108
|
process.stdout.write(JSON.stringify({ report, maturity }, null, 2) + '\n');
|
|
258
109
|
return;
|
|
259
110
|
}
|
|
260
111
|
|
|
261
|
-
//
|
|
112
|
+
// Revealed by SECTIONS instead of landing in one write.
|
|
262
113
|
if (gate.sessionStatus === 'expired') {
|
|
263
114
|
process.stdout.write(`\n ${termColors.warning}${catalog.terminal.sessionExpiredNotice}${termColors.reset}\n`);
|
|
264
115
|
}
|
|
@@ -276,7 +127,7 @@ async function renderAndFinalize({ report, maturity, lang, opts, root, gate, cat
|
|
|
276
127
|
}
|
|
277
128
|
}
|
|
278
129
|
|
|
279
|
-
// "Construir el siguiente nivel ahora"
|
|
130
|
+
// "Construir el siguiente nivel ahora": optional, only when
|
|
280
131
|
// explicitly requested — never part of a normal run otherwise.
|
|
281
132
|
if (opts.buildNextLevel) {
|
|
282
133
|
doBuildNextLevel(root, maturity, opts.force, catalog);
|
|
@@ -343,11 +194,8 @@ async function maybeRenderAiProfile({ report, maturity, lang, root, save, submit
|
|
|
343
194
|
}
|
|
344
195
|
|
|
345
196
|
module.exports = {
|
|
346
|
-
performScan,
|
|
347
197
|
prepareIdentityAndUsage,
|
|
348
198
|
resolveAiConsent,
|
|
349
|
-
runAggregation,
|
|
350
|
-
runModelCalls,
|
|
351
199
|
renderAndFinalize,
|
|
352
200
|
shouldRenderAiProfile,
|
|
353
201
|
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 willAttemptRoadmapPersonalization =
|
|
128
|
+
aiConsentGranted
|
|
129
|
+
&& gate.showRoadmap
|
|
130
|
+
&& !!roadmapEntryForTier && !roadmapEntryForTier.maxTier
|
|
131
|
+
&& !!getRoadmapEndpoint();
|
|
132
|
+
|
|
133
|
+
// THE THREE MODEL CALLS RUN ONE AFTER ANOTHER, NOT IN PARALLEL.
|
|
134
|
+
const runPhase = (willAttempt, label, call) => (willAttempt ? withSpinner(label, call) : call());
|
|
135
|
+
|
|
136
|
+
// `aiConsentGranted` is `false` whenever consent for this run was declined, denied in the past, or (on `--json`) simply never granted.
|
|
137
|
+
let synthesis = null;
|
|
138
|
+
let evaluation = null;
|
|
139
|
+
let roadmapPersonalization = null;
|
|
140
|
+
if (aiConsentGranted && !opts.noAi) {
|
|
141
|
+
synthesis = await runPhase(
|
|
142
|
+
willAttemptSynthesis,
|
|
143
|
+
catalog.cli.synthesizingLabel,
|
|
144
|
+
() => maybeSynthesizeAgents(report, rootDir),
|
|
145
|
+
);
|
|
146
|
+
evaluation = hasAgents
|
|
147
|
+
? await runPhase(
|
|
148
|
+
willAttemptEvaluation,
|
|
149
|
+
catalog.cli.evaluatingAgentsLabel,
|
|
150
|
+
() => maybeEvaluateAgents(report, agentRoots, lang),
|
|
151
|
+
)
|
|
152
|
+
: null;
|
|
153
|
+
// `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.
|
|
154
|
+
roadmapPersonalization = gate.showRoadmap
|
|
155
|
+
? await runPhase(
|
|
156
|
+
willAttemptRoadmapPersonalization,
|
|
157
|
+
catalog.cli.personalizingRoadmapLabel,
|
|
158
|
+
() => maybePersonalizeRoadmap(report, maturity, lang),
|
|
159
|
+
)
|
|
160
|
+
: null;
|
|
161
|
+
} else {
|
|
162
|
+
const skipReason = aiConsentGranted ? REASON.NOT_APPLICABLE : REASON.NO_CONSENT;
|
|
163
|
+
attachModelCall(report, MODEL_CALL.SYNTHESIS, skipped(skipReason));
|
|
164
|
+
attachModelCall(report, MODEL_CALL.EVALUATION, skipped(skipReason));
|
|
165
|
+
attachModelCall(report, MODEL_CALL.ROADMAP, skipped(skipReason));
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
if (synthesis) report.agentSynthesis = synthesis;
|
|
169
|
+
if (evaluation) report.agentEvaluation = evaluation;
|
|
170
|
+
if (roadmapPersonalization) report.roadmapPersonalization = roadmapPersonalization;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// Steps 1-6 of a run: scan, scope, aggregate, classify; nothing is rendered or sent.
|
|
174
|
+
async function computeUsage({
|
|
175
|
+
opts,
|
|
176
|
+
catalog,
|
|
177
|
+
lang,
|
|
178
|
+
inRegistration = false,
|
|
179
|
+
prepareIdentity = collectIdentityAndUsage,
|
|
180
|
+
resolveConsent = storedAiConsent,
|
|
181
|
+
resolveScope = resolveRepoScope,
|
|
182
|
+
}) {
|
|
183
|
+
// 1. Local scan + classify (always computed and shown).
|
|
184
|
+
const { report, root, rootDir } = performScan({ opts, catalog });
|
|
185
|
+
|
|
186
|
+
// 2. Identity gate + local agent-usage signal. Roadmap only for anonymous runs
|
|
187
|
+
// (gate.showRoadmap = !loggedIn) AND never during register/onboarding.
|
|
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 };
|
|
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);
|
|
221
|
+
const lang = opts.lang || detectReportLang();
|
|
222
|
+
const { report, maturity } = await computeUsage({ opts, catalog: getCatalog(lang), lang });
|
|
223
|
+
return { report, maturity };
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
module.exports = {
|
|
227
|
+
performScan,
|
|
228
|
+
collectIdentityAndUsage,
|
|
229
|
+
reanchorReusedGrant,
|
|
230
|
+
storedAiConsent,
|
|
231
|
+
runAggregation,
|
|
232
|
+
runModelCalls,
|
|
233
|
+
computeUsage,
|
|
234
|
+
scanUsage,
|
|
235
|
+
};
|