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
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { isRefusedWalkRoot } = require('./scan-exclusions');
|
|
4
|
+
const {
|
|
5
|
+
detectRepos,
|
|
6
|
+
repoIdentityForCwd,
|
|
7
|
+
matchReposByFlag,
|
|
8
|
+
scopeFromSelection,
|
|
9
|
+
indexOfCurrentRepo,
|
|
10
|
+
} = require('./detect-repos');
|
|
11
|
+
|
|
12
|
+
const MAX_MACHINE_WIDE_REPOS = 5;
|
|
13
|
+
const SCOPE_MACHINE = 'machine';
|
|
14
|
+
const SCOPE_REPO = 'repo';
|
|
15
|
+
|
|
16
|
+
function currentRepoToplevel(cwd, repoIdentityFn = repoIdentityForCwd) {
|
|
17
|
+
try {
|
|
18
|
+
return repoIdentityFn(cwd).toplevel || cwd;
|
|
19
|
+
} catch {
|
|
20
|
+
return cwd;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// 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).
|
|
25
|
+
function cwdRepoToplevel(cwd, repoIdentityFn = repoIdentityForCwd) {
|
|
26
|
+
if (isRefusedWalkRoot(cwd)) return null;
|
|
27
|
+
try {
|
|
28
|
+
return repoIdentityFn(cwd).toplevel || null;
|
|
29
|
+
} catch {
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Pure: builds the machine-wide root set.
|
|
35
|
+
function machineWideToplevels(cwdRepoTop, detectedRepos, cap = MAX_MACHINE_WIDE_REPOS) {
|
|
36
|
+
const ordered = [];
|
|
37
|
+
const seen = new Set();
|
|
38
|
+
const push = (t) => {
|
|
39
|
+
if (typeof t !== 'string' || !t) return;
|
|
40
|
+
if (isRefusedWalkRoot(t)) return;
|
|
41
|
+
if (seen.has(t)) return;
|
|
42
|
+
seen.add(t);
|
|
43
|
+
ordered.push(t);
|
|
44
|
+
};
|
|
45
|
+
push(cwdRepoTop);
|
|
46
|
+
for (const r of detectedRepos || []) push(r && r.toplevel);
|
|
47
|
+
const capped = ordered.slice(0, cap);
|
|
48
|
+
return { toplevels: capped, total: ordered.length, capped: ordered.length > capped.length };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function cwdOnlyScope(repos, cwdToplevel) {
|
|
52
|
+
const idx = indexOfCurrentRepo(repos, cwdToplevel);
|
|
53
|
+
if (idx === -1) {
|
|
54
|
+
return { selectedCwds: new Set(), toplevels: [cwdToplevel], mode: 'cwd' };
|
|
55
|
+
}
|
|
56
|
+
const { selectedCwds, selectedToplevels } = scopeFromSelection(repos, [idx]);
|
|
57
|
+
return {
|
|
58
|
+
selectedCwds,
|
|
59
|
+
toplevels: selectedToplevels.length ? selectedToplevels : [cwdToplevel],
|
|
60
|
+
mode: 'cwd',
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Resolve which repos feed the AI-usage evaluation.
|
|
65
|
+
async function resolveRepoScope({
|
|
66
|
+
opts,
|
|
67
|
+
cwd,
|
|
68
|
+
injectedAsk,
|
|
69
|
+
catalog,
|
|
70
|
+
detectReposFn = detectRepos,
|
|
71
|
+
repoIdentityFn = repoIdentityForCwd,
|
|
72
|
+
chooseScope = null,
|
|
73
|
+
isTTY = !!process.stdin.isTTY && !!process.stdout.isTTY,
|
|
74
|
+
note = () => {},
|
|
75
|
+
}) {
|
|
76
|
+
const cwdToplevel = currentRepoToplevel(cwd, repoIdentityFn);
|
|
77
|
+
const cwdRepoTop = cwdRepoToplevel(cwd, repoIdentityFn);
|
|
78
|
+
const detect = () => {
|
|
79
|
+
try {
|
|
80
|
+
return detectReposFn(process.env);
|
|
81
|
+
} catch {
|
|
82
|
+
return { repos: [], unassignedSessionCount: 0 };
|
|
83
|
+
}
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
// 1. Explicit repo list always wins.
|
|
87
|
+
if (opts.repos) {
|
|
88
|
+
const { repos, unassignedSessionCount } = detect();
|
|
89
|
+
note(catalog.cli.reposDetectedNote(repos.length, unassignedSessionCount));
|
|
90
|
+
const { indices, unmatched } = matchReposByFlag(repos, opts.repos);
|
|
91
|
+
if (unmatched.length > 0) {
|
|
92
|
+
process.stderr.write(` ${catalog.cli.reposFlagUnmatched(unmatched.join(', '))}\n`);
|
|
93
|
+
}
|
|
94
|
+
if (indices.length > 0) {
|
|
95
|
+
const { selectedCwds, selectedToplevels } = scopeFromSelection(repos, indices);
|
|
96
|
+
return { selectedCwds, toplevels: [...new Set(selectedToplevels)], mode: 'flag' };
|
|
97
|
+
}
|
|
98
|
+
return cwdOnlyScope(repos, cwdToplevel);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const wantsMachine = opts.allRepos || opts.machine || opts.scope === SCOPE_MACHINE;
|
|
102
|
+
const wantsRepo = opts.repo || opts.scope === SCOPE_REPO;
|
|
103
|
+
|
|
104
|
+
let choice = null;
|
|
105
|
+
if (wantsMachine) choice = SCOPE_MACHINE;
|
|
106
|
+
else if (wantsRepo) choice = SCOPE_REPO;
|
|
107
|
+
else if (chooseScope && isTTY && !injectedAsk && !opts.json) {
|
|
108
|
+
choice = await chooseScope({ catalog, stdinIsTTY: true });
|
|
109
|
+
}
|
|
110
|
+
if (choice === null) choice = SCOPE_REPO;
|
|
111
|
+
|
|
112
|
+
if (choice === SCOPE_MACHINE) {
|
|
113
|
+
const { repos, unassignedSessionCount } = detect();
|
|
114
|
+
note(catalog.cli.reposDetectedNote(repos.length, unassignedSessionCount));
|
|
115
|
+
const mw = machineWideToplevels(cwdRepoTop, repos);
|
|
116
|
+
if (mw.capped) {
|
|
117
|
+
process.stderr.write(`\n ${catalog.cli.reposScopeCapped(mw.toplevels.length, mw.total)}\n`);
|
|
118
|
+
}
|
|
119
|
+
note(catalog.cli.reposScopeAll);
|
|
120
|
+
return {
|
|
121
|
+
selectedCwds: null,
|
|
122
|
+
toplevels: mw.toplevels.length ? mw.toplevels : [cwdToplevel],
|
|
123
|
+
mode: 'all',
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Current repo.
|
|
128
|
+
const { repos } = detect();
|
|
129
|
+
return cwdOnlyScope(repos, cwdToplevel);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
module.exports = { resolveRepoScope, machineWideToplevels, SCOPE_MACHINE, SCOPE_REPO };
|
package/src/report-gating.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
const { loadAuthSession, sessionStatus, isPlatformTalent } = require('./auth-session-store');
|
|
4
4
|
const { getProfile } = require('./config');
|
|
5
5
|
|
|
6
|
-
// THE ONE LAYER THAT DECIDES WHAT THE REPORT SHOWS ACCORDING TO IDENTITY
|
|
6
|
+
// THE ONE LAYER THAT DECIDES WHAT THE REPORT SHOWS ACCORDING TO IDENTITY.
|
|
7
7
|
|
|
8
8
|
// Pure: identity in, policy out.
|
|
9
9
|
function resolveReportGate({ loggedIn = false, status = null, email = null, userType = null, profile = 'talent' } = {}) {
|
package/src/report-store.js
CHANGED
|
@@ -14,8 +14,8 @@ const SCHEMA_VERSION = 2;
|
|
|
14
14
|
function configDir() {
|
|
15
15
|
return getConfigDir(process.env);
|
|
16
16
|
}
|
|
17
|
-
function statePath() {
|
|
18
|
-
return path.join(
|
|
17
|
+
function statePath(env = process.env) {
|
|
18
|
+
return path.join(getConfigDir(env), 'report-state.json');
|
|
19
19
|
}
|
|
20
20
|
|
|
21
21
|
// Stable, filesystem-safe per-project file name derived from the absolute path.
|
|
@@ -84,7 +84,7 @@ function getOrCreateProject(state, absRoot) {
|
|
|
84
84
|
};
|
|
85
85
|
}
|
|
86
86
|
// Backward-compat: a project persisted before the two-backend chain
|
|
87
|
-
// existed
|
|
87
|
+
// existed.
|
|
88
88
|
if (!state.projects[absRoot].backendAcceptance) {
|
|
89
89
|
state.projects[absRoot].backendAcceptance = {};
|
|
90
90
|
}
|
|
@@ -98,7 +98,7 @@ function renderProjectHtml(project, lang, gate) {
|
|
|
98
98
|
}
|
|
99
99
|
|
|
100
100
|
/* ---------- persist (state only) + materialize (render HTML) ---------- */
|
|
101
|
-
//
|
|
101
|
+
// Split: `footprint` and `certify` now PERSIST state only (no HTML file, no printed link).
|
|
102
102
|
|
|
103
103
|
function stampAndSaveState(state, absRoot) {
|
|
104
104
|
state.schemaVersion = SCHEMA_VERSION;
|
|
@@ -179,12 +179,12 @@ function materializeProjectReport({ root, lang }) {
|
|
|
179
179
|
const state = loadState();
|
|
180
180
|
const project = state.projects[absRoot];
|
|
181
181
|
if (!hasRenderableData(project)) return { hasData: false };
|
|
182
|
-
//
|
|
182
|
+
// The shareable HTML is gated by identity AT GENERATION TIME — this is the moment the `report` command produces the file, so this is where the gate is resolved.
|
|
183
183
|
const gate = reportGateForIdentity();
|
|
184
184
|
return { hasData: true, ...writeProjectHtml(project, absRoot, lang, gate) };
|
|
185
185
|
}
|
|
186
186
|
|
|
187
|
-
// `upsertFootprint` AND `upsertCertification` WERE HERE, and
|
|
187
|
+
// `upsertFootprint` AND `upsertCertification` WERE HERE, and were retired.
|
|
188
188
|
|
|
189
189
|
function certKey(item) {
|
|
190
190
|
if (item && item.skillId != null) return `id:${item.skillId}`;
|
package/src/roadmap-content.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
// Curated tier roadmap content
|
|
3
|
+
// Curated tier roadmap content.
|
|
4
4
|
|
|
5
5
|
const TIER_JUMPS_ES = {
|
|
6
6
|
T0: {
|
|
@@ -201,13 +201,13 @@ const T7_TERMINAL_ES = {
|
|
|
201
201
|
whatRemains: 'El tier ya no sube, pero el equipamiento no es el trabajo. A partir de aquí el margen está en usar lo que tienes, no en instalar más: documentar tu setup para que otros lo repliquen, podar lo que no usas, y afilar cada agente/comando con el trabajo real. La escala mide cómo tienes montado el banco; que salgan buenas piezas de él depende de ti.',
|
|
202
202
|
consolidationSteps: [
|
|
203
203
|
'Afina tus hooks y agentes con el trabajo real: recorta lo que estorba, acota los matchers y ajusta las tools/límites de cada agente a su rol.',
|
|
204
|
-
'Poda lo que no usas: revisa qué assets llevas tiempo sin tocar (el CLI expone recency/mtime de forma informativa, no como gate
|
|
204
|
+
'Poda lo que no usas: revisa qué assets llevas tiempo sin tocar (el CLI expone recency/mtime de forma informativa, no como gate) y elimina lo redundante.',
|
|
205
205
|
'Mantén vivos tus ficheros de contexto (CLAUDE.md, reglas, comandos): actualízalos cuando cambien el stack o las convenciones, o dejan de ayudar.',
|
|
206
206
|
'Empaqueta y contribuye tus skills/comandos a un repo compartido del equipo para que tu setup escale más allá de tu máquina.',
|
|
207
207
|
'Documenta tu organigrama de agentes y compártelo como referencia interna para que otros lo repliquen.',
|
|
208
208
|
'Mide el impacto real (tiempo ahorrado, tareas delegadas de principio a fin) y usa ese dato para decidir qué merece seguir en tu banco.',
|
|
209
209
|
],
|
|
210
|
-
honestyNote: 'Estar en T7 significa que tienes el banco de trabajo mejor equipado que el CLI sabe medir. No significa que seas quien mejor usa la IA: el dato es una proxy de setup, indicativa y no verificada
|
|
210
|
+
honestyNote: 'Estar en T7 significa que tienes el banco de trabajo mejor equipado que el CLI sabe medir. No significa que seas quien mejor usa la IA: el dato es una proxy de setup, indicativa y no verificada. No se lee como "calidad IA del Talent".',
|
|
211
211
|
};
|
|
212
212
|
|
|
213
213
|
const TIER_JUMPS_EN = {
|
|
@@ -409,13 +409,13 @@ const T7_TERMINAL_EN = {
|
|
|
409
409
|
whatRemains: "The tier no longer rises, but equipment isn't the work. From here the margin is in using what you have, not installing more: documenting your setup so others can replicate it, pruning what you don't use, and sharpening each agent/command with real work. The ladder measures how your bench is set up; whether good pieces come out of it is up to you.",
|
|
410
410
|
consolidationSteps: [
|
|
411
411
|
"Sharpen your hooks and agents against real work: trim what gets in the way, scope the matchers, and tune each agent's tools/limits to its role.",
|
|
412
|
-
"Prune what you don't use: review which assets you haven't touched in a while (the CLI exposes recency/mtime informatively, not as a gate
|
|
412
|
+
"Prune what you don't use: review which assets you haven't touched in a while (the CLI exposes recency/mtime informatively, not as a gate) and drop the redundant ones.",
|
|
413
413
|
'Keep your context files (CLAUDE.md, rules, commands) alive: update them when the stack or conventions change, or they stop helping.',
|
|
414
414
|
'Package and contribute your skills/commands to a shared team repo so your setup scales beyond your own machine.',
|
|
415
415
|
'Document your agent org chart and share it as an internal reference so others can replicate it.',
|
|
416
416
|
'Measure the real impact (time saved, tasks delegated end to end) and use that to decide what deserves to stay on your bench.',
|
|
417
417
|
],
|
|
418
|
-
honestyNote: 'Being at T7 means you have the best-equipped bench the CLI knows how to measure. It does not mean you\'re the one who uses AI best: the data is a proxy for setup, indicative and unverified
|
|
418
|
+
honestyNote: 'Being at T7 means you have the best-equipped bench the CLI knows how to measure. It does not mean you\'re the one who uses AI best: the data is a proxy for setup, indicative and unverified. It\'s not read as "the Talent\'s AI quality".',
|
|
419
419
|
};
|
|
420
420
|
|
|
421
421
|
const CONTENT_BY_LANG = {
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
const { requestBackend } = require('./backend-request');
|
|
4
4
|
const { reporterFor, REASON } = require('./model-call-record');
|
|
5
5
|
|
|
6
|
-
// Roadmap personalization client
|
|
6
|
+
// Roadmap personalization client.
|
|
7
7
|
|
|
8
8
|
const DEFAULT_TIMEOUT_MS = 8000;
|
|
9
9
|
|
|
@@ -38,7 +38,7 @@ function buildRoadmapPersonalizationRequest(curatedEntry, tierResult, report, lo
|
|
|
38
38
|
mistakes: curatedEntry.commonMistakes,
|
|
39
39
|
},
|
|
40
40
|
signals: buildRoadmapSignals(report, tierResult.signals),
|
|
41
|
-
//
|
|
41
|
+
// Detected report language for the personalized prose.
|
|
42
42
|
...(locale === 'es' || locale === 'en' ? { locale } : {}),
|
|
43
43
|
...(consentGranted === true ? { traceContentConsent: true } : {}),
|
|
44
44
|
};
|
|
@@ -66,7 +66,7 @@ function isValidPersonalizedRoadmap(parsed, curated) {
|
|
|
66
66
|
|
|
67
67
|
// Requests the roadmap personalization endpoint.
|
|
68
68
|
async function requestRoadmapPersonalization(requestBody, { endpoint, timeoutMs = DEFAULT_TIMEOUT_MS, onOutcome = null } = {}) {
|
|
69
|
-
//
|
|
69
|
+
// See agent-synthesis.js#requestAgentSynthesis for the contract.
|
|
70
70
|
const outcome = reporterFor(onOutcome, { timeoutMs });
|
|
71
71
|
if (!endpoint) {
|
|
72
72
|
outcome.skip(REASON.NO_ENDPOINT);
|
package/src/roadmap-prompt.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
const { tierName: getTierName } = require('./i18n');
|
|
4
4
|
|
|
5
|
-
// Deterministic, ready-to-paste "implementation prompt" (
|
|
5
|
+
// Deterministic, ready-to-paste "implementation prompt" ("next steps -> prompt").
|
|
6
6
|
|
|
7
7
|
const TEMPLATES = {
|
|
8
8
|
es: {
|
|
@@ -65,7 +65,7 @@ function buildImplementationPrompt(entry, report, maturity, lang) {
|
|
|
65
65
|
const frameworks = Array.isArray(r.technologies) ? r.technologies : [];
|
|
66
66
|
const toolNames = Array.isArray(r.tools) ? r.tools.filter((x) => x && x.detected).map((x) => x.name) : [];
|
|
67
67
|
const tierKey = (maturity && maturity.tierKey) || entry.tierKey || '';
|
|
68
|
-
//
|
|
68
|
+
// NEVER maturity.tierName directly — that field comes straight from tier-engine.js and is Spanish-only by design (domain logic, not i18n).
|
|
69
69
|
const tierName = getTierName(tierKey, lang);
|
|
70
70
|
|
|
71
71
|
const lines = [];
|
package/src/scan-exclusions.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
const path = require('path');
|
|
4
4
|
|
|
5
|
-
// Directory names never descended into when walking a project tree
|
|
5
|
+
// Directory names never descended into when walking a project tree.
|
|
6
6
|
const EXCLUDED_DIRS = new Set([
|
|
7
7
|
'node_modules', 'dist', 'build', '.git', 'vendor', 'coverage', '.next', 'out',
|
|
8
8
|
// Python
|
package/src/scanner.js
CHANGED
|
@@ -163,7 +163,7 @@ function aggregateFootprint(paths) {
|
|
|
163
163
|
return { bytes, files };
|
|
164
164
|
}
|
|
165
165
|
|
|
166
|
-
// Recency = ONLY mtime of already-detected config files/dirs
|
|
166
|
+
// Recency = ONLY mtime of already-detected config files/dirs; never reads
|
|
167
167
|
// log/history content.
|
|
168
168
|
|
|
169
169
|
function latestMtime(paths) {
|
|
@@ -317,8 +317,8 @@ const probes = {
|
|
|
317
317
|
}),
|
|
318
318
|
};
|
|
319
319
|
|
|
320
|
-
// Agent org-chart counts
|
|
321
|
-
//
|
|
320
|
+
// Agent org-chart counts: deterministic, structure+names only. PROJECT ∪ HOME
|
|
321
|
+
// since the tier engine's T6 (`agents >= 2`) needs the full picture.
|
|
322
322
|
|
|
323
323
|
function agentOrgChartCounts(root, agentsCount, mcp) {
|
|
324
324
|
const home = getHomeDir();
|
|
@@ -330,7 +330,7 @@ function agentOrgChartCounts(root, agentsCount, mcp) {
|
|
|
330
330
|
commands:
|
|
331
331
|
countFiles(path.join(root, '.claude', 'commands'), '.md') +
|
|
332
332
|
countFiles(path.join(home, '.claude', 'commands'), '.md'),
|
|
333
|
-
//
|
|
333
|
+
// The third copy of the MCP paths lived here.
|
|
334
334
|
mcpServers: (mcp && typeof mcp.total === 'number' ? mcp.total : 0),
|
|
335
335
|
hooks:
|
|
336
336
|
countJsonKeys(path.join(root, '.claude', 'settings.json'), 'hooks') +
|
|
@@ -342,7 +342,7 @@ function agentOrgChartCounts(root, agentsCount, mcp) {
|
|
|
342
342
|
|
|
343
343
|
function scan(options = {}) {
|
|
344
344
|
const root = options.root || process.cwd();
|
|
345
|
-
// MCP detection runs FIRST
|
|
345
|
+
// MCP detection runs FIRST, because the per-tool depth probes now read their `mcpServers` count from it.
|
|
346
346
|
const mcp = detectMcpServers(root);
|
|
347
347
|
const scanCtx = { mcp };
|
|
348
348
|
|
|
@@ -366,7 +366,7 @@ function scan(options = {}) {
|
|
|
366
366
|
// Config footprint: ONLY the aggregated size in bytes and file count, never paths or names.
|
|
367
367
|
footprint: null,
|
|
368
368
|
// Recency: ONLY the date derived from the most recent mtime among its
|
|
369
|
-
// already-detected config files
|
|
369
|
+
// already-detected config files. Never content, logs or history.
|
|
370
370
|
recency: { lastModified: null, daysSinceModified: null, bucket: null },
|
|
371
371
|
// Version: only if the tool was detected via a binary on PATH; runs
|
|
372
372
|
// THAT already-detected binary, with `--version` only.
|
|
@@ -397,7 +397,7 @@ function scan(options = {}) {
|
|
|
397
397
|
|
|
398
398
|
const detectedTools = tools.filter((t) => t.detected);
|
|
399
399
|
|
|
400
|
-
// Deterministic (no-LLM) agent org chart
|
|
400
|
+
// Deterministic (no-LLM) agent org chart: structure + names
|
|
401
401
|
// only (name, wired tools, model, hierarchy). Never descriptions/prompts.
|
|
402
402
|
const agents = parseAgentOrgChart(root);
|
|
403
403
|
|
|
@@ -405,7 +405,7 @@ function scan(options = {}) {
|
|
|
405
405
|
|
|
406
406
|
const rawDependencyNames = detectRawDependencyNames(root);
|
|
407
407
|
|
|
408
|
-
// Deterministic (no-LLM) memory STRUCTURE
|
|
408
|
+
// Deterministic (no-LLM) memory STRUCTURE: import count, nesting depth, sections, size — from known context files (project ∪ home).
|
|
409
409
|
const memory = analyzeMemoryStructure(root);
|
|
410
410
|
|
|
411
411
|
const automations = detectAutomations(root);
|
|
@@ -436,19 +436,19 @@ function scan(options = {}) {
|
|
|
436
436
|
categories: [...new Set(detectedTools.map((t) => t.category))],
|
|
437
437
|
},
|
|
438
438
|
tools,
|
|
439
|
-
// Agent org chart
|
|
439
|
+
// Agent org chart: structure + names only, never content.
|
|
440
440
|
agents,
|
|
441
441
|
agentCounts: agentOrgChartCounts(root, agents.length, mcp),
|
|
442
442
|
// Projects where each detected AI tool has been used (skill-code-
|
|
443
|
-
// Project technologies
|
|
443
|
+
// Project technologies: dependency manifest names only.
|
|
444
444
|
technologies,
|
|
445
|
-
// MCP servers by name/category
|
|
445
|
+
// MCP servers by name/category: never the raw config.
|
|
446
446
|
mcp,
|
|
447
|
-
// Memory structure
|
|
447
|
+
// Memory structure: imports/nesting/sections/size, never text.
|
|
448
448
|
memory,
|
|
449
|
-
// Automations
|
|
449
|
+
// Automations: script/scheduler signals, never raw content.
|
|
450
450
|
automations,
|
|
451
|
-
// Browser tools
|
|
451
|
+
// Browser tools: composition over technologies + mcp.
|
|
452
452
|
browserTools,
|
|
453
453
|
};
|
|
454
454
|
}
|
package/src/secret-prompt.js
CHANGED
package/src/session-gate.js
CHANGED
|
@@ -20,7 +20,7 @@ async function hasUsableSession(env = process.env, deps = {}) {
|
|
|
20
20
|
|
|
21
21
|
const session = load(env);
|
|
22
22
|
if (sessionStatus(session, now) === 'active') return true;
|
|
23
|
-
if (!session || !session.cookie) return false;
|
|
23
|
+
if (!session || (!session.cookie && !session.sessionToken)) return false;
|
|
24
24
|
|
|
25
25
|
let refreshed = null;
|
|
26
26
|
try { refreshed = await refresh(env, { now }); } catch { refreshed = null; }
|
package/src/session-refresh.js
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
// On-demand hub-JWT minting for an email (better-auth) session.
|
|
4
4
|
|
|
5
5
|
const REFRESH_MARGIN_MS = 120 * 1000;
|
|
6
|
+
const REVOKED_STATUSES = new Set([401, 403]);
|
|
6
7
|
|
|
7
8
|
function jwtStillFresh(accessTokenExpiresAt, now, marginMs) {
|
|
8
9
|
const exp = Date.parse(String(accessTokenExpiresAt || ''));
|
|
@@ -14,6 +15,7 @@ async function ensureFreshSession(env = process.env, deps = {}) {
|
|
|
14
15
|
const {
|
|
15
16
|
loadAuthSession = require('./auth-session-store').loadAuthSession,
|
|
16
17
|
saveAuthSession = require('./auth-session-store').saveAuthSession,
|
|
18
|
+
clearAuthSession = require('./auth-session-store').clearAuthSession,
|
|
17
19
|
getAuthTokenEndpoint = require('./config').getAuthTokenEndpoint,
|
|
18
20
|
requestAuthToken = require('./auth-client').requestAuthToken,
|
|
19
21
|
now = Date.now(),
|
|
@@ -22,13 +24,17 @@ async function ensureFreshSession(env = process.env, deps = {}) {
|
|
|
22
24
|
|
|
23
25
|
const session = loadAuthSession(env);
|
|
24
26
|
if (!session) return null;
|
|
25
|
-
if (!session.cookie) return session;
|
|
27
|
+
if (!session.cookie && !session.sessionToken) return session;
|
|
26
28
|
if (session.accessToken && jwtStillFresh(session.accessTokenExpiresAt, now, marginMs)) return session;
|
|
27
29
|
|
|
28
30
|
const endpoint = getAuthTokenEndpoint(env);
|
|
29
31
|
if (!endpoint) return session;
|
|
30
32
|
|
|
31
|
-
const tok = await requestAuthToken({ cookie: session.cookie }, { endpoint });
|
|
33
|
+
const tok = await requestAuthToken({ cookie: session.cookie, sessionToken: session.sessionToken }, { endpoint });
|
|
34
|
+
if (!tok.ok && REVOKED_STATUSES.has(tok.status)) {
|
|
35
|
+
clearAuthSession(env);
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
32
38
|
if (!tok.ok) return session;
|
|
33
39
|
|
|
34
40
|
try {
|
|
@@ -48,4 +54,28 @@ async function ensureFreshSession(env = process.env, deps = {}) {
|
|
|
48
54
|
return loadAuthSession(env);
|
|
49
55
|
}
|
|
50
56
|
|
|
51
|
-
|
|
57
|
+
function webOrigin(url) {
|
|
58
|
+
try {
|
|
59
|
+
return new URL(url).origin;
|
|
60
|
+
} catch {
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
async function endSession(env = process.env, deps = {}) {
|
|
66
|
+
const {
|
|
67
|
+
loadAuthSession = require('./auth-session-store').loadAuthSession,
|
|
68
|
+
clearAuthSession = require('./auth-session-store').clearAuthSession,
|
|
69
|
+
getSignOutEndpoint = require('./config').getSignOutEndpoint,
|
|
70
|
+
getTalentProfileUrl = require('./config').getTalentProfileUrl,
|
|
71
|
+
requestSignOut = require('./auth-client').requestSignOut,
|
|
72
|
+
} = deps;
|
|
73
|
+
const session = loadAuthSession(env);
|
|
74
|
+
if (session && (session.sessionToken || session.cookie)) {
|
|
75
|
+
await requestSignOut({ cookie: session.cookie, sessionToken: session.sessionToken }, { endpoint: getSignOutEndpoint(env), origin: webOrigin(getTalentProfileUrl(env)) });
|
|
76
|
+
}
|
|
77
|
+
clearAuthSession(env);
|
|
78
|
+
return session !== null;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
module.exports = { ensureFreshSession, endSession, jwtStillFresh, REFRESH_MARGIN_MS };
|
package/src/session-scan.js
CHANGED
|
@@ -12,7 +12,7 @@ const {
|
|
|
12
12
|
parseAiderHistory,
|
|
13
13
|
} = require('./session-normalizers');
|
|
14
14
|
|
|
15
|
-
// GLOBAL, machine-wide inventory of AI coding-tool SESSIONS
|
|
15
|
+
// GLOBAL, machine-wide inventory of AI coding-tool SESSIONS.
|
|
16
16
|
|
|
17
17
|
const MAX_FILES_PER_TOOL = 2000;
|
|
18
18
|
const MAX_BYTES_PER_FILE = 8 * 1024 * 1024;
|
package/src/shakers-wordmark.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
// Shakers hand-drawn "shakers" WORDMARK, inlined VERBATIM from the
|
|
3
|
+
// Shakers hand-drawn "shakers" WORDMARK, inlined VERBATIM from the Shakers text-logo SVG (viewBox 0 0 87 18, 7 glyph <path>s).
|
|
4
4
|
|
|
5
5
|
const WORDMARK_VIEWBOX = { w: 87, h: 18 };
|
|
6
6
|
|
package/src/share-card.js
CHANGED
|
@@ -70,7 +70,7 @@ function loadProjectFootprint(root, { load = loadState } = {}) {
|
|
|
70
70
|
score: m.score,
|
|
71
71
|
level: m.level,
|
|
72
72
|
levelKey: m.key,
|
|
73
|
-
//
|
|
73
|
+
// Setup Level for the card pill (replaces the 0-4 band). Prefer the
|
|
74
74
|
// persisted `setupLevel`; derive from the tier for older stored reports.
|
|
75
75
|
setupLevelKey: (m.setupLevel && m.setupLevel.key)
|
|
76
76
|
|| (typeof m.tier === 'number' ? setupLevelForTier(m.tier).key : 'none'),
|
|
@@ -95,7 +95,7 @@ function buildCardModel(fp) {
|
|
|
95
95
|
const en = getCatalog('en');
|
|
96
96
|
const tierKey = fp.tierKey;
|
|
97
97
|
const tierName = (en.tierNames && en.tierNames[tierKey]) || tierKey;
|
|
98
|
-
//
|
|
98
|
+
// Setup Level label for the pill (English-fixed like the tier name).
|
|
99
99
|
const setupKey = fp.setupLevelKey || 'none';
|
|
100
100
|
const bandName = (en.setupLevels && en.setupLevels[setupKey] && en.setupLevels[setupKey].label) || '';
|
|
101
101
|
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
// Certification-persistence payload
|
|
3
|
+
// Certification-persistence payload, extracted from src/share.js.
|
|
4
4
|
|
|
5
5
|
const { scrubSecrets } = require('./agent-synthesis');
|
|
6
6
|
|
|
7
7
|
const CERTIFY_SEND_THROTTLE_MS = 60 * 60 * 1000;
|
|
8
8
|
const CERT_SCHEMA_VERSION = 1;
|
|
9
9
|
|
|
10
|
-
// Strict whitelist: the analyzed RESULT + observability provenance
|
|
10
|
+
// Strict whitelist: the analyzed RESULT + observability provenance, rebuilt field-by-field (never spread).
|
|
11
11
|
const CERT_DIMENSION_KEYS = ['idiomatic', 'correctness', 'depth', 'structure', 'testing'];
|
|
12
12
|
function deriveDimensionScores(dimensions) {
|
|
13
13
|
if (!dimensions || typeof dimensions !== 'object') return null;
|
|
@@ -74,13 +74,13 @@ function deriveCertificationPayload(items, { repository = null, commitRange = nu
|
|
|
74
74
|
note: typeof f.note === 'string' && f.note ? scrubSecrets(f.note) : null,
|
|
75
75
|
}))
|
|
76
76
|
: [],
|
|
77
|
-
//
|
|
77
|
+
// Rubric dimensions (0-4 or null) — the inputs the server used to
|
|
78
78
|
// compute the deterministic score. Rebuilt key-by-key, never spread.
|
|
79
79
|
dimensionScores: deriveDimensionScores(i.result.dimensions),
|
|
80
80
|
};
|
|
81
81
|
const certifyToken = certifyEvidenceTokenOf(i);
|
|
82
82
|
if (certifyToken) assessment.evidenceToken = certifyToken;
|
|
83
|
-
//
|
|
83
|
+
// The attested combined level, bound ONLY when an
|
|
84
84
|
// interview ran. Added conditionally so an item without one is byte-identical.
|
|
85
85
|
const token = interviewEvidenceTokenOf(i);
|
|
86
86
|
if (token) assessment.interviewEvidenceToken = token;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
// Consent state + email identity + client-side throttle, extracted from src/share.js
|
|
3
|
+
// Consent state + email identity + client-side throttle, extracted from src/share.js.
|
|
4
4
|
|
|
5
5
|
const fs = require('fs');
|
|
6
6
|
const path = require('path');
|
|
@@ -10,8 +10,8 @@ function configDir() {
|
|
|
10
10
|
return getConfigDir(process.env);
|
|
11
11
|
}
|
|
12
12
|
|
|
13
|
-
function consentPath() {
|
|
14
|
-
return path.join(
|
|
13
|
+
function consentPath(env = process.env) {
|
|
14
|
+
return path.join(getConfigDir(env), 'consent.json');
|
|
15
15
|
}
|
|
16
16
|
|
|
17
17
|
// Client-side throttle: don't retry a submission if the last one was less than 1h ago.
|
|
@@ -70,7 +70,7 @@ function recordConsent(decision, email = null, { verified } = {}) {
|
|
|
70
70
|
state.consent = decision;
|
|
71
71
|
if (email !== null) state.email = normalizeEmail(email);
|
|
72
72
|
if (state.lastSentAt === undefined) state.lastSentAt = null;
|
|
73
|
-
// Email-ownership verification flag
|
|
73
|
+
// Email-ownership verification flag.
|
|
74
74
|
if (decision === 'granted' && verified !== undefined) {
|
|
75
75
|
state.emailVerified = verified === true;
|
|
76
76
|
}
|
|
@@ -78,7 +78,7 @@ function recordConsent(decision, email = null, { verified } = {}) {
|
|
|
78
78
|
return state;
|
|
79
79
|
}
|
|
80
80
|
|
|
81
|
-
// consent management (
|
|
81
|
+
// consent management (status / revoke / change email). One-shot actions (do not scan), same pattern the retired `--enroll` used.
|
|
82
82
|
|
|
83
83
|
// Read-only snapshot for `--consent-status`. Never throws.
|
|
84
84
|
function getConsentStatus() {
|
|
@@ -100,7 +100,7 @@ function revokeConsent() {
|
|
|
100
100
|
function resetConsent() {
|
|
101
101
|
const state = loadConsentState() || {};
|
|
102
102
|
delete state.consent;
|
|
103
|
-
// A re-grant is "concede de cero"
|
|
103
|
+
// A re-grant is "concede de cero": drop the verification flag so the OTP runs again from scratch and nothing is sent under the old email until it's re-verified.
|
|
104
104
|
delete state.emailVerified;
|
|
105
105
|
if (state.lastSentAt === undefined) state.lastSentAt = null;
|
|
106
106
|
saveConsentState(state);
|
|
@@ -121,7 +121,7 @@ function setEmail(newEmail) {
|
|
|
121
121
|
}
|
|
122
122
|
const state = loadConsentState() || {};
|
|
123
123
|
state.email = normalizeEmail(newEmail);
|
|
124
|
-
// A new email is unverified until proven
|
|
124
|
+
// A new email is unverified until proven: reset the flag so
|
|
125
125
|
// autoShare won't send under an address whose ownership wasn't checked.
|
|
126
126
|
state.emailVerified = false;
|
|
127
127
|
saveConsentState(state);
|
package/src/share-payload.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
// The derived-payload whitelist for the `usage/reports` ingest, extracted from src/share.js
|
|
3
|
+
// The derived-payload whitelist for the `usage/reports` ingest, extracted from src/share.js.
|
|
4
4
|
|
|
5
5
|
const { scrubSecrets } = require('./agent-synthesis');
|
|
6
6
|
const { MAX_DEFINITION_CHARS } = require('./agent-evaluation');
|
|
@@ -13,7 +13,7 @@ function whitelistSchedulerProbe(probe) {
|
|
|
13
13
|
};
|
|
14
14
|
}
|
|
15
15
|
|
|
16
|
-
// WHY ONE MORE FIELD IS ADMISSIBLE ON THE AGENT WHITELIST
|
|
16
|
+
// WHY ONE MORE FIELD IS ADMISSIBLE ON THE AGENT WHITELIST.
|
|
17
17
|
function agentCatalogIdsByName(report) {
|
|
18
18
|
const evaluations =
|
|
19
19
|
report.agentEvaluation && Array.isArray(report.agentEvaluation.evaluations)
|
|
@@ -86,7 +86,7 @@ function derivePayload(report, maturity) {
|
|
|
86
86
|
tools: Array.isArray(a.tools) ? a.tools : [],
|
|
87
87
|
model: a.model || null,
|
|
88
88
|
parent: a.parent || null,
|
|
89
|
-
//
|
|
89
|
+
// The ONLY classification field that travels.
|
|
90
90
|
catalogId: catalogIdsByName.get(a.name) || null,
|
|
91
91
|
// agent-classifier fix — the agent's own definition (frontmatter description + body), scrubbed + capped.
|
|
92
92
|
...(definitionsByName.get(a.name)
|
|
@@ -132,14 +132,14 @@ function derivePayload(report, maturity) {
|
|
|
132
132
|
mcp: Array.isArray(browserToolsVia.mcp) && browserToolsVia.mcp.length > 0,
|
|
133
133
|
},
|
|
134
134
|
},
|
|
135
|
-
//
|
|
135
|
+
// Two DETERMINISTIC behavioural blocks (no LLM).
|
|
136
136
|
gitActivity: whitelistGitActivity(report.gitActivity),
|
|
137
137
|
sessions: whitelistSessions(report.sessions),
|
|
138
|
-
//
|
|
138
|
+
// One DETERMINISTIC block (workStreams) and two REDACTED-excerpt blocks (steering, decisions).
|
|
139
139
|
workStreams: whitelistWorkStreams(report.workStreams),
|
|
140
140
|
steering: whitelistSteering(report.steering),
|
|
141
141
|
decisions: whitelistDecisions(report.decisions),
|
|
142
|
-
//
|
|
142
|
+
// PER-CANDIDATE authorship + depth (per detected skill, per detected agent).
|
|
143
143
|
detectionEvidence: whitelistDetectionEvidence(report.detectionEvidence),
|
|
144
144
|
};
|
|
145
145
|
}
|
|
@@ -176,7 +176,7 @@ function whitelistDetectionEvidence(evidence) {
|
|
|
176
176
|
return { skills, agents, provenance: whitelistProvenance(evidence.provenance) };
|
|
177
177
|
}
|
|
178
178
|
|
|
179
|
-
// Re-derives the repo-level anti-noise `provenance
|
|
179
|
+
// Re-derives the repo-level anti-noise `provenance`: the (weak) fork signal and the (conservative) boilerplate/tutorial signal.
|
|
180
180
|
function whitelistProvenance(provenance) {
|
|
181
181
|
if (!provenance || typeof provenance !== 'object') return null;
|
|
182
182
|
const marker =
|