getprismo 0.1.23 → 0.1.25
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 +5 -1
- package/lib/prismo-dev/context-detect.js +342 -0
- package/lib/prismo-dev/context-optimize.js +11 -324
- 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-cost.js +336 -0
- package/lib/prismo-dev/usage-sessions.js +298 -0
- package/lib/prismo-dev/usage-watch.js +59 -1198
- package/lib/prismo-dev/watch-live.js +353 -0
- package/lib/prismo-dev/watch-render.js +290 -0
- package/package.json +3 -3
|
@@ -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 = [];
|
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
module.exports = function createScanDetect(deps) {
|
|
2
|
+
const { fs, http, https, os, path, readIfText, estimateTokens, getClaudeSessionFiles, getCodexSessionFiles, normalizeRel } = deps;
|
|
3
|
+
|
|
4
|
+
function countJsonObjectKeys(value, keyName) {
|
|
5
|
+
if (!value || typeof value !== "object") return 0;
|
|
6
|
+
let count = 0;
|
|
7
|
+
if (value[keyName] && typeof value[keyName] === "object") {
|
|
8
|
+
count += Array.isArray(value[keyName]) ? value[keyName].length : Object.keys(value[keyName]).length;
|
|
9
|
+
}
|
|
10
|
+
for (const child of Object.values(value)) {
|
|
11
|
+
if (child && typeof child === "object") count += countJsonObjectKeys(child, keyName);
|
|
12
|
+
}
|
|
13
|
+
return count;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function scanClaudeConfig(root) {
|
|
17
|
+
const candidates = [
|
|
18
|
+
path.join(os.homedir(), ".claude", "settings.json"),
|
|
19
|
+
path.join(os.homedir(), ".claude.json"),
|
|
20
|
+
path.join(root, ".claude", "settings.json"),
|
|
21
|
+
path.join(root, ".claude.json"),
|
|
22
|
+
];
|
|
23
|
+
const found = [];
|
|
24
|
+
let mcpServers = 0;
|
|
25
|
+
let hooks = 0;
|
|
26
|
+
let pluginRefs = 0;
|
|
27
|
+
|
|
28
|
+
for (const filePath of candidates) {
|
|
29
|
+
if (!fs.existsSync(filePath)) continue;
|
|
30
|
+
const text = readIfText(filePath);
|
|
31
|
+
if (!text) continue;
|
|
32
|
+
const rel = filePath.startsWith(root) ? normalizeRel(path.relative(root, filePath)) : filePath;
|
|
33
|
+
found.push(rel);
|
|
34
|
+
try {
|
|
35
|
+
const json = JSON.parse(text);
|
|
36
|
+
mcpServers += countJsonObjectKeys(json, "mcpServers");
|
|
37
|
+
hooks += countJsonObjectKeys(json, "hooks");
|
|
38
|
+
} catch {
|
|
39
|
+
mcpServers += (text.match(/mcpServers|mcp_servers|mcp-server/g) || []).length;
|
|
40
|
+
hooks += (text.match(/hooks|hook/g) || []).length;
|
|
41
|
+
}
|
|
42
|
+
pluginRefs += (text.match(/plugin|skill/gi) || []).length;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return { files: found, mcpServers, hooks, pluginRefs };
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function scanCodexConfig(root) {
|
|
49
|
+
const candidates = [
|
|
50
|
+
path.join(root, ".codex", "config.toml"),
|
|
51
|
+
path.join(os.homedir(), ".codex", "config.toml"),
|
|
52
|
+
path.join(root, "AGENTS.md"),
|
|
53
|
+
];
|
|
54
|
+
const found = [];
|
|
55
|
+
let mcpServers = 0;
|
|
56
|
+
|
|
57
|
+
for (const filePath of candidates) {
|
|
58
|
+
if (!fs.existsSync(filePath)) continue;
|
|
59
|
+
const text = readIfText(filePath);
|
|
60
|
+
if (!text) continue;
|
|
61
|
+
found.push(filePath.startsWith(root) ? normalizeRel(path.relative(root, filePath)) : filePath);
|
|
62
|
+
mcpServers += (text.match(/\[mcp|mcp_servers|mcp-server|server\]/gi) || []).length;
|
|
63
|
+
}
|
|
64
|
+
return { files: found, mcpServers };
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function commandExists(command) {
|
|
68
|
+
const pathEntries = (process.env.PATH || "").split(path.delimiter).filter(Boolean);
|
|
69
|
+
const names = process.platform === "win32" ? [command, `${command}.cmd`, `${command}.exe`, `${command}.ps1`] : [command];
|
|
70
|
+
return pathEntries.some((entry) => names.some((name) => fs.existsSync(path.join(entry, name))));
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function pathExistsAny(paths) {
|
|
74
|
+
return paths.some((candidate) => fs.existsSync(candidate));
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function detectOptimizationStack(root, claudeConfig, codexConfig) {
|
|
78
|
+
const projectClaudePlugin = fs.existsSync(path.join(root, ".claude-plugin")) || fs.existsSync(path.join(root, ".claude", "settings.json"));
|
|
79
|
+
const projectMana = fs.existsSync(path.join(root, ".mana-mcp.json")) || fs.existsSync(path.join(os.homedir(), ".mana"));
|
|
80
|
+
const projectHeadroom = fs.existsSync(path.join(root, ".headroom")) || fs.existsSync(path.join(os.homedir(), ".headroom"));
|
|
81
|
+
const projectDistill = fs.existsSync(path.join(os.homedir(), ".config", "distill")) || commandExists("distill");
|
|
82
|
+
const projectRtk = fs.existsSync(path.join(root, ".rtk")) || commandExists("rtk");
|
|
83
|
+
const packageText = readIfText(path.join(root, "package.json"), 512 * 1024) || "";
|
|
84
|
+
const readmeText = readIfText(path.join(root, "README.md"), 512 * 1024) || "";
|
|
85
|
+
const projectText = `${packageText}\n${readmeText}`.toLowerCase();
|
|
86
|
+
const hasText = (pattern) => pattern.test(projectText);
|
|
87
|
+
|
|
88
|
+
const tools = {
|
|
89
|
+
rtk: { detected: projectRtk, source: projectRtk ? "binary-or-project-config" : "not-detected" },
|
|
90
|
+
headroom: { detected: projectHeadroom || commandExists("headroom"), source: projectHeadroom ? "local-config" : commandExists("headroom") ? "binary" : "not-detected" },
|
|
91
|
+
distill: { detected: projectDistill, source: projectDistill ? "binary-or-user-config" : "not-detected" },
|
|
92
|
+
mana: { detected: projectMana || commandExists("mana"), source: projectMana ? "local-config" : commandExists("mana") ? "binary" : "not-detected" },
|
|
93
|
+
contextMode: { detected: commandExists("context-mode") || hasText(/context-mode/), source: commandExists("context-mode") ? "binary" : hasText(/context-mode/) ? "project-reference" : "not-detected" },
|
|
94
|
+
leanCtx: { detected: commandExists("lean-ctx") || hasText(/lean-ctx|lean ctx/), source: commandExists("lean-ctx") ? "binary" : hasText(/lean-ctx|lean ctx/) ? "project-reference" : "not-detected" },
|
|
95
|
+
repomix: { detected: commandExists("repomix") || hasText(/repomix/), source: commandExists("repomix") ? "binary" : hasText(/repomix/) ? "project-reference" : "not-detected" },
|
|
96
|
+
codegraph: { detected: commandExists("codegraph") || hasText(/codegraph|codebase-memory-mcp|jcodemunch|sigmap/), source: commandExists("codegraph") ? "binary" : hasText(/codegraph|codebase-memory-mcp|jcodemunch|sigmap/) ? "project-reference" : "not-detected" },
|
|
97
|
+
tokf: { detected: commandExists("tokf") || hasText(/tokf/), source: commandExists("tokf") ? "binary" : hasText(/tokf/) ? "project-reference" : "not-detected" },
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
return {
|
|
101
|
+
tools,
|
|
102
|
+
claudeHooks: claudeConfig.hooks,
|
|
103
|
+
claudeMcpServers: claudeConfig.mcpServers,
|
|
104
|
+
codexMcpServers: codexConfig.mcpServers,
|
|
105
|
+
claudePluginDetected: projectClaudePlugin,
|
|
106
|
+
mcpServerTotal: claudeConfig.mcpServers + codexConfig.mcpServers,
|
|
107
|
+
detectedTools: Object.entries(tools).filter(([, value]) => value.detected).map(([name]) => name),
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function detectAgentReadiness(root, claudeConfig, codexConfig, realUsage) {
|
|
112
|
+
const claudeHome = process.env.PRISMO_CLAUDE_HOME || path.join(os.homedir(), ".claude");
|
|
113
|
+
const codexHome = process.env.PRISMO_CODEX_HOME || path.join(os.homedir(), ".codex");
|
|
114
|
+
const cursorPaths = [
|
|
115
|
+
path.join(root, ".cursor"),
|
|
116
|
+
path.join(root, ".cursorrules"),
|
|
117
|
+
path.join(os.homedir(), ".cursor"),
|
|
118
|
+
path.join(os.homedir(), ".config", "Cursor"),
|
|
119
|
+
];
|
|
120
|
+
const usageSources = new Set(realUsage && realUsage.sources ? realUsage.sources : []);
|
|
121
|
+
|
|
122
|
+
const claudeSessionFiles = getClaudeSessionFiles(root);
|
|
123
|
+
const codexSessionFiles = getCodexSessionFiles();
|
|
124
|
+
|
|
125
|
+
return {
|
|
126
|
+
claudeCode: {
|
|
127
|
+
detected: claudeConfig.files.length > 0 || fs.existsSync(claudeHome) || claudeSessionFiles.length > 0,
|
|
128
|
+
configFiles: claudeConfig.files,
|
|
129
|
+
localLogsFound: claudeSessionFiles.length > 0 || usageSources.has("claude-code"),
|
|
130
|
+
mcpServers: claudeConfig.mcpServers,
|
|
131
|
+
hooks: claudeConfig.hooks,
|
|
132
|
+
exactProxyTracking: "limited-for-subscription-mode",
|
|
133
|
+
recommendedMode: "local-log-and-repo-scan",
|
|
134
|
+
},
|
|
135
|
+
codex: {
|
|
136
|
+
detected: codexConfig.files.length > 0 || fs.existsSync(codexHome) || codexSessionFiles.length > 0,
|
|
137
|
+
configFiles: codexConfig.files,
|
|
138
|
+
localLogsFound: codexSessionFiles.length > 0 || usageSources.has("codex"),
|
|
139
|
+
mcpServers: codexConfig.mcpServers,
|
|
140
|
+
exactProxyTracking: "available-when-using-api-key-base-url-mode",
|
|
141
|
+
recommendedMode: "prismo-proxy-for-api-mode-or-local-log-watch",
|
|
142
|
+
},
|
|
143
|
+
cursor: {
|
|
144
|
+
detected: pathExistsAny(cursorPaths),
|
|
145
|
+
configFiles: cursorPaths.filter((candidate) => fs.existsSync(candidate)),
|
|
146
|
+
localLogsFound: false,
|
|
147
|
+
exactProxyTracking: "available-only-if-configured-for-openai-compatible-base-url",
|
|
148
|
+
recommendedMode: "repo-scan-and-prismo-proxy-when-supported",
|
|
149
|
+
},
|
|
150
|
+
localUsageLogsAvailable: Boolean((realUsage && realUsage.sessions.length) || claudeSessionFiles.length || codexSessionFiles.length),
|
|
151
|
+
exactProxyTrackingAvailable: true,
|
|
152
|
+
notes: [
|
|
153
|
+
"Exact tracking is available when a tool sends OpenAI/Anthropic API traffic through Prismo.",
|
|
154
|
+
"Subscription coding-agent sessions usually require local-log visibility unless the tool supports a custom base URL.",
|
|
155
|
+
],
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function detectToolOutputRisk({ exposedLargeFiles, exposedHighRiskDirs, highRiskDirs }) {
|
|
160
|
+
const noisyDirs = highRiskDirs.filter((dir) => ["coverage", "test-results", "playwright-report", "logs", "dist", "build", ".next"].some((name) => dir.path.split("/").includes(name)));
|
|
161
|
+
const exposedNoisyDirs = exposedHighRiskDirs.filter((dir) => noisyDirs.some((candidate) => candidate.path === dir.path));
|
|
162
|
+
const noisyFiles = exposedLargeFiles.filter((file) => ["log", "json", "minified", "lock/generated"].includes(file.kind) || /\.(log|json|ndjson|out|trace|har)$/i.test(file.path));
|
|
163
|
+
const estimatedExposureTokens = estimateTokens(noisyFiles.reduce((sum, file) => sum + file.size, 0));
|
|
164
|
+
let level = "Low";
|
|
165
|
+
if (exposedNoisyDirs.length >= 3 || noisyFiles.length >= 3 || estimatedExposureTokens >= 250000) level = "High";
|
|
166
|
+
else if (exposedNoisyDirs.length || noisyFiles.length || estimatedExposureTokens >= 50000) level = "Medium";
|
|
167
|
+
|
|
168
|
+
return {
|
|
169
|
+
level,
|
|
170
|
+
exposedNoisyDirectories: exposedNoisyDirs.map((dir) => dir.path),
|
|
171
|
+
noisyDirectoriesDetected: noisyDirs.map((dir) => ({ path: dir.path, exposed: dir.exposed })),
|
|
172
|
+
exposedNoisyFiles: noisyFiles.map((file) => ({
|
|
173
|
+
path: file.path,
|
|
174
|
+
kind: file.kind,
|
|
175
|
+
sizeBytes: file.size,
|
|
176
|
+
estimatedTokensIfRead: estimateTokens(file.size),
|
|
177
|
+
})),
|
|
178
|
+
estimatedExposureTokens,
|
|
179
|
+
summary:
|
|
180
|
+
level === "High"
|
|
181
|
+
? "Large logs, test reports, build output, or generated files are exposed to coding-agent reads."
|
|
182
|
+
: level === "Medium"
|
|
183
|
+
? "Some noisy tool-output artifacts are present and may enter context during broad exploration."
|
|
184
|
+
: "No major exposed tool-output artifacts detected.",
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function detectOperationalNoise(files) {
|
|
189
|
+
const candidates = files
|
|
190
|
+
.filter((file) => !file.ignored && file.size >= 16 * 1024 && file.kind !== "binary")
|
|
191
|
+
.filter((file) => /\.(json|jsonl|ndjson|log|md|txt)$/i.test(file.path) || /(events?|inbox|calendar|github|issues?|heartbeat|poll|source-stream|session-dump|activity|notifications?)/i.test(file.path))
|
|
192
|
+
.slice(0, 120);
|
|
193
|
+
const findings = [];
|
|
194
|
+
|
|
195
|
+
for (const file of candidates) {
|
|
196
|
+
const text = readIfText(file.fullPath, 512 * 1024) || "";
|
|
197
|
+
if (!text) continue;
|
|
198
|
+
const signals = [];
|
|
199
|
+
const timestampCount = (text.match(/\b20\d{2}-\d{2}-\d{2}[T ][0-2]\d:/g) || []).length;
|
|
200
|
+
const emailCount = (text.match(/[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}/gi) || []).length;
|
|
201
|
+
const eventKeyCount = (text.match(/"(event|type|timestamp|created_at|updated_at|attendees|organizer|sender|subject|body|issue|pull_request|repository|payload)"\s*:/gi) || []).length;
|
|
202
|
+
const markdownEventCount = (text.match(/\b(calendar|inbox|email|github|issue|pull request|notification|attendee|heartbeat|poll(ed|ing)?)\b/gi) || []).length;
|
|
203
|
+
const repeatedObjectCount = (text.match(/^\s*\{.*\}\s*$/gm) || []).length;
|
|
204
|
+
|
|
205
|
+
if (timestampCount >= 12) signals.push(`${timestampCount} timestamps`);
|
|
206
|
+
if (emailCount >= 8) signals.push(`${emailCount} email-like strings`);
|
|
207
|
+
if (eventKeyCount >= 30) signals.push(`${eventKeyCount} event-shaped JSON keys`);
|
|
208
|
+
if (markdownEventCount >= 20) signals.push(`${markdownEventCount} operational keywords`);
|
|
209
|
+
if (repeatedObjectCount >= 15) signals.push(`${repeatedObjectCount} JSONL-style objects`);
|
|
210
|
+
|
|
211
|
+
if (signals.length >= 2 || eventKeyCount >= 60 || (timestampCount >= 20 && markdownEventCount >= 15)) {
|
|
212
|
+
findings.push({
|
|
213
|
+
path: file.path,
|
|
214
|
+
sizeBytes: file.size,
|
|
215
|
+
estimatedTokensIfRead: estimateTokens(file.size),
|
|
216
|
+
signals: signals.slice(0, 4),
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
const estimatedExposureTokens = findings.reduce((sum, item) => sum + item.estimatedTokensIfRead, 0);
|
|
222
|
+
let level = "Low";
|
|
223
|
+
if (findings.length >= 3 || estimatedExposureTokens >= 150000) level = "High";
|
|
224
|
+
else if (findings.length || estimatedExposureTokens >= 25000) level = "Medium";
|
|
225
|
+
|
|
226
|
+
return {
|
|
227
|
+
level,
|
|
228
|
+
files: findings.slice(0, 12),
|
|
229
|
+
estimatedExposureTokens,
|
|
230
|
+
summary:
|
|
231
|
+
level === "High"
|
|
232
|
+
? "Operational source-stream dumps may be leaking inbox/calendar/GitHub-style noise back into coding context."
|
|
233
|
+
: level === "Medium"
|
|
234
|
+
? "Possible operational source-stream dumps detected; these can become second-order context leaks."
|
|
235
|
+
: "No obvious operational source-stream dumps detected.",
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function buildProxyTrackingReadiness({ codexConfig, claudeConfig, realUsage }) {
|
|
240
|
+
return {
|
|
241
|
+
exactApiTracking: {
|
|
242
|
+
available: true,
|
|
243
|
+
description: "Available for apps and coding tools that send OpenAI or Anthropic API traffic through the Prismo base URL.",
|
|
244
|
+
},
|
|
245
|
+
codingAgentBaseUrlMode: {
|
|
246
|
+
codex: codexConfig.files.length ? "possible-if-using-api-key-mode" : "not-detected",
|
|
247
|
+
claudeCode: "limited-for-subscription-sessions",
|
|
248
|
+
cursor: "possible-if-configured-for-openai-compatible-provider",
|
|
249
|
+
},
|
|
250
|
+
localEstimateTracking: {
|
|
251
|
+
available: true,
|
|
252
|
+
logsFound: Boolean(realUsage && realUsage.sessions.length),
|
|
253
|
+
description: "Available for subscription coding tools when local Codex/Claude Code logs exist; accuracy depends on token fields exposed by those tools.",
|
|
254
|
+
},
|
|
255
|
+
unsupported: [
|
|
256
|
+
"Exact billing for hidden subscription sessions without provider traffic, API keys, or local token fields.",
|
|
257
|
+
"Prompt interception or tool rewriting is not enabled by PrismoDev Scan.",
|
|
258
|
+
],
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function checkUrlReachable(url, timeoutMs = 650) {
|
|
263
|
+
return new Promise((resolve) => {
|
|
264
|
+
let parsed;
|
|
265
|
+
try {
|
|
266
|
+
parsed = new URL(url);
|
|
267
|
+
} catch {
|
|
268
|
+
resolve({ url, reachable: false, error: "invalid-url" });
|
|
269
|
+
return;
|
|
270
|
+
}
|
|
271
|
+
const client = parsed.protocol === "https:" ? https : http;
|
|
272
|
+
const request = client.request(
|
|
273
|
+
{
|
|
274
|
+
method: "GET",
|
|
275
|
+
hostname: parsed.hostname,
|
|
276
|
+
port: parsed.port,
|
|
277
|
+
path: parsed.pathname === "/" ? "/health" : parsed.pathname,
|
|
278
|
+
timeout: timeoutMs,
|
|
279
|
+
},
|
|
280
|
+
(response) => {
|
|
281
|
+
response.resume();
|
|
282
|
+
resolve({ url, reachable: response.statusCode >= 200 && response.statusCode < 500, statusCode: response.statusCode });
|
|
283
|
+
}
|
|
284
|
+
);
|
|
285
|
+
request.on("timeout", () => {
|
|
286
|
+
request.destroy();
|
|
287
|
+
resolve({ url, reachable: false, error: "timeout" });
|
|
288
|
+
});
|
|
289
|
+
request.on("error", (error) => {
|
|
290
|
+
resolve({ url, reachable: false, error: error.code || error.message });
|
|
291
|
+
});
|
|
292
|
+
request.end();
|
|
293
|
+
});
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
return {
|
|
297
|
+
buildProxyTrackingReadiness,
|
|
298
|
+
checkUrlReachable,
|
|
299
|
+
detectAgentReadiness,
|
|
300
|
+
detectOperationalNoise,
|
|
301
|
+
detectOptimizationStack,
|
|
302
|
+
detectToolOutputRisk,
|
|
303
|
+
scanClaudeConfig,
|
|
304
|
+
scanCodexConfig,
|
|
305
|
+
};
|
|
306
|
+
};
|