shakers-cli-beta 0.8.0-beta.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/LICENSE +59 -0
- package/README.md +1175 -0
- package/bin/add-agent.js +88 -0
- package/bin/add-project.js +88 -0
- package/bin/add-role.js +61 -0
- package/bin/add-skill.js +88 -0
- package/bin/ai-usage.js +81 -0
- package/bin/alma.js +99 -0
- package/bin/applications.js +69 -0
- package/bin/availability.js +70 -0
- package/bin/certifications.js +69 -0
- package/bin/certify.js +73 -0
- package/bin/change-role.js +61 -0
- package/bin/config.js +120 -0
- package/bin/experiences.js +69 -0
- package/bin/find-projects.js +89 -0
- package/bin/invitations.js +69 -0
- package/bin/lang.js +70 -0
- package/bin/login.js +283 -0
- package/bin/mcp.js +98 -0
- package/bin/me.js +69 -0
- package/bin/onboarding.js +134 -0
- package/bin/portfolios.js +69 -0
- package/bin/rate.js +70 -0
- package/bin/register.js +325 -0
- package/bin/report-html.js +134 -0
- package/bin/save.js +77 -0
- package/bin/shakers.js +344 -0
- package/bin/share.js +72 -0
- package/bin/show-project.js +72 -0
- package/bin/socials.js +70 -0
- package/bin/superadmin.js +351 -0
- package/package.json +40 -0
- package/src/agent-category.js +13 -0
- package/src/agent-evaluation.js +177 -0
- package/src/agent-org-chart.js +355 -0
- package/src/agent-synthesis.js +132 -0
- package/src/agent-usage.js +118 -0
- package/src/agents-client.js +165 -0
- package/src/ai-fluency-cell.js +72 -0
- package/src/ai-profile-client.js +45 -0
- package/src/ai-profile-preview.js +154 -0
- package/src/ai.js +66 -0
- package/src/alma-client.js +123 -0
- package/src/alma-flow.js +32 -0
- package/src/alma-repo-context.js +47 -0
- package/src/alma-shell.js +161 -0
- package/src/ansi.js +62 -0
- package/src/applications-client.js +79 -0
- package/src/applications-flow.js +58 -0
- package/src/auth-client.js +144 -0
- package/src/auth-device-client.js +118 -0
- package/src/auth-method-picker.js +44 -0
- package/src/auth-methods.js +35 -0
- package/src/auth-session-store.js +214 -0
- package/src/authorship.js +203 -0
- package/src/automations-detector.js +170 -0
- package/src/availability-client.js +92 -0
- package/src/availability-flow.js +105 -0
- package/src/backend-request.js +170 -0
- package/src/blind-approval-extract.js +48 -0
- package/src/brand-ansi.js +14 -0
- package/src/brand-svg.js +9 -0
- package/src/browser-tools-detector.js +21 -0
- package/src/build-next-level.js +56 -0
- package/src/certifications-client.js +89 -0
- package/src/certifications-flow.js +75 -0
- package/src/certify-dimension-client.js +217 -0
- package/src/certify-dimension-flow.js +193 -0
- package/src/certify-disclaimer.js +51 -0
- package/src/certify-remediation-prompt.js +23 -0
- package/src/cli-args.js +83 -0
- package/src/config-dir.js +120 -0
- package/src/config.js +778 -0
- package/src/consent-flow.js +115 -0
- package/src/consent-skip.js +27 -0
- package/src/decisions-extract.js +43 -0
- package/src/detect-repos.js +145 -0
- package/src/detected-tools.js +15 -0
- package/src/detection-evidence.js +105 -0
- package/src/detection-noise.js +132 -0
- package/src/detectors.js +221 -0
- package/src/device-auth-store.js +100 -0
- package/src/device-login.js +118 -0
- package/src/email-verification.js +152 -0
- package/src/env-paths.js +16 -0
- package/src/find-projects-cache.js +89 -0
- package/src/find-projects-client.js +138 -0
- package/src/find-projects-flow.js +164 -0
- package/src/git-activity.js +123 -0
- package/src/graph-certs.js +58 -0
- package/src/graph-generator.js +230 -0
- package/src/graph-scan.js +239 -0
- package/src/i18n.js +3264 -0
- package/src/inter-fonts.js +6 -0
- package/src/interactive-select.js +183 -0
- package/src/interview-client.js +226 -0
- package/src/interview-transcript.js +56 -0
- package/src/inventory-client.js +90 -0
- package/src/invitations-client.js +66 -0
- package/src/invitations-flow.js +50 -0
- package/src/lang-client.js +147 -0
- package/src/lang-codes.js +14 -0
- package/src/lang-flow.js +85 -0
- package/src/legal-notice.js +53 -0
- package/src/locale.js +51 -0
- package/src/markdown-ansi.js +50 -0
- package/src/maturity.js +111 -0
- package/src/mcp-ai-usage-result-tool.js +109 -0
- package/src/mcp-ai-usage-tool.js +333 -0
- package/src/mcp-alma-tools.js +91 -0
- package/src/mcp-applications-tools.js +42 -0
- package/src/mcp-ask-tools.js +50 -0
- package/src/mcp-auth-form.js +282 -0
- package/src/mcp-availability-tools.js +43 -0
- package/src/mcp-certifications-tools.js +42 -0
- package/src/mcp-certify-dimension-tools.js +58 -0
- package/src/mcp-detector.js +223 -0
- package/src/mcp-device-login-tools.js +173 -0
- package/src/mcp-find-projects-tools.js +67 -0
- package/src/mcp-install.js +245 -0
- package/src/mcp-invitations-tools.js +42 -0
- package/src/mcp-lang-tools.js +39 -0
- package/src/mcp-onboarding-tools.js +388 -0
- package/src/mcp-portfolios-tools.js +46 -0
- package/src/mcp-profile-tools.js +46 -0
- package/src/mcp-rate-tools.js +42 -0
- package/src/mcp-register-tools.js +517 -0
- package/src/mcp-report-tools.js +101 -0
- package/src/mcp-role-tools.js +112 -0
- package/src/mcp-saved-positions-tools.js +55 -0
- package/src/mcp-server.js +170 -0
- package/src/mcp-show-project-tools.js +50 -0
- package/src/mcp-socials-tools.js +39 -0
- package/src/memory-structure-detector.js +108 -0
- package/src/model-call-record.js +198 -0
- package/src/onboarding-availability.js +26 -0
- package/src/onboarding-client.js +364 -0
- package/src/onboarding-flow.js +1078 -0
- package/src/onboarding-livekit-client.js +102 -0
- package/src/onboarding-status-client.js +42 -0
- package/src/open-file.js +32 -0
- package/src/osc-link.js +16 -0
- package/src/portfolio-client.js +164 -0
- package/src/portfolio-git-info.js +61 -0
- package/src/portfolios-client.js +85 -0
- package/src/portfolios-flow.js +79 -0
- package/src/profile-client.js +125 -0
- package/src/profile-flow.js +115 -0
- package/src/profile-switch.js +46 -0
- package/src/prompt-select.js +29 -0
- package/src/rate-client.js +82 -0
- package/src/rate-flow.js +137 -0
- package/src/render-ai-fluency.js +266 -0
- package/src/render-certification.js +177 -0
- package/src/render-html.js +140 -0
- package/src/render-report-text.js +27 -0
- package/src/render-sheet.js +261 -0
- package/src/render-terminal.js +109 -0
- package/src/render-usage-report-text.js +155 -0
- package/src/repo-context.js +215 -0
- package/src/report-gating.js +49 -0
- package/src/report-store.js +226 -0
- package/src/report-theme.js +177 -0
- package/src/report-view-client.js +50 -0
- package/src/reveal-output.js +29 -0
- package/src/roadmap-content.js +453 -0
- package/src/roadmap-personalization.js +132 -0
- package/src/roadmap-prompt.js +154 -0
- package/src/roles-client.js +114 -0
- package/src/roles-flow.js +192 -0
- package/src/sanitize-network-text.js +36 -0
- package/src/saved-positions-client.js +71 -0
- package/src/saved-positions-flow.js +50 -0
- package/src/scan-exclusions.js +50 -0
- package/src/scanner.js +612 -0
- package/src/scope-signals.js +148 -0
- package/src/secret-prompt.js +72 -0
- package/src/session-gate.js +30 -0
- package/src/session-refresh.js +51 -0
- package/src/session-scan.js +336 -0
- package/src/session-signals.js +36 -0
- package/src/shakers-wordmark.js +17 -0
- package/src/share-card.js +362 -0
- package/src/share-certification.js +111 -0
- package/src/share-consent-state.js +157 -0
- package/src/share-payload.js +300 -0
- package/src/share.js +184 -0
- package/src/show-project-client.js +58 -0
- package/src/show-project-flow.js +119 -0
- package/src/signup-client.js +91 -0
- package/src/skills-client.js +102 -0
- package/src/socials-client.js +91 -0
- package/src/socials-flow.js +83 -0
- package/src/specificity-extract.js +41 -0
- package/src/start-add-agents.js +306 -0
- package/src/start-add-portfolio.js +388 -0
- package/src/start-add-skills.js +260 -0
- package/src/stdin-ask.js +129 -0
- package/src/steering-extract.js +43 -0
- package/src/strip-internal-comments.js +45 -0
- package/src/tech-detector.js +295 -0
- package/src/templates/report-sheet.html +572 -0
- package/src/terminal-agents.js +199 -0
- package/src/terminal-format.js +85 -0
- package/src/terminal-progress.js +106 -0
- package/src/terminal-sections.js +219 -0
- package/src/tier-analysis.js +126 -0
- package/src/tier-engine.js +114 -0
- package/src/tier-projection.js +117 -0
- package/src/transcript-turns.js +49 -0
- package/src/translations-client.js +84 -0
- package/src/typewriter.js +154 -0
- package/src/usage-model-calls.js +119 -0
- package/src/usage-oneshot-commands.js +180 -0
- package/src/usage-repo-scope.js +162 -0
- package/src/usage-run-steps.js +338 -0
- package/src/work-streams.js +74 -0
package/src/scanner.js
ADDED
|
@@ -0,0 +1,612 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const os = require('os');
|
|
5
|
+
const path = require('path');
|
|
6
|
+
const crypto = require('crypto');
|
|
7
|
+
const { execFileSync } = require('child_process');
|
|
8
|
+
const { detectors } = require('./detectors');
|
|
9
|
+
const { parseAgentOrgChart } = require('./agent-org-chart');
|
|
10
|
+
const { detectTechnologies, detectRawDependencyNames } = require('./tech-detector');
|
|
11
|
+
const { detectMcpServers, buildMcpAggregate } = require('./mcp-detector');
|
|
12
|
+
const { analyzeMemoryStructure } = require('./memory-structure-detector');
|
|
13
|
+
const { detectAutomations } = require('./automations-detector');
|
|
14
|
+
const { detectBrowserTools } = require('./browser-tools-detector');
|
|
15
|
+
const { getHomeDir } = require('./env-paths');
|
|
16
|
+
|
|
17
|
+
/* ---------- existence-check utilities (existence only, never content) ---------- */
|
|
18
|
+
|
|
19
|
+
function exists(p) {
|
|
20
|
+
try {
|
|
21
|
+
fs.accessSync(p);
|
|
22
|
+
return true;
|
|
23
|
+
} catch {
|
|
24
|
+
return false;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function onPath(bin) {
|
|
29
|
+
const cmd = process.platform === 'win32' ? 'where' : 'command';
|
|
30
|
+
const args = process.platform === 'win32' ? [bin] : ['-v', bin];
|
|
31
|
+
try {
|
|
32
|
+
// command -v needs a shell; we use a portable approach
|
|
33
|
+
if (process.platform === 'win32') {
|
|
34
|
+
execFileSync('where', [bin], { stdio: 'ignore' });
|
|
35
|
+
} else {
|
|
36
|
+
execFileSync('sh', ['-c', `command -v ${bin}`], { stdio: 'ignore' });
|
|
37
|
+
}
|
|
38
|
+
return true;
|
|
39
|
+
} catch {
|
|
40
|
+
return false;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function vscodeExtInstalled(prefix) {
|
|
45
|
+
const dirs = [
|
|
46
|
+
path.join(os.homedir(), '.vscode', 'extensions'),
|
|
47
|
+
path.join(os.homedir(), '.vscode-insiders', 'extensions'),
|
|
48
|
+
path.join(os.homedir(), '.cursor', 'extensions'),
|
|
49
|
+
];
|
|
50
|
+
for (const d of dirs) {
|
|
51
|
+
if (!exists(d)) continue;
|
|
52
|
+
try {
|
|
53
|
+
const entries = fs.readdirSync(d);
|
|
54
|
+
if (entries.some((e) => e.toLowerCase().startsWith(prefix.toLowerCase()))) {
|
|
55
|
+
return true;
|
|
56
|
+
}
|
|
57
|
+
} catch {
|
|
58
|
+
/* no permissions: ignored */
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return false;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function evalSignal(sig, root) {
|
|
65
|
+
switch (sig.type) {
|
|
66
|
+
case 'projectPath':
|
|
67
|
+
return exists(path.join(root, sig.path));
|
|
68
|
+
case 'homePath':
|
|
69
|
+
return exists(path.join(os.homedir(), sig.path));
|
|
70
|
+
case 'bin':
|
|
71
|
+
return onPath(sig.name);
|
|
72
|
+
case 'vscodeExt':
|
|
73
|
+
return vscodeExtInstalled(sig.prefix);
|
|
74
|
+
default:
|
|
75
|
+
return false;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Resolves the absolute path of a projectPath/homePath signal.
|
|
80
|
+
function resolveSignalPath(sig, root) {
|
|
81
|
+
if (sig.type === 'projectPath') return path.join(root, sig.path);
|
|
82
|
+
if (sig.type === 'homePath') return path.join(os.homedir(), sig.path);
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/* ---------- DEPTH probes: return ONLY numbers ---------- */
|
|
87
|
+
|
|
88
|
+
function countDirEntries(p) {
|
|
89
|
+
try {
|
|
90
|
+
return fs.readdirSync(p, { withFileTypes: true }).filter((e) => e.isDirectory()).length;
|
|
91
|
+
} catch {
|
|
92
|
+
return 0;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function countFiles(p, ext) {
|
|
97
|
+
try {
|
|
98
|
+
return fs.readdirSync(p).filter((f) => (ext ? f.endsWith(ext) : true)).length;
|
|
99
|
+
} catch {
|
|
100
|
+
return 0;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function countJsonKeys(file, key) {
|
|
105
|
+
// Parses the JSON ONLY to count keys; no value or name is ever stored.
|
|
106
|
+
try {
|
|
107
|
+
const raw = fs.readFileSync(file, 'utf8');
|
|
108
|
+
const obj = JSON.parse(raw);
|
|
109
|
+
const target = key ? obj[key] : obj;
|
|
110
|
+
return target && typeof target === 'object' ? Object.keys(target).length : 0;
|
|
111
|
+
} catch {
|
|
112
|
+
return 0;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/* ---------- config footprint: ONLY size in bytes and file count ---------- */
|
|
117
|
+
/* No file name or path is ever stored: only numbers are aggregated. */
|
|
118
|
+
|
|
119
|
+
const FOOTPRINT_MAX_DEPTH = 4; // caps the cost if some config dir is deeply nested
|
|
120
|
+
const FOOTPRINT_MAX_FILES = 5000; // safety cap, avoids expensive scans
|
|
121
|
+
|
|
122
|
+
function pathFootprint(p, depth = 0, budget = { files: 0 }) {
|
|
123
|
+
if (budget.files >= FOOTPRINT_MAX_FILES) return { bytes: 0, files: 0 };
|
|
124
|
+
try {
|
|
125
|
+
const st = fs.lstatSync(p);
|
|
126
|
+
if (st.isSymbolicLink()) return { bytes: 0, files: 0 }; // we don't follow symlinks
|
|
127
|
+
if (st.isFile()) {
|
|
128
|
+
budget.files += 1;
|
|
129
|
+
return { bytes: st.size, files: 1 };
|
|
130
|
+
}
|
|
131
|
+
if (st.isDirectory() && depth < FOOTPRINT_MAX_DEPTH) {
|
|
132
|
+
let bytes = 0;
|
|
133
|
+
let files = 0;
|
|
134
|
+
let entries = [];
|
|
135
|
+
try {
|
|
136
|
+
entries = fs.readdirSync(p, { withFileTypes: true });
|
|
137
|
+
} catch {
|
|
138
|
+
return { bytes: 0, files: 0 };
|
|
139
|
+
}
|
|
140
|
+
for (const e of entries) {
|
|
141
|
+
if (budget.files >= FOOTPRINT_MAX_FILES) break;
|
|
142
|
+
const sub = pathFootprint(path.join(p, e.name), depth + 1, budget);
|
|
143
|
+
bytes += sub.bytes;
|
|
144
|
+
files += sub.files;
|
|
145
|
+
}
|
|
146
|
+
return { bytes, files };
|
|
147
|
+
}
|
|
148
|
+
return { bytes: 0, files: 0 };
|
|
149
|
+
} catch {
|
|
150
|
+
return { bytes: 0, files: 0 };
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function aggregateFootprint(paths) {
|
|
155
|
+
const budget = { files: 0 };
|
|
156
|
+
let bytes = 0;
|
|
157
|
+
let files = 0;
|
|
158
|
+
for (const p of paths) {
|
|
159
|
+
const sub = pathFootprint(p, 0, budget);
|
|
160
|
+
bytes += sub.bytes;
|
|
161
|
+
files += sub.files;
|
|
162
|
+
}
|
|
163
|
+
return { bytes, files };
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Recency = ONLY mtime of already-detected config files/dirs (ADR-003); never reads
|
|
167
|
+
// log/history content.
|
|
168
|
+
|
|
169
|
+
function latestMtime(paths) {
|
|
170
|
+
let max = null;
|
|
171
|
+
for (const p of paths) {
|
|
172
|
+
try {
|
|
173
|
+
const st = fs.statSync(p);
|
|
174
|
+
if (!max || st.mtime > max) max = st.mtime;
|
|
175
|
+
} catch {
|
|
176
|
+
/* ignored: doesn't exist or no permissions */
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
return max;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function recencyBucket(days) {
|
|
183
|
+
if (days === null || days === undefined) return null;
|
|
184
|
+
if (days <= 1) return 'today';
|
|
185
|
+
if (days <= 7) return 'this_week';
|
|
186
|
+
if (days <= 30) return 'this_month';
|
|
187
|
+
if (days <= 90) return 'this_quarter';
|
|
188
|
+
return 'stale';
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function computeRecency(paths) {
|
|
192
|
+
const mtime = latestMtime(paths);
|
|
193
|
+
if (!mtime) return { lastModified: null, daysSinceModified: null, bucket: null };
|
|
194
|
+
const daysSinceModified = Math.max(0, Math.floor((Date.now() - mtime.getTime()) / 86400000));
|
|
195
|
+
return {
|
|
196
|
+
lastModified: mtime.toISOString(),
|
|
197
|
+
daysSinceModified,
|
|
198
|
+
bucket: recencyBucket(daysSinceModified),
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/* ---------- version: ONLY runs the ALREADY detected binary, with --version ---------- */
|
|
203
|
+
/* Never arbitrary commands; all output is discarded except the version pattern. */
|
|
204
|
+
|
|
205
|
+
function getVersion(bin) {
|
|
206
|
+
try {
|
|
207
|
+
const out = execFileSync(bin, ['--version'], {
|
|
208
|
+
timeout: 3000,
|
|
209
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
210
|
+
}).toString('utf8');
|
|
211
|
+
const match = out.match(/\d+\.\d+(?:\.\d+)?(?:[-.\w]*)?/);
|
|
212
|
+
return match ? match[0] : null;
|
|
213
|
+
} catch {
|
|
214
|
+
return null; // binary without --version, timeout, or any failure: ignored
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/* ---------- environment metadata: OS/architecture/installed editors ---------- */
|
|
219
|
+
|
|
220
|
+
const EDITOR_CANDIDATES = [
|
|
221
|
+
{
|
|
222
|
+
id: 'vscode',
|
|
223
|
+
name: 'Visual Studio Code',
|
|
224
|
+
signals: [{ type: 'bin', name: 'code' }, { type: 'homePath', path: '.vscode' }],
|
|
225
|
+
},
|
|
226
|
+
{
|
|
227
|
+
id: 'vscode-insiders',
|
|
228
|
+
name: 'VS Code Insiders',
|
|
229
|
+
signals: [{ type: 'bin', name: 'code-insiders' }, { type: 'homePath', path: '.vscode-insiders' }],
|
|
230
|
+
},
|
|
231
|
+
{ id: 'sublime-text', name: 'Sublime Text', signals: [{ type: 'bin', name: 'subl' }] },
|
|
232
|
+
{ id: 'vim', name: 'Vim', signals: [{ type: 'bin', name: 'vim' }] },
|
|
233
|
+
{ id: 'neovim', name: 'Neovim', signals: [{ type: 'bin', name: 'nvim' }] },
|
|
234
|
+
{ id: 'emacs', name: 'Emacs', signals: [{ type: 'bin', name: 'emacs' }] },
|
|
235
|
+
{
|
|
236
|
+
// Medium confidence: JetBrains's config path varies by OS/product/version;
|
|
237
|
+
// we check each OS's umbrella folder, not a specific IDE.
|
|
238
|
+
id: 'jetbrains',
|
|
239
|
+
name: 'JetBrains IDEs',
|
|
240
|
+
signals: [
|
|
241
|
+
{ type: 'homePath', path: '.config/JetBrains' },
|
|
242
|
+
{ type: 'homePath', path: 'Library/Application Support/JetBrains' },
|
|
243
|
+
{ type: 'homePath', path: 'AppData/Roaming/JetBrains' },
|
|
244
|
+
],
|
|
245
|
+
},
|
|
246
|
+
];
|
|
247
|
+
|
|
248
|
+
function detectEditors(root) {
|
|
249
|
+
const installed = [];
|
|
250
|
+
for (const ed of EDITOR_CANDIDATES) {
|
|
251
|
+
const hit = ed.signals.some((s) => evalSignal(s, root));
|
|
252
|
+
if (hit) installed.push(ed.id);
|
|
253
|
+
}
|
|
254
|
+
return installed;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// Per-tool depth probes.
|
|
258
|
+
const mcpCount = (ctx, toolId) => (ctx && ctx.mcp && ctx.mcp.byTool && ctx.mcp.byTool[toolId]) || 0;
|
|
259
|
+
|
|
260
|
+
const probes = {
|
|
261
|
+
'claude-code': (root, ctx) => ({
|
|
262
|
+
mcpServers: mcpCount(ctx, 'claude-code'),
|
|
263
|
+
skills:
|
|
264
|
+
countDirEntries(path.join(root, '.claude', 'skills')) +
|
|
265
|
+
countDirEntries(path.join(os.homedir(), '.claude', 'skills')),
|
|
266
|
+
commands: countFiles(path.join(root, '.claude', 'commands'), '.md'),
|
|
267
|
+
instructions: exists(path.join(root, 'CLAUDE.md')) ? 1 : 0,
|
|
268
|
+
hooks: countJsonKeys(path.join(root, '.claude', 'settings.json'), 'hooks'),
|
|
269
|
+
}),
|
|
270
|
+
cursor: (root, ctx) => ({
|
|
271
|
+
rules:
|
|
272
|
+
countFiles(path.join(root, '.cursor', 'rules'), '.mdc') +
|
|
273
|
+
(exists(path.join(root, '.cursorrules')) ? 1 : 0),
|
|
274
|
+
mcpServers: mcpCount(ctx, 'cursor'),
|
|
275
|
+
}),
|
|
276
|
+
'github-copilot': (root) => ({
|
|
277
|
+
instructions: exists(path.join(root, '.github', 'copilot-instructions.md')) ? 1 : 0,
|
|
278
|
+
}),
|
|
279
|
+
windsurf: (root, ctx) => ({
|
|
280
|
+
rules:
|
|
281
|
+
(exists(path.join(root, '.windsurfrules')) ? 1 : 0) +
|
|
282
|
+
countFiles(path.join(root, '.windsurf', 'rules'), '.md'),
|
|
283
|
+
// The path (and its medium confidence) now lives in mcp-detector.js's
|
|
284
|
+
// MCP_CONFIG_LOCATIONS, with the rest.
|
|
285
|
+
mcpServers: mcpCount(ctx, 'windsurf'),
|
|
286
|
+
}),
|
|
287
|
+
aider: (root) => ({
|
|
288
|
+
config: exists(path.join(root, '.aider.conf.yml')) ? 1 : 0,
|
|
289
|
+
}),
|
|
290
|
+
continue: (root) => ({
|
|
291
|
+
config:
|
|
292
|
+
(exists(path.join(root, '.continue')) ? 1 : 0) +
|
|
293
|
+
(exists(path.join(os.homedir(), '.continue')) ? 1 : 0),
|
|
294
|
+
}),
|
|
295
|
+
'gemini-cli': (root, ctx) => ({
|
|
296
|
+
instructions: exists(path.join(root, 'GEMINI.md')) ? 1 : 0,
|
|
297
|
+
mcpServers: mcpCount(ctx, 'gemini-cli'),
|
|
298
|
+
}),
|
|
299
|
+
'codex-cli': (root) => ({
|
|
300
|
+
instructions: exists(path.join(root, 'AGENTS.md')) ? 1 : 0,
|
|
301
|
+
}),
|
|
302
|
+
trae: (root) => ({
|
|
303
|
+
// Low confidence: `.trae/rules` structure not verified (see detectors.js).
|
|
304
|
+
rules: countFiles(path.join(root, '.trae', 'rules')),
|
|
305
|
+
}),
|
|
306
|
+
};
|
|
307
|
+
|
|
308
|
+
// Agent org-chart counts (ADR-009): deterministic, structure+names only. PROJECT ∪ HOME
|
|
309
|
+
// per ADR-014, since the tier engine's T6 (`agents >= 2`) needs the full picture.
|
|
310
|
+
|
|
311
|
+
function agentOrgChartCounts(root, agentsCount, mcp) {
|
|
312
|
+
const home = getHomeDir();
|
|
313
|
+
return {
|
|
314
|
+
agents: agentsCount,
|
|
315
|
+
skills:
|
|
316
|
+
countDirEntries(path.join(root, '.claude', 'skills')) +
|
|
317
|
+
countDirEntries(path.join(home, '.claude', 'skills')),
|
|
318
|
+
commands:
|
|
319
|
+
countFiles(path.join(root, '.claude', 'commands'), '.md') +
|
|
320
|
+
countFiles(path.join(home, '.claude', 'commands'), '.md'),
|
|
321
|
+
// Issue 112: the third copy of the MCP paths lived here.
|
|
322
|
+
mcpServers: (mcp && typeof mcp.total === 'number' ? mcp.total : 0),
|
|
323
|
+
hooks:
|
|
324
|
+
countJsonKeys(path.join(root, '.claude', 'settings.json'), 'hooks') +
|
|
325
|
+
countJsonKeys(path.join(home, '.claude', 'settings.json'), 'hooks'),
|
|
326
|
+
};
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
/* ---------- main scan ---------- */
|
|
330
|
+
|
|
331
|
+
function scan(options = {}) {
|
|
332
|
+
const root = options.root || process.cwd();
|
|
333
|
+
// MCP detection runs FIRST (issue 112), because the per-tool depth probes now read their `mcpServers` count from it.
|
|
334
|
+
const mcp = detectMcpServers(root);
|
|
335
|
+
const scanCtx = { mcp };
|
|
336
|
+
|
|
337
|
+
const tools = [];
|
|
338
|
+
|
|
339
|
+
for (const det of detectors) {
|
|
340
|
+
const matched = det.signals.filter((s) => evalSignal(s, root));
|
|
341
|
+
const detected = matched.length > 0;
|
|
342
|
+
|
|
343
|
+
const tool = {
|
|
344
|
+
id: det.id,
|
|
345
|
+
name: det.name,
|
|
346
|
+
vendor: det.vendor,
|
|
347
|
+
category: det.category,
|
|
348
|
+
detected,
|
|
349
|
+
// Only the signal TYPE that matched (projectPath/homePath/bin/vscodeExt),
|
|
350
|
+
// never the concrete path, so as not to leak private folder structure.
|
|
351
|
+
signalTypes: [...new Set(matched.map((s) => s.type))],
|
|
352
|
+
signalCount: matched.length,
|
|
353
|
+
depth: {},
|
|
354
|
+
// Config footprint: ONLY the aggregated size in bytes and file count, never paths or names.
|
|
355
|
+
footprint: null,
|
|
356
|
+
// Recency: ONLY the date derived from the most recent mtime among its
|
|
357
|
+
// already-detected config files (ADR-003). Never content, logs or history.
|
|
358
|
+
recency: { lastModified: null, daysSinceModified: null, bucket: null },
|
|
359
|
+
// Version: only if the tool was detected via a binary on PATH; runs
|
|
360
|
+
// THAT already-detected binary, with `--version` only.
|
|
361
|
+
version: null,
|
|
362
|
+
};
|
|
363
|
+
|
|
364
|
+
if (detected && probes[det.id]) {
|
|
365
|
+
tool.depth = probes[det.id](root, scanCtx);
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
if (detected) {
|
|
369
|
+
const pathSignals = matched
|
|
370
|
+
.map((s) => resolveSignalPath(s, root))
|
|
371
|
+
.filter((p) => p !== null);
|
|
372
|
+
if (pathSignals.length > 0) {
|
|
373
|
+
tool.footprint = aggregateFootprint(pathSignals);
|
|
374
|
+
tool.recency = computeRecency(pathSignals);
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
const binSignal = matched.find((s) => s.type === 'bin');
|
|
378
|
+
if (binSignal) {
|
|
379
|
+
tool.version = getVersion(binSignal.name);
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
tools.push(tool);
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
const detectedTools = tools.filter((t) => t.detected);
|
|
387
|
+
|
|
388
|
+
// Deterministic (no-LLM) agent org chart (ADR-009): structure + names
|
|
389
|
+
// only (name, wired tools, model, hierarchy). Never descriptions/prompts.
|
|
390
|
+
const agents = parseAgentOrgChart(root);
|
|
391
|
+
|
|
392
|
+
const technologies = detectTechnologies(root);
|
|
393
|
+
|
|
394
|
+
const rawDependencyNames = detectRawDependencyNames(root);
|
|
395
|
+
|
|
396
|
+
// Deterministic (no-LLM) memory STRUCTURE (talents-ai-score, issue 016 / ADR-013-014): import count, nesting depth, sections, size — from known context files (project ∪ home).
|
|
397
|
+
const memory = analyzeMemoryStructure(root);
|
|
398
|
+
|
|
399
|
+
const automations = detectAutomations(root);
|
|
400
|
+
|
|
401
|
+
const browserTools = detectBrowserTools(rawDependencyNames, mcp);
|
|
402
|
+
|
|
403
|
+
// Stable per-machine anonymous id: hash of hostname + user.
|
|
404
|
+
const anonId = crypto
|
|
405
|
+
.createHash('sha256')
|
|
406
|
+
.update(`${os.hostname()}::${os.userInfo().username}`)
|
|
407
|
+
.digest('hex')
|
|
408
|
+
.slice(0, 12);
|
|
409
|
+
|
|
410
|
+
return {
|
|
411
|
+
schemaVersion: '1.1',
|
|
412
|
+
generatedAt: new Date().toISOString(),
|
|
413
|
+
anonId,
|
|
414
|
+
platform: process.platform,
|
|
415
|
+
scope: options.root ? 'custom' : 'cwd',
|
|
416
|
+
environment: {
|
|
417
|
+
platform: process.platform,
|
|
418
|
+
arch: process.arch,
|
|
419
|
+
nodeVersion: process.version,
|
|
420
|
+
editorsInstalled: detectEditors(root),
|
|
421
|
+
},
|
|
422
|
+
summary: {
|
|
423
|
+
totalDetected: detectedTools.length,
|
|
424
|
+
categories: [...new Set(detectedTools.map((t) => t.category))],
|
|
425
|
+
},
|
|
426
|
+
tools,
|
|
427
|
+
// Agent org chart (ADR-009): structure + names only, never content.
|
|
428
|
+
agents,
|
|
429
|
+
agentCounts: agentOrgChartCounts(root, agents.length, mcp),
|
|
430
|
+
// Projects where each detected AI tool has been used (skill-code-
|
|
431
|
+
// Project technologies (ADR-012): dependency manifest names only.
|
|
432
|
+
technologies,
|
|
433
|
+
// MCP servers by name/category (issue 015): never the raw config.
|
|
434
|
+
mcp,
|
|
435
|
+
// Memory structure (issue 016): imports/nesting/sections/size, never text.
|
|
436
|
+
memory,
|
|
437
|
+
// Automations (issue 017): script/scheduler signals, never raw content.
|
|
438
|
+
automations,
|
|
439
|
+
// Browser tools (issue 018): composition over technologies + mcp.
|
|
440
|
+
browserTools,
|
|
441
|
+
};
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
/* ---------- multi-repo scan (per-selected-repo scoping) ---------- */
|
|
445
|
+
|
|
446
|
+
function scanForRoots(roots, options = {}) {
|
|
447
|
+
const list = (Array.isArray(roots) ? roots : [roots]).filter(
|
|
448
|
+
(r) => typeof r === 'string' && r && exists(r),
|
|
449
|
+
);
|
|
450
|
+
if (list.length === 0) return scan(options);
|
|
451
|
+
const reports = list.map((r) => scan({ ...options, root: r }));
|
|
452
|
+
if (reports.length === 1) return reports[0]; // 1-repo default: identical to scan()
|
|
453
|
+
return mergeReports(reports);
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
function moreRecent(a, b) {
|
|
457
|
+
const ad = a && typeof a.daysSinceModified === 'number' ? a.daysSinceModified : null;
|
|
458
|
+
const bd = b && typeof b.daysSinceModified === 'number' ? b.daysSinceModified : null;
|
|
459
|
+
if (ad === null) return bd === null ? a : b;
|
|
460
|
+
if (bd === null) return a;
|
|
461
|
+
return bd < ad ? b : a;
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
function mergeTools(reports) {
|
|
465
|
+
const byId = new Map();
|
|
466
|
+
for (const rep of reports) {
|
|
467
|
+
for (const t of rep.tools || []) {
|
|
468
|
+
const ex = byId.get(t.id);
|
|
469
|
+
if (!ex) {
|
|
470
|
+
byId.set(t.id, { ...t, depth: { ...t.depth }, signalTypes: [...(t.signalTypes || [])] });
|
|
471
|
+
continue;
|
|
472
|
+
}
|
|
473
|
+
ex.detected = ex.detected || t.detected;
|
|
474
|
+
ex.signalCount = Math.max(ex.signalCount || 0, t.signalCount || 0);
|
|
475
|
+
ex.signalTypes = [...new Set([...(ex.signalTypes || []), ...(t.signalTypes || [])])];
|
|
476
|
+
const keys = new Set([...Object.keys(ex.depth || {}), ...Object.keys(t.depth || {})]);
|
|
477
|
+
const depth = {};
|
|
478
|
+
for (const k of keys) depth[k] = Math.max((ex.depth && ex.depth[k]) || 0, (t.depth && t.depth[k]) || 0);
|
|
479
|
+
ex.depth = depth;
|
|
480
|
+
if (t.footprint && (!ex.footprint || (t.footprint.bytes || 0) > (ex.footprint.bytes || 0))) {
|
|
481
|
+
ex.footprint = t.footprint;
|
|
482
|
+
}
|
|
483
|
+
ex.recency = moreRecent(ex.recency, t.recency);
|
|
484
|
+
if (!ex.version && t.version) ex.version = t.version;
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
return [...byId.values()];
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
function mergeMcp(reports) {
|
|
491
|
+
const seen = new Map();
|
|
492
|
+
const byTool = {};
|
|
493
|
+
for (const rep of reports) {
|
|
494
|
+
const mcp = rep.mcp || {};
|
|
495
|
+
for (const s of mcp.servers || []) if (!seen.has(s.name)) seen.set(s.name, s);
|
|
496
|
+
for (const [tool, n] of Object.entries(mcp.byTool || {})) byTool[tool] = Math.max(byTool[tool] || 0, n || 0);
|
|
497
|
+
}
|
|
498
|
+
return buildMcpAggregate([...seen.values()], byTool);
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
function mergeTechnologies(reports) {
|
|
502
|
+
const set = new Set();
|
|
503
|
+
for (const rep of reports) for (const t of rep.technologies || []) set.add(t);
|
|
504
|
+
return [...set].sort();
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
function mergeMemory(reports) {
|
|
508
|
+
const byId = new Map();
|
|
509
|
+
for (const rep of reports) {
|
|
510
|
+
for (const f of (rep.memory && rep.memory.files) || []) {
|
|
511
|
+
const ex = byId.get(f.id);
|
|
512
|
+
const richer =
|
|
513
|
+
!ex ||
|
|
514
|
+
(f.imports || 0) > (ex.imports || 0) ||
|
|
515
|
+
((f.imports || 0) === (ex.imports || 0) && (f.depth || 0) > (ex.depth || 0));
|
|
516
|
+
if (richer) byId.set(f.id, f);
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
const files = [...byId.values()];
|
|
520
|
+
const totalImports = files.reduce((s, f) => s + (f.imports || 0), 0);
|
|
521
|
+
const maxDepth = files.reduce((m, f) => Math.max(m, f.depth || 0), 0);
|
|
522
|
+
return { files, totalImports, maxDepth, layered: maxDepth > 2 };
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
function mergeAutomations(reports) {
|
|
526
|
+
let npm = 0;
|
|
527
|
+
let shell = 0;
|
|
528
|
+
let jsonPiping = 0;
|
|
529
|
+
for (const rep of reports) {
|
|
530
|
+
const a = rep.automations || {};
|
|
531
|
+
npm += (a.scripts && a.scripts.npm) || 0;
|
|
532
|
+
shell += (a.scripts && a.scripts.shell) || 0;
|
|
533
|
+
jsonPiping += a.jsonPiping || 0;
|
|
534
|
+
}
|
|
535
|
+
// Schedulers (cron/launchd/pm2/systemd) probe the machine, not the repo, so
|
|
536
|
+
// they are identical across roots — take the first report's block verbatim.
|
|
537
|
+
const schedulers = (reports[0].automations && reports[0].automations.schedulers) || {};
|
|
538
|
+
return { scripts: { npm, shell }, jsonPiping, schedulers };
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
function mergeBrowserTools(reports) {
|
|
542
|
+
const deps = new Set();
|
|
543
|
+
const mcpNames = new Set();
|
|
544
|
+
for (const rep of reports) {
|
|
545
|
+
const via = (rep.browserTools && rep.browserTools.via) || {};
|
|
546
|
+
for (const d of via.dependencies || []) deps.add(d);
|
|
547
|
+
for (const m of via.mcp || []) mcpNames.add(m);
|
|
548
|
+
}
|
|
549
|
+
const dependencies = [...deps];
|
|
550
|
+
const mcp = [...mcpNames];
|
|
551
|
+
return { detected: dependencies.length > 0 || mcp.length > 0, via: { dependencies, mcp }, count: dependencies.length + mcp.length };
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
function mergeAgents(reports) {
|
|
555
|
+
const seen = new Set();
|
|
556
|
+
const agents = [];
|
|
557
|
+
for (const rep of reports) {
|
|
558
|
+
for (const a of rep.agents || []) {
|
|
559
|
+
if (seen.has(a.name)) continue;
|
|
560
|
+
seen.add(a.name);
|
|
561
|
+
agents.push(a);
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
return agents;
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
function mergeAgentCounts(reports, mcpTotal, agentsCount) {
|
|
568
|
+
let skills = 0;
|
|
569
|
+
let commands = 0;
|
|
570
|
+
let hooks = 0;
|
|
571
|
+
for (const rep of reports) {
|
|
572
|
+
const c = rep.agentCounts || {};
|
|
573
|
+
skills = Math.max(skills, c.skills || 0);
|
|
574
|
+
commands = Math.max(commands, c.commands || 0);
|
|
575
|
+
hooks = Math.max(hooks, c.hooks || 0);
|
|
576
|
+
}
|
|
577
|
+
return { agents: agentsCount, skills, commands, mcpServers: mcpTotal, hooks };
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
function mergeEnvironment(reports) {
|
|
581
|
+
const base = reports[0].environment || {};
|
|
582
|
+
const editors = new Set();
|
|
583
|
+
for (const rep of reports) for (const e of (rep.environment && rep.environment.editorsInstalled) || []) editors.add(e);
|
|
584
|
+
return { ...base, editorsInstalled: [...editors] };
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
function mergeReports(reports) {
|
|
588
|
+
const tools = mergeTools(reports);
|
|
589
|
+
const mcp = mergeMcp(reports);
|
|
590
|
+
const agents = mergeAgents(reports);
|
|
591
|
+
const detectedTools = tools.filter((t) => t.detected);
|
|
592
|
+
return {
|
|
593
|
+
// Identity/machine fields (schemaVersion, generatedAt, anonId, platform,
|
|
594
|
+
// scope) are root-independent — carried from the first scan unchanged.
|
|
595
|
+
...reports[0],
|
|
596
|
+
environment: mergeEnvironment(reports),
|
|
597
|
+
summary: {
|
|
598
|
+
totalDetected: detectedTools.length,
|
|
599
|
+
categories: [...new Set(detectedTools.map((t) => t.category))],
|
|
600
|
+
},
|
|
601
|
+
tools,
|
|
602
|
+
agents,
|
|
603
|
+
agentCounts: mergeAgentCounts(reports, mcp.total, agents.length),
|
|
604
|
+
technologies: mergeTechnologies(reports),
|
|
605
|
+
mcp,
|
|
606
|
+
memory: mergeMemory(reports),
|
|
607
|
+
automations: mergeAutomations(reports),
|
|
608
|
+
browserTools: mergeBrowserTools(reports),
|
|
609
|
+
};
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
module.exports = { scan, scanForRoots };
|