getprismo 0.1.24 → 0.1.26
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 +14 -1
- package/docs/mcp.md +1 -0
- package/lib/prismo-dev/context-detect.js +342 -0
- package/lib/prismo-dev/context-optimize.js +11 -324
- package/lib/prismo-dev/doctor.js +12 -0
- package/lib/prismo-dev/mcp.js +24 -2
- package/lib/prismo-dev/report.js +24 -0
- package/lib/prismo-dev/scan-detect.js +306 -0
- package/lib/prismo-dev/scan-score.js +473 -0
- package/lib/prismo-dev/scan.js +29 -739
- package/lib/prismo-dev/usage-watch.js +23 -20
- package/lib/prismo-dev/watch-live.js +103 -0
- package/lib/prismo-dev/watch-render.js +65 -0
- package/lib/prismo-dev-scan.js +6 -2
- package/package.json +1 -1
|
@@ -10,198 +10,19 @@ module.exports = function createContextOptimize(deps) {
|
|
|
10
10
|
color,
|
|
11
11
|
writeGeneratedFile,
|
|
12
12
|
} = deps;
|
|
13
|
-
const { execFileSync } = require("child_process");
|
|
14
13
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
const deps = { ...(pkg && pkg.dependencies), ...(pkg && pkg.devDependencies) };
|
|
27
|
-
if (deps.next) frameworks.add("Next.js");
|
|
28
|
-
if (deps.react) frameworks.add("React");
|
|
29
|
-
if (deps.vue || deps["@vue/runtime-core"]) frameworks.add("Vue");
|
|
30
|
-
if (deps.svelte || deps["@sveltejs/kit"]) frameworks.add(deps["@sveltejs/kit"] ? "SvelteKit" : "Svelte");
|
|
31
|
-
if (deps["solid-js"]) frameworks.add("Solid");
|
|
32
|
-
if (deps.astro) frameworks.add("Astro");
|
|
33
|
-
if (deps.nuxt) frameworks.add("Nuxt");
|
|
34
|
-
if (deps.express) frameworks.add("Express");
|
|
35
|
-
if (deps["@nestjs/core"]) frameworks.add("NestJS");
|
|
36
|
-
if (deps.prisma || deps["@prisma/client"]) frameworks.add("Prisma");
|
|
37
|
-
if (deps.tailwindcss) frameworks.add("Tailwind");
|
|
38
|
-
if (deps.typescript) frameworks.add("TypeScript");
|
|
39
|
-
if (pkg) frameworks.add("Node.js");
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
const textFiles = new Map(result.files.map((file) => [file.path, file]));
|
|
43
|
-
const requirements = [...textFiles.keys()].filter((rel) => rel.endsWith("requirements.txt"));
|
|
44
|
-
for (const rel of requirements) {
|
|
45
|
-
const text = readIfText(path.join(root, rel)) || "";
|
|
46
|
-
if (/fastapi/i.test(text)) frameworks.add("FastAPI");
|
|
47
|
-
if (/django/i.test(text)) frameworks.add("Django");
|
|
48
|
-
if (/flask/i.test(text)) frameworks.add("Flask");
|
|
49
|
-
if (/psycopg2|asyncpg|sqlalchemy/i.test(text)) frameworks.add("PostgreSQL");
|
|
50
|
-
if (/redis/i.test(text)) frameworks.add("Redis");
|
|
51
|
-
frameworks.add("Python");
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
const pyprojectFiles = [...textFiles.keys()].filter((rel) => rel.endsWith("pyproject.toml") && !rel.includes("node_modules/"));
|
|
55
|
-
for (const rel of pyprojectFiles) {
|
|
56
|
-
const text = readIfText(path.join(root, rel)) || "";
|
|
57
|
-
if (/fastapi/i.test(text)) frameworks.add("FastAPI");
|
|
58
|
-
if (/django/i.test(text)) frameworks.add("Django");
|
|
59
|
-
if (/flask/i.test(text)) frameworks.add("Flask");
|
|
60
|
-
if (/sqlalchemy/i.test(text)) frameworks.add("SQLAlchemy");
|
|
61
|
-
if (/psycopg|asyncpg/i.test(text)) frameworks.add("PostgreSQL");
|
|
62
|
-
if (/redis/i.test(text)) frameworks.add("Redis");
|
|
63
|
-
if (/celery/i.test(text)) frameworks.add("Celery");
|
|
64
|
-
frameworks.add("Python");
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
if (pyprojectFiles.length && !frameworks.has("Python")) frameworks.add("Python");
|
|
68
|
-
const pythonFiles = [...textFiles.values()].filter((file) => file.path.endsWith(".py") && !isNonSourcePath(file.path)).slice(0, 80);
|
|
69
|
-
for (const file of pythonFiles) {
|
|
70
|
-
const text = readIfText(path.join(root, file.path), 128 * 1024) || "";
|
|
71
|
-
if (/FastAPI\s*\(|from\s+fastapi\s+import|APIRouter\s*\(/.test(text)) frameworks.add("FastAPI");
|
|
72
|
-
if (/from\s+django|import\s+django|DJANGO_SETTINGS_MODULE/.test(text)) frameworks.add("Django");
|
|
73
|
-
if (/Flask\s*\(|from\s+flask\s+import/.test(text)) frameworks.add("Flask");
|
|
74
|
-
if (/sqlalchemy|create_engine|SessionLocal/i.test(text)) frameworks.add("SQLAlchemy");
|
|
75
|
-
}
|
|
76
|
-
if (pythonFiles.length) frameworks.add("Python");
|
|
77
|
-
if ([...textFiles.keys()].some((rel) => rel.endsWith("Cargo.toml"))) frameworks.add("Rust");
|
|
78
|
-
if ([...textFiles.keys()].some((rel) => rel.endsWith("go.mod"))) frameworks.add("Go");
|
|
79
|
-
if ([...textFiles.keys()].some((rel) => rel.endsWith("docker-compose.yml") || rel.endsWith("docker-compose.yaml"))) frameworks.add("Docker");
|
|
80
|
-
if ([...textFiles.keys()].some((rel) => rel.includes("prisma/schema.prisma"))) frameworks.add("Prisma");
|
|
81
|
-
if ([...textFiles.keys()].some((rel) => rel.includes("next.config."))) frameworks.add("Next.js");
|
|
82
|
-
if ([...textFiles.keys()].some((rel) => rel.includes("vite.config."))) frameworks.add("Vite");
|
|
83
|
-
if ([...textFiles.keys()].some((rel) => rel.includes("svelte.config."))) frameworks.add("SvelteKit");
|
|
84
|
-
if ([...textFiles.keys()].some((rel) => rel.includes("astro.config."))) frameworks.add("Astro");
|
|
85
|
-
if ([...textFiles.keys()].some((rel) => rel.includes("nuxt.config."))) frameworks.add("Nuxt");
|
|
86
|
-
if ([...textFiles.keys()].some((rel) => rel.endsWith(".vue"))) frameworks.add("Vue");
|
|
87
|
-
if ([...textFiles.keys()].some((rel) => rel.endsWith(".svelte"))) frameworks.add("Svelte");
|
|
88
|
-
if ([...textFiles.keys()].some((rel) => rel.includes("tailwind.config."))) frameworks.add("Tailwind");
|
|
89
|
-
if ([...textFiles.keys()].some((rel) => rel.endsWith("tsconfig.json"))) frameworks.add("TypeScript");
|
|
90
|
-
if ([...textFiles.keys()].some((rel) => rel.includes("alembic/") || rel.endsWith("alembic.ini"))) frameworks.add("Alembic");
|
|
91
|
-
if ([...textFiles.keys()].some((rel) => /postgres|postgresql/i.test(rel))) frameworks.add("PostgreSQL");
|
|
92
|
-
return Array.from(frameworks).sort();
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
function topLevelDirectories(root) {
|
|
96
|
-
try {
|
|
97
|
-
return fs.readdirSync(root, { withFileTypes: true })
|
|
98
|
-
.filter((entry) => entry.isDirectory() && !entry.name.startsWith(".") && entry.name !== "node_modules")
|
|
99
|
-
.map((entry) => entry.name)
|
|
100
|
-
.sort();
|
|
101
|
-
} catch {
|
|
102
|
-
return [];
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
function hasPath(result, matcher) {
|
|
107
|
-
return result.files.some((file) => matcher(file.path));
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
function detectEntrypoints(result) {
|
|
111
|
-
const candidates = [
|
|
112
|
-
"backend/app/main.py",
|
|
113
|
-
"backend/main.py",
|
|
114
|
-
"app/main.py",
|
|
115
|
-
"main.py",
|
|
116
|
-
"manage.py",
|
|
117
|
-
"frontend/src/app/page.tsx",
|
|
118
|
-
"frontend/src/app/layout.tsx",
|
|
119
|
-
"src/app/page.tsx",
|
|
120
|
-
"src/main.tsx",
|
|
121
|
-
"src/index.tsx",
|
|
122
|
-
"src/App.vue",
|
|
123
|
-
"src/App.svelte",
|
|
124
|
-
"src/routes/+page.svelte",
|
|
125
|
-
"src/pages/index.astro",
|
|
126
|
-
"app.vue",
|
|
127
|
-
"server.js",
|
|
128
|
-
"index.js",
|
|
129
|
-
"docker/docker-compose.yml",
|
|
130
|
-
"docker-compose.yml",
|
|
131
|
-
];
|
|
132
|
-
const paths = new Set(result.files.map((file) => file.path));
|
|
133
|
-
const found = candidates.filter((candidate) => paths.has(candidate));
|
|
134
|
-
if (!found.length) {
|
|
135
|
-
const pyMain = result.files.find((f) => /^[^/]+\/__main__\.py$/.test(f.path));
|
|
136
|
-
if (pyMain) found.push(pyMain.path);
|
|
137
|
-
const pyInit = result.files.find((f) => /^[^/]+\/__init__\.py$/.test(f.path) && !f.path.includes("test"));
|
|
138
|
-
if (pyInit && !found.length) found.push(pyInit.path);
|
|
139
|
-
}
|
|
140
|
-
return found;
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
function isNonSourcePath(rel) {
|
|
144
|
-
if (/\.(test|spec|e2e)\.[jt]sx?$/.test(rel)) return true;
|
|
145
|
-
return /^(docs|docs_src|examples|samples|tutorials|tests|test|spec|__tests__|fixtures)\//i.test(rel) ||
|
|
146
|
-
/\/(docs|docs_src|examples|samples|tutorials|tests|test|__tests__|fixtures)\//.test(rel);
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
function detectBackendPaths(result) {
|
|
150
|
-
const pythonText = (file) => readIfText(path.join(result.root, file.path), 256 * 1024) || "";
|
|
151
|
-
const isRootPython = (rel) => /^[^/]+\.py$/.test(rel);
|
|
152
|
-
const isPython = (rel) => rel.endsWith(".py");
|
|
153
|
-
const hasFastApiSignal = (file) => /FastAPI\s*\(|APIRouter\s*\(|@app\.(get|post|put|patch|delete)|@router\.(get|post|put|patch|delete)/.test(pythonText(file));
|
|
154
|
-
const api = findRepoFiles(result, (rel, file) => !isNonSourcePath(rel) && (
|
|
155
|
-
/(backend|app|src)\/.*(router|routes|api)\//.test(rel) ||
|
|
156
|
-
/(backend|app|src)\/.*(router|routes).*\.py$/.test(rel) ||
|
|
157
|
-
/\/(routing|routers?)\.py$/.test(rel) ||
|
|
158
|
-
(isRootPython(rel) && hasFastApiSignal(file))
|
|
159
|
-
), 30).map((f) => f.path);
|
|
160
|
-
const services = findRepoFiles(result, (rel) => !isNonSourcePath(rel) && (
|
|
161
|
-
/(backend|app|src)\/.*(service|services|application)/.test(rel) ||
|
|
162
|
-
/\/applications?\.py$/.test(rel) ||
|
|
163
|
-
(isRootPython(rel) && /(service|worker|manager|client|heartbeat|memory|chat|tool|orchestrator|pipeline)/i.test(rel))
|
|
164
|
-
), 40).map((f) => f.path);
|
|
165
|
-
const models = findRepoFiles(result, (rel) => !isNonSourcePath(rel) && (
|
|
166
|
-
/(backend|app|src)\/.*models\.py$/.test(rel) ||
|
|
167
|
-
/(backend|app|src)\/.*schema/.test(rel) ||
|
|
168
|
-
/\/models\.py$/.test(rel) ||
|
|
169
|
-
(isRootPython(rel) && /(model|schema|entity|types?)/i.test(rel))
|
|
170
|
-
), 30).map((f) => f.path);
|
|
171
|
-
const db = findRepoFiles(result, (rel) => !isNonSourcePath(rel) && (
|
|
172
|
-
/(backend|app|src)\/.*\/(db|database|alembic|migrations)[/.]/.test(rel) ||
|
|
173
|
-
(isPython(rel) && /(sqlite|qdrant|neo4j|database|storage|repository|vector|graph|migration)/i.test(rel))
|
|
174
|
-
), 30).map((f) => f.path);
|
|
175
|
-
const config = findRepoFiles(result, (rel) => !isNonSourcePath(rel) && (
|
|
176
|
-
/(backend|app|src)\/.*(config|settings|env).*\.py$/.test(rel) ||
|
|
177
|
-
(isRootPython(rel) && /(config|settings|env)/i.test(rel)) ||
|
|
178
|
-
rel.endsWith("requirements.txt") ||
|
|
179
|
-
rel === "pyproject.toml"
|
|
180
|
-
), 20).map((f) => f.path);
|
|
181
|
-
const auth = findRepoFiles(result, (rel, file) => !isNonSourcePath(rel) && (
|
|
182
|
-
/(backend|app|src)\/.*auth/.test(rel) ||
|
|
183
|
-
/\/security\.py$/.test(rel) ||
|
|
184
|
-
(isPython(rel) && /(auth|security|permission|token|session|oauth|jwt|middleware)/i.test(rel)) ||
|
|
185
|
-
(isRootPython(rel) && /(Depends|HTTPBearer|OAuth2|JWT|Authorization)/.test(pythonText(file)))
|
|
186
|
-
), 30).map((f) => f.path);
|
|
187
|
-
return { api, services, models, db, config, auth };
|
|
188
|
-
}
|
|
14
|
+
const {
|
|
15
|
+
detectBackendPaths,
|
|
16
|
+
detectEntrypoints,
|
|
17
|
+
detectFrameworks,
|
|
18
|
+
detectFrontendPaths,
|
|
19
|
+
getGitActivity,
|
|
20
|
+
hasPath,
|
|
21
|
+
isNonSourcePath,
|
|
22
|
+
topLevelDirectories,
|
|
23
|
+
topLoadBearing,
|
|
24
|
+
} = require("./context-detect")({ fs, path, safeReadJson, readIfText, formatBytes });
|
|
189
25
|
|
|
190
|
-
function detectFrontendPaths(result) {
|
|
191
|
-
const appSurface = /(^|\/)(frontend\/)?src\/(app|pages|routes)\//;
|
|
192
|
-
const appFile = /(^|\/)(frontend\/)?src\/(App|main|index)\.(jsx?|tsx?|vue|svelte)$/;
|
|
193
|
-
const componentFile = /(^|\/)(frontend\/)?src\/(components|ui|widgets)\//;
|
|
194
|
-
const apiFile = /(^|\/)(frontend\/)?src\/(lib|hooks|composables|stores|services|api)\/.*(api|client|query|fetch|request|finops)/;
|
|
195
|
-
const stylingFile = /tailwind\.config|globals\.css|app\.css|\.module\.css|\.scss$|(^|\/)(frontend\/)?src\/styles?\//;
|
|
196
|
-
const stateFile = /providers?\.(tsx?|jsx?)|react-query|use[A-Z].*\.(ts|tsx|js|jsx)|(^|\/)(stores?|state|pinia|zustand|redux)\//;
|
|
197
|
-
return {
|
|
198
|
-
app: findRepoFiles(result, (rel) => !isNonSourcePath(rel) && (appSurface.test(rel) || appFile.test(rel) || /(^|\/)app\.vue$/.test(rel) || /apps\/[^/]+\/(app|pages|routes|src\/app|src\/pages|src\/routes)\//.test(rel)), 24).map((f) => f.path),
|
|
199
|
-
components: findRepoFiles(result, (rel) => !isNonSourcePath(rel) && (componentFile.test(rel) || /apps\/[^/]+\/.*(components|ui|widgets)\//.test(rel) || /packages\/[^/]+\/.*(components|ui|widgets)\//.test(rel)), 24).map((f) => f.path),
|
|
200
|
-
apiClient: findRepoFiles(result, (rel) => !isNonSourcePath(rel) && (apiFile.test(rel) || /apps\/[^/]+\/.*(lib|hooks|composables|services|api)\/.*(api|client|query|fetch|request)/.test(rel)), 24).map((f) => f.path),
|
|
201
|
-
styling: findRepoFiles(result, (rel) => !isNonSourcePath(rel) && stylingFile.test(rel), 24).map((f) => f.path),
|
|
202
|
-
state: findRepoFiles(result, (rel) => !isNonSourcePath(rel) && stateFile.test(rel), 24).map((f) => f.path),
|
|
203
|
-
};
|
|
204
|
-
}
|
|
205
26
|
|
|
206
27
|
function createOptimizeContext(rootDir = process.cwd(), scope = null) {
|
|
207
28
|
const scan = scanRepo(rootDir);
|
|
@@ -253,140 +74,6 @@ function mdList(items, empty = "None detected.") {
|
|
|
253
74
|
return items.map((item) => `- \`${item}\``).join("\n");
|
|
254
75
|
}
|
|
255
76
|
|
|
256
|
-
function moduleNameForPath(rel) {
|
|
257
|
-
return rel
|
|
258
|
-
.replace(/\.[^.]+$/, "")
|
|
259
|
-
.replace(/\/__init__$/, "")
|
|
260
|
-
.replace(/\/index$/, "")
|
|
261
|
-
.replace(/\//g, ".");
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
function basenameStem(rel) {
|
|
265
|
-
return path.basename(rel).replace(/\.[^.]+$/, "");
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
function extractPythonImports(text) {
|
|
269
|
-
const imports = new Set();
|
|
270
|
-
const importRe = /^\s*import\s+([a-zA-Z0-9_.,\s]+)/gm;
|
|
271
|
-
const fromRe = /^\s*from\s+([a-zA-Z0-9_.]+)\s+import\s+/gm;
|
|
272
|
-
let match;
|
|
273
|
-
while ((match = importRe.exec(text))) {
|
|
274
|
-
match[1].split(",").map((part) => part.trim().split(/\s+as\s+/)[0]).filter(Boolean).forEach((name) => imports.add(name));
|
|
275
|
-
}
|
|
276
|
-
while ((match = fromRe.exec(text))) imports.add(match[1]);
|
|
277
|
-
return imports;
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
function resolveJsImport(fromRel, specifier, sourcePaths) {
|
|
281
|
-
if (!specifier.startsWith(".")) return null;
|
|
282
|
-
const fromDir = path.posix.dirname(fromRel);
|
|
283
|
-
const base = path.posix.normalize(path.posix.join(fromDir, specifier));
|
|
284
|
-
const candidates = [
|
|
285
|
-
base,
|
|
286
|
-
`${base}.ts`,
|
|
287
|
-
`${base}.tsx`,
|
|
288
|
-
`${base}.js`,
|
|
289
|
-
`${base}.jsx`,
|
|
290
|
-
`${base}/index.ts`,
|
|
291
|
-
`${base}/index.tsx`,
|
|
292
|
-
`${base}/index.js`,
|
|
293
|
-
`${base}/index.jsx`,
|
|
294
|
-
];
|
|
295
|
-
return candidates.find((candidate) => sourcePaths.has(candidate)) || null;
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
function extractJsImports(fromRel, text, sourcePaths) {
|
|
299
|
-
const imports = new Set();
|
|
300
|
-
const importRe = /\bfrom\s+["']([^"']+)["']|import\s*\(\s*["']([^"']+)["']\s*\)|require\s*\(\s*["']([^"']+)["']\s*\)/g;
|
|
301
|
-
let match;
|
|
302
|
-
while ((match = importRe.exec(text))) {
|
|
303
|
-
const specifier = match[1] || match[2] || match[3];
|
|
304
|
-
const resolved = resolveJsImport(fromRel, specifier, sourcePaths);
|
|
305
|
-
if (resolved) imports.add(resolved);
|
|
306
|
-
}
|
|
307
|
-
return imports;
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
function getGitActivity(root) {
|
|
311
|
-
const activity = new Map();
|
|
312
|
-
try {
|
|
313
|
-
const output = execFileSync("git", ["-C", root, "log", "--since=180 days ago", "--name-only", "--format=%ct"], {
|
|
314
|
-
encoding: "utf8",
|
|
315
|
-
maxBuffer: 8 * 1024 * 1024,
|
|
316
|
-
stdio: ["ignore", "pipe", "ignore"],
|
|
317
|
-
});
|
|
318
|
-
let currentTs = 0;
|
|
319
|
-
for (const rawLine of output.split(/\r?\n/)) {
|
|
320
|
-
const line = rawLine.trim();
|
|
321
|
-
if (!line) continue;
|
|
322
|
-
if (/^\d{9,}$/.test(line)) {
|
|
323
|
-
currentTs = Number(line);
|
|
324
|
-
continue;
|
|
325
|
-
}
|
|
326
|
-
const rel = line.replace(/\\/g, "/");
|
|
327
|
-
const previous = activity.get(rel) || { touches: 0, lastTouched: 0 };
|
|
328
|
-
previous.touches += 1;
|
|
329
|
-
previous.lastTouched = Math.max(previous.lastTouched, currentTs);
|
|
330
|
-
activity.set(rel, previous);
|
|
331
|
-
}
|
|
332
|
-
} catch {
|
|
333
|
-
return activity;
|
|
334
|
-
}
|
|
335
|
-
return activity;
|
|
336
|
-
}
|
|
337
|
-
|
|
338
|
-
function formatGitActivity(activity) {
|
|
339
|
-
if (!activity || !activity.touches) return "no recent git touches";
|
|
340
|
-
const date = activity.lastTouched ? new Date(activity.lastTouched * 1000).toISOString().slice(0, 10) : "unknown date";
|
|
341
|
-
return `${activity.touches} recent git touch${activity.touches === 1 ? "" : "es"}, last ${date}`;
|
|
342
|
-
}
|
|
343
|
-
|
|
344
|
-
function topLoadBearing(root, files, allFiles, gitActivity, limit = 8) {
|
|
345
|
-
const textFiles = (allFiles || []).filter((file) => !file.ignored && file.kind !== "binary" && /\.(py|tsx?|jsx?|mjs|cjs|vue|svelte)$/.test(file.path));
|
|
346
|
-
const sourcePaths = new Set(textFiles.map((file) => file.path));
|
|
347
|
-
const pythonModuleToPath = new Map();
|
|
348
|
-
for (const file of textFiles.filter((candidate) => candidate.path.endsWith(".py"))) {
|
|
349
|
-
pythonModuleToPath.set(moduleNameForPath(file.path), file.path);
|
|
350
|
-
pythonModuleToPath.set(basenameStem(file.path), file.path);
|
|
351
|
-
}
|
|
352
|
-
|
|
353
|
-
const importRefs = new Map();
|
|
354
|
-
const textRefs = new Map();
|
|
355
|
-
const corpus = textFiles.map((file) => {
|
|
356
|
-
const text = readIfText(path.join(root, file.path), 512 * 1024) || "";
|
|
357
|
-
if (file.path.endsWith(".py")) {
|
|
358
|
-
for (const imported of extractPythonImports(text)) {
|
|
359
|
-
const target = pythonModuleToPath.get(imported) || pythonModuleToPath.get(imported.split(".").pop());
|
|
360
|
-
if (target && target !== file.path) importRefs.set(target, (importRefs.get(target) || 0) + 1);
|
|
361
|
-
}
|
|
362
|
-
} else if (/\.(tsx?|jsx?|mjs|cjs|vue|svelte)$/.test(file.path)) {
|
|
363
|
-
for (const target of extractJsImports(file.path, text, sourcePaths)) {
|
|
364
|
-
if (target !== file.path) importRefs.set(target, (importRefs.get(target) || 0) + 1);
|
|
365
|
-
}
|
|
366
|
-
}
|
|
367
|
-
return `${file.path}\n${text}`;
|
|
368
|
-
});
|
|
369
|
-
|
|
370
|
-
for (const file of files || []) {
|
|
371
|
-
const base = basenameStem(file.path);
|
|
372
|
-
const importName = base.replace(/[-.]/g, "_");
|
|
373
|
-
const refs = corpus.reduce((sum, text) => sum + (text.includes(base) || text.includes(importName) ? 1 : 0), 0);
|
|
374
|
-
textRefs.set(file.path, refs);
|
|
375
|
-
}
|
|
376
|
-
|
|
377
|
-
return [...(files || [])]
|
|
378
|
-
.filter((file) => !file.ignored && file.kind !== "binary")
|
|
379
|
-
.map((file) => {
|
|
380
|
-
const imports = importRefs.get(file.path) || 0;
|
|
381
|
-
const refs = textRefs.get(file.path) || 0;
|
|
382
|
-
const git = gitActivity.get(file.path) || { touches: 0, lastTouched: 0 };
|
|
383
|
-
const score = imports * 1000 + refs * 25 + Math.min(git.touches, 20) * 10 + Math.log10(file.size + 1);
|
|
384
|
-
return { file, imports, refs, git, score };
|
|
385
|
-
})
|
|
386
|
-
.sort((a, b) => b.score - a.score || b.file.size - a.file.size)
|
|
387
|
-
.slice(0, limit)
|
|
388
|
-
.map(({ file, imports, refs, git }) => `${file.path} (${imports} import ref${imports === 1 ? "" : "s"}, ${refs} text reference signal${refs === 1 ? "" : "s"}, ${formatGitActivity(git)}, ${formatBytes(file.size)})`);
|
|
389
|
-
}
|
|
390
77
|
|
|
391
78
|
function inferGaps(ctx) {
|
|
392
79
|
const gaps = [];
|
package/lib/prismo-dev/doctor.js
CHANGED
|
@@ -324,6 +324,7 @@ function toDoctorJsonPayload(result) {
|
|
|
324
324
|
confidence: usage.confidence,
|
|
325
325
|
totals: usage.totals,
|
|
326
326
|
sources: usage.sources,
|
|
327
|
+
multiAgent: usage.multiAgent || null,
|
|
327
328
|
}
|
|
328
329
|
: null;
|
|
329
330
|
return {
|
|
@@ -381,6 +382,13 @@ function renderDoctorTerminal(result) {
|
|
|
381
382
|
if (result.before.realUsage && result.before.realUsage.sessions.length) {
|
|
382
383
|
lines.push(`Local usage: ${formatTokenCount(result.before.realUsage.totals.displayTokens)} tokens across ${result.before.realUsage.sessions.length} recent session(s)`);
|
|
383
384
|
}
|
|
385
|
+
const multiAgent = result.before.realUsage && result.before.realUsage.multiAgent;
|
|
386
|
+
if (multiAgent && multiAgent.agentCount > 1) {
|
|
387
|
+
lines.push(`Multi-agent: ${multiAgent.agentCount} agents visible; highest pressure ${multiAgent.highestPressure}`);
|
|
388
|
+
if (multiAgent.coordinationWarnings && multiAgent.coordinationWarnings[0]) {
|
|
389
|
+
lines.push(`Coordination warning: ${multiAgent.coordinationWarnings[0]}`);
|
|
390
|
+
}
|
|
391
|
+
}
|
|
384
392
|
if (!(result.dryRun && result.applySuggestions)) lines.push(`Estimated exposed context reduction: ${result.exposedTokenReductionPercent}%`);
|
|
385
393
|
if (!result.dryRun) {
|
|
386
394
|
lines.push(`Payoff: ${scoreDelta > 0 ? `repo is ${scoreDelta} points cleaner for AI coding sessions` : "safe fixes applied; remaining risk needs manual cleanup"}`);
|
|
@@ -457,6 +465,10 @@ function renderDevTerminal(result) {
|
|
|
457
465
|
} else if (result.scan.realUsage) {
|
|
458
466
|
lines.push("Real local usage: no matching local sessions found for this repo");
|
|
459
467
|
}
|
|
468
|
+
const multiAgent = result.scan.realUsage && result.scan.realUsage.multiAgent;
|
|
469
|
+
if (multiAgent && multiAgent.agentCount > 1) {
|
|
470
|
+
lines.push(`Multi-agent: ${multiAgent.agentCount} agents visible; highest pressure ${multiAgent.highestPressure}`);
|
|
471
|
+
}
|
|
460
472
|
lines.push("");
|
|
461
473
|
lines.push("Generated:");
|
|
462
474
|
result.optimize.generatedFiles.slice(0, 8).forEach((file) => lines.push(`- ${file}`));
|
package/lib/prismo-dev/mcp.js
CHANGED
|
@@ -69,6 +69,11 @@ function createMcpTools(deps) {
|
|
|
69
69
|
tool: { type: "string", enum: ["all", "codex", "claude"], description: "Which local session logs to inspect." },
|
|
70
70
|
limit: limitProperty,
|
|
71
71
|
}),
|
|
72
|
+
makeTool("prismo_multi_agent_watch", "Return multi-agent coordination risks across visible local Codex/Claude sessions.", {
|
|
73
|
+
path: pathProperty,
|
|
74
|
+
tool: { type: "string", enum: ["all", "codex", "claude"], description: "Which local session logs to inspect." },
|
|
75
|
+
limit: limitProperty,
|
|
76
|
+
}),
|
|
72
77
|
makeTool("prismo_shield_run", "Run a noisy command through Prismo shield and store full output locally.", {
|
|
73
78
|
path: pathProperty,
|
|
74
79
|
command: {
|
|
@@ -131,11 +136,27 @@ function createMcpTools(deps) {
|
|
|
131
136
|
const summary = getUsageSummary({
|
|
132
137
|
cwd: target,
|
|
133
138
|
limit: Number(args.limit) || 3,
|
|
134
|
-
|
|
139
|
+
tool: args.tool || "all",
|
|
135
140
|
});
|
|
136
141
|
return createTextResult(summary);
|
|
137
142
|
}
|
|
138
143
|
|
|
144
|
+
if (name === "prismo_multi_agent_watch") {
|
|
145
|
+
const summary = getUsageSummary({
|
|
146
|
+
cwd: target,
|
|
147
|
+
limit: Number(args.limit) || 8,
|
|
148
|
+
tool: args.tool || "all",
|
|
149
|
+
});
|
|
150
|
+
return createTextResult({
|
|
151
|
+
schemaVersion: 1,
|
|
152
|
+
generatedAt: summary.generatedAt,
|
|
153
|
+
scannedPath: summary.scannedPath,
|
|
154
|
+
tool: summary.tool,
|
|
155
|
+
totals: summary.totals,
|
|
156
|
+
multiAgent: summary.multiAgent,
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
|
|
139
160
|
if (name === "prismo_shield_run") {
|
|
140
161
|
return createTextResult(runShield(target, args.command));
|
|
141
162
|
}
|
|
@@ -265,6 +286,7 @@ async function runMcpDoctor(deps) {
|
|
|
265
286
|
"prismo_scan",
|
|
266
287
|
"prismo_doctor_dry_run",
|
|
267
288
|
"prismo_watch_snapshot",
|
|
289
|
+
"prismo_multi_agent_watch",
|
|
268
290
|
"prismo_shield_run",
|
|
269
291
|
"prismo_shield_search",
|
|
270
292
|
"prismo_shield_last",
|
|
@@ -315,7 +337,7 @@ async function runMcpDoctor(deps) {
|
|
|
315
337
|
next: [
|
|
316
338
|
"Add the config snippet to your MCP-compatible client.",
|
|
317
339
|
"Restart the client and confirm prismodev appears in the MCP tool list.",
|
|
318
|
-
"Ask the agent to call prismo_scan or prismo_shield_run.",
|
|
340
|
+
"Ask the agent to call prismo_scan, prismo_multi_agent_watch, or prismo_shield_run.",
|
|
319
341
|
],
|
|
320
342
|
};
|
|
321
343
|
}
|
package/lib/prismo-dev/report.js
CHANGED
|
@@ -45,6 +45,10 @@ function renderTerminalReport(result, options = {}) {
|
|
|
45
45
|
if (result.realUsage && result.realUsage.sessions.length) {
|
|
46
46
|
lines.push(`- Real local usage: ${formatTokenCount(result.realUsage.totals.displayTokens)} tokens across ${result.realUsage.sessions.length} session(s)`);
|
|
47
47
|
lines.push(`- Usage confidence: ${result.realUsage.confidence}`);
|
|
48
|
+
if (result.realUsage.multiAgent && result.realUsage.multiAgent.agentCount > 1) {
|
|
49
|
+
lines.push(`- Multi-agent: ${result.realUsage.multiAgent.agentCount} agents visible; highest pressure ${result.realUsage.multiAgent.highestPressure}`);
|
|
50
|
+
if (result.realUsage.multiAgent.coordinationWarnings[0]) lines.push(`- Coordination warning: ${result.realUsage.multiAgent.coordinationWarnings[0]}`);
|
|
51
|
+
}
|
|
48
52
|
} else if (result.realUsage) {
|
|
49
53
|
lines.push("- Real local usage: no matching local Codex/Claude Code sessions found for this repo");
|
|
50
54
|
}
|
|
@@ -116,6 +120,9 @@ function renderOptimizerFitTerminal(result, options = {}) {
|
|
|
116
120
|
lines.push(`Primary bottleneck: ${color(fit.summary, tone, useColor)}`);
|
|
117
121
|
if (result.realUsage && result.realUsage.sessions.length) {
|
|
118
122
|
lines.push(`Local usage: ${formatTokenCount(result.realUsage.totals.displayTokens)} tokens across ${result.realUsage.sessions.length} session(s)`);
|
|
123
|
+
if (result.realUsage.multiAgent && result.realUsage.multiAgent.agentCount > 1) {
|
|
124
|
+
lines.push(`Multi-agent: ${result.realUsage.multiAgent.agentCount} agents visible; highest pressure ${result.realUsage.multiAgent.highestPressure}`);
|
|
125
|
+
}
|
|
119
126
|
} else if (result.realUsage) {
|
|
120
127
|
lines.push("Local usage: no matching local Claude/Codex sessions found");
|
|
121
128
|
}
|
|
@@ -261,6 +268,9 @@ function renderMarkdownReport(result) {
|
|
|
261
268
|
if (result.realUsage) {
|
|
262
269
|
lines.push(`- **Real Local Usage:** ${result.realUsage.totals.displayTokens.toLocaleString()} tokens across ${result.realUsage.sessions.length} session(s)`);
|
|
263
270
|
lines.push(`- **Usage Confidence:** ${result.realUsage.confidence}`);
|
|
271
|
+
if (result.realUsage.multiAgent && result.realUsage.multiAgent.agentCount > 1) {
|
|
272
|
+
lines.push(`- **Multi-Agent:** ${result.realUsage.multiAgent.agentCount} agents visible; highest pressure ${result.realUsage.multiAgent.highestPressure}`);
|
|
273
|
+
}
|
|
264
274
|
}
|
|
265
275
|
lines.push("");
|
|
266
276
|
lines.push("Estimates are based on local file-size and configuration heuristics. They are not provider billing data and are not guaranteed savings.");
|
|
@@ -346,6 +356,20 @@ function renderMarkdownReport(result) {
|
|
|
346
356
|
lines.push(`- Estimated tool/output tokens: ${result.realUsage.totals.toolTokens.toLocaleString()}`);
|
|
347
357
|
lines.push(`- Confidence: ${result.realUsage.confidence}`);
|
|
348
358
|
lines.push("");
|
|
359
|
+
if (result.realUsage.multiAgent && result.realUsage.multiAgent.agentCount > 1) {
|
|
360
|
+
lines.push("### Multi-Agent Coordination");
|
|
361
|
+
lines.push("");
|
|
362
|
+
lines.push(`- Agents visible: ${result.realUsage.multiAgent.agentCount}`);
|
|
363
|
+
lines.push(`- Highest pressure: ${result.realUsage.multiAgent.highestPressure}`);
|
|
364
|
+
result.realUsage.multiAgent.coordinationWarnings.slice(0, 5).forEach((warning) => lines.push(`- ${warning}`));
|
|
365
|
+
if (result.realUsage.multiAgent.sharedFiles.length) {
|
|
366
|
+
lines.push(`- Shared repeated files: ${result.realUsage.multiAgent.sharedFiles.slice(0, 5).map((item) => `\`${item.path}\` (${item.agents} agents)`).join(", ")}`);
|
|
367
|
+
}
|
|
368
|
+
if (result.realUsage.multiAgent.sharedArtifacts.length) {
|
|
369
|
+
lines.push(`- Shared artifact leaks: ${result.realUsage.multiAgent.sharedArtifacts.slice(0, 5).map((item) => `${item.type} (${item.agents} agents)`).join(", ")}`);
|
|
370
|
+
}
|
|
371
|
+
lines.push("");
|
|
372
|
+
}
|
|
349
373
|
result.realUsage.sessions.slice(0, 5).forEach((session, index) => {
|
|
350
374
|
lines.push(`${index + 1}. ${session.tool} - ${session.title || session.sessionId}`);
|
|
351
375
|
lines.push(` - Tokens: ${session.displayTokens.toLocaleString()} (${session.confidence})`);
|