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 CHANGED
@@ -552,6 +552,16 @@ watch reads local session logs from codex and claude code. it detects:
552
552
 
553
553
  watch tells you the single most useful action to take right now. usually: start a fresh session, or switch to a scoped context pack.
554
554
 
555
+ if you run multiple agents in the same repo, use:
556
+
557
+ ```bash
558
+ npx getprismo watch --agents
559
+ ```
560
+
561
+ multi-agent watch shows every visible local Codex/Claude Code session for the repo, ranks each agent by context pressure, and flags coordination risks like two agents repeatedly loading the same file, shared artifact leaks, multiple high-pressure sessions, or agents that should move noisy commands into `shield`.
562
+
563
+ the same multi-agent coordination signal is included in `usage --json`, `scan --usage --json`, doctor output, and the generated markdown report whenever multiple local sessions are visible for the repo.
564
+
555
565
  `watch --rescue` prints a paste-ready prompt for the active coding session. use it when the agent is looping, reading too many files, or flooding context with logs:
556
566
 
557
567
  ```bash
@@ -644,6 +654,8 @@ npx getprismo doctor --json # machine-readable output
644
654
  ```bash
645
655
  npx getprismo watch # live refresh
646
656
  npx getprismo watch --once # single snapshot
657
+ npx getprismo watch --agents # multi-agent coordination view
658
+ npx getprismo watch --agents --json # machine-readable multi-agent state
647
659
  npx getprismo watch --once --report # write .prismo/watch-report.md
648
660
  npx getprismo watch --once --json # machine-readable
649
661
  npx getprismo watch --auto # guardrails + throttle + 600k budget
@@ -681,6 +693,7 @@ npx getprismo mcp /path/to/repo
681
693
  - `prismo_scan`
682
694
  - `prismo_doctor_dry_run`
683
695
  - `prismo_watch_snapshot`
696
+ - `prismo_multi_agent_watch`
684
697
  - `prismo_shield_run`
685
698
  - `prismo_shield_search`
686
699
  - `prismo_shield_last`
@@ -688,7 +701,7 @@ npx getprismo mcp /path/to/repo
688
701
  - `prismo_firewall`
689
702
  - `prismo_cc_timeline`
690
703
 
691
- This lets an MCP-compatible agent search prior shielded test/build output, request scoped context packs, or inspect token-waste signals without pasting giant logs into the conversation.
704
+ This lets an MCP-compatible agent search prior shielded test/build output, request scoped context packs, inspect token-waste signals, or coordinate multiple local agents without pasting giant logs into the conversation.
692
705
 
693
706
  Generic MCP client config:
694
707
 
package/docs/mcp.md CHANGED
@@ -49,6 +49,7 @@ For local development from this repo:
49
49
  - `prismo_scan`: scan repo context/token waste
50
50
  - `prismo_doctor_dry_run`: preview doctor payoff without writing files
51
51
  - `prismo_watch_snapshot`: inspect live context pressure
52
+ - `prismo_multi_agent_watch`: inspect coordination risks across parallel local agents
52
53
  - `prismo_shield_run`: run a noisy command and store full output locally
53
54
  - `prismo_shield_search`: search stored shield output
54
55
  - `prismo_shield_last`: list recent shielded command runs
@@ -0,0 +1,342 @@
1
+ module.exports = function createContextDetect(deps) {
2
+ const { fs, path, safeReadJson, readIfText, formatBytes } = deps;
3
+ const { execFileSync } = require("child_process");
4
+
5
+ function findRepoFiles(result, predicate, limit = 40) {
6
+ return result.files
7
+ ? result.files.filter((file) => !file.ignored && file.kind !== "binary" && predicate(file.path, file)).slice(0, limit)
8
+ : [];
9
+ }
10
+
11
+ function detectFrameworks(root, result) {
12
+ const frameworks = new Set();
13
+ const packageFiles = findRepoFiles(result, (rel) => path.basename(rel) === "package.json" && !rel.includes("node_modules/"), 12);
14
+ for (const file of packageFiles) {
15
+ const pkg = safeReadJson(path.join(root, file.path));
16
+ const deps = { ...(pkg && pkg.dependencies), ...(pkg && pkg.devDependencies) };
17
+ if (deps.next) frameworks.add("Next.js");
18
+ if (deps.react) frameworks.add("React");
19
+ if (deps.vue || deps["@vue/runtime-core"]) frameworks.add("Vue");
20
+ if (deps.svelte || deps["@sveltejs/kit"]) frameworks.add(deps["@sveltejs/kit"] ? "SvelteKit" : "Svelte");
21
+ if (deps["solid-js"]) frameworks.add("Solid");
22
+ if (deps.astro) frameworks.add("Astro");
23
+ if (deps.nuxt) frameworks.add("Nuxt");
24
+ if (deps.express) frameworks.add("Express");
25
+ if (deps["@nestjs/core"]) frameworks.add("NestJS");
26
+ if (deps.prisma || deps["@prisma/client"]) frameworks.add("Prisma");
27
+ if (deps.tailwindcss) frameworks.add("Tailwind");
28
+ if (deps.typescript) frameworks.add("TypeScript");
29
+ if (pkg) frameworks.add("Node.js");
30
+ }
31
+
32
+ const textFiles = new Map(result.files.map((file) => [file.path, file]));
33
+ const requirements = [...textFiles.keys()].filter((rel) => rel.endsWith("requirements.txt"));
34
+ for (const rel of requirements) {
35
+ const text = readIfText(path.join(root, rel)) || "";
36
+ if (/fastapi/i.test(text)) frameworks.add("FastAPI");
37
+ if (/django/i.test(text)) frameworks.add("Django");
38
+ if (/flask/i.test(text)) frameworks.add("Flask");
39
+ if (/psycopg2|asyncpg|sqlalchemy/i.test(text)) frameworks.add("PostgreSQL");
40
+ if (/redis/i.test(text)) frameworks.add("Redis");
41
+ frameworks.add("Python");
42
+ }
43
+
44
+ const pyprojectFiles = [...textFiles.keys()].filter((rel) => rel.endsWith("pyproject.toml") && !rel.includes("node_modules/"));
45
+ for (const rel of pyprojectFiles) {
46
+ const text = readIfText(path.join(root, rel)) || "";
47
+ if (/fastapi/i.test(text)) frameworks.add("FastAPI");
48
+ if (/django/i.test(text)) frameworks.add("Django");
49
+ if (/flask/i.test(text)) frameworks.add("Flask");
50
+ if (/sqlalchemy/i.test(text)) frameworks.add("SQLAlchemy");
51
+ if (/psycopg|asyncpg/i.test(text)) frameworks.add("PostgreSQL");
52
+ if (/redis/i.test(text)) frameworks.add("Redis");
53
+ if (/celery/i.test(text)) frameworks.add("Celery");
54
+ frameworks.add("Python");
55
+ }
56
+
57
+ if (pyprojectFiles.length && !frameworks.has("Python")) frameworks.add("Python");
58
+ const pythonFiles = [...textFiles.values()].filter((file) => file.path.endsWith(".py") && !isNonSourcePath(file.path)).slice(0, 80);
59
+ for (const file of pythonFiles) {
60
+ const text = readIfText(path.join(root, file.path), 128 * 1024) || "";
61
+ if (/FastAPI\s*\(|from\s+fastapi\s+import|APIRouter\s*\(/.test(text)) frameworks.add("FastAPI");
62
+ if (/from\s+django|import\s+django|DJANGO_SETTINGS_MODULE/.test(text)) frameworks.add("Django");
63
+ if (/Flask\s*\(|from\s+flask\s+import/.test(text)) frameworks.add("Flask");
64
+ if (/sqlalchemy|create_engine|SessionLocal/i.test(text)) frameworks.add("SQLAlchemy");
65
+ }
66
+ if (pythonFiles.length) frameworks.add("Python");
67
+ if ([...textFiles.keys()].some((rel) => rel.endsWith("Cargo.toml"))) frameworks.add("Rust");
68
+ if ([...textFiles.keys()].some((rel) => rel.endsWith("go.mod"))) frameworks.add("Go");
69
+ if ([...textFiles.keys()].some((rel) => rel.endsWith("docker-compose.yml") || rel.endsWith("docker-compose.yaml"))) frameworks.add("Docker");
70
+ if ([...textFiles.keys()].some((rel) => rel.includes("prisma/schema.prisma"))) frameworks.add("Prisma");
71
+ if ([...textFiles.keys()].some((rel) => rel.includes("next.config."))) frameworks.add("Next.js");
72
+ if ([...textFiles.keys()].some((rel) => rel.includes("vite.config."))) frameworks.add("Vite");
73
+ if ([...textFiles.keys()].some((rel) => rel.includes("svelte.config."))) frameworks.add("SvelteKit");
74
+ if ([...textFiles.keys()].some((rel) => rel.includes("astro.config."))) frameworks.add("Astro");
75
+ if ([...textFiles.keys()].some((rel) => rel.includes("nuxt.config."))) frameworks.add("Nuxt");
76
+ if ([...textFiles.keys()].some((rel) => rel.endsWith(".vue"))) frameworks.add("Vue");
77
+ if ([...textFiles.keys()].some((rel) => rel.endsWith(".svelte"))) frameworks.add("Svelte");
78
+ if ([...textFiles.keys()].some((rel) => rel.includes("tailwind.config."))) frameworks.add("Tailwind");
79
+ if ([...textFiles.keys()].some((rel) => rel.endsWith("tsconfig.json"))) frameworks.add("TypeScript");
80
+ if ([...textFiles.keys()].some((rel) => rel.includes("alembic/") || rel.endsWith("alembic.ini"))) frameworks.add("Alembic");
81
+ if ([...textFiles.keys()].some((rel) => /postgres|postgresql/i.test(rel))) frameworks.add("PostgreSQL");
82
+ return Array.from(frameworks).sort();
83
+ }
84
+
85
+ function topLevelDirectories(root) {
86
+ try {
87
+ return fs.readdirSync(root, { withFileTypes: true })
88
+ .filter((entry) => entry.isDirectory() && !entry.name.startsWith(".") && entry.name !== "node_modules")
89
+ .map((entry) => entry.name)
90
+ .sort();
91
+ } catch {
92
+ return [];
93
+ }
94
+ }
95
+
96
+ function hasPath(result, matcher) {
97
+ return result.files.some((file) => matcher(file.path));
98
+ }
99
+
100
+ function detectEntrypoints(result) {
101
+ const candidates = [
102
+ "backend/app/main.py",
103
+ "backend/main.py",
104
+ "app/main.py",
105
+ "main.py",
106
+ "manage.py",
107
+ "frontend/src/app/page.tsx",
108
+ "frontend/src/app/layout.tsx",
109
+ "src/app/page.tsx",
110
+ "src/main.tsx",
111
+ "src/index.tsx",
112
+ "src/App.vue",
113
+ "src/App.svelte",
114
+ "src/routes/+page.svelte",
115
+ "src/pages/index.astro",
116
+ "app.vue",
117
+ "server.js",
118
+ "index.js",
119
+ "docker/docker-compose.yml",
120
+ "docker-compose.yml",
121
+ ];
122
+ const paths = new Set(result.files.map((file) => file.path));
123
+ const found = candidates.filter((candidate) => paths.has(candidate));
124
+ if (!found.length) {
125
+ const pyMain = result.files.find((f) => /^[^/]+\/__main__\.py$/.test(f.path));
126
+ if (pyMain) found.push(pyMain.path);
127
+ const pyInit = result.files.find((f) => /^[^/]+\/__init__\.py$/.test(f.path) && !f.path.includes("test"));
128
+ if (pyInit && !found.length) found.push(pyInit.path);
129
+ }
130
+ return found;
131
+ }
132
+
133
+ function isNonSourcePath(rel) {
134
+ if (/\.(test|spec|e2e)\.[jt]sx?$/.test(rel)) return true;
135
+ return /^(docs|docs_src|examples|samples|tutorials|tests|test|spec|__tests__|fixtures)\//i.test(rel) ||
136
+ /\/(docs|docs_src|examples|samples|tutorials|tests|test|__tests__|fixtures)\//.test(rel);
137
+ }
138
+
139
+ function detectBackendPaths(result) {
140
+ const pythonText = (file) => readIfText(path.join(result.root, file.path), 256 * 1024) || "";
141
+ const isRootPython = (rel) => /^[^/]+\.py$/.test(rel);
142
+ const isPython = (rel) => rel.endsWith(".py");
143
+ const hasFastApiSignal = (file) => /FastAPI\s*\(|APIRouter\s*\(|@app\.(get|post|put|patch|delete)|@router\.(get|post|put|patch|delete)/.test(pythonText(file));
144
+ const api = findRepoFiles(result, (rel, file) => !isNonSourcePath(rel) && (
145
+ /(backend|app|src)\/.*(router|routes|api)\//.test(rel) ||
146
+ /(backend|app|src)\/.*(router|routes).*\.py$/.test(rel) ||
147
+ /\/(routing|routers?)\.py$/.test(rel) ||
148
+ (isRootPython(rel) && hasFastApiSignal(file))
149
+ ), 30).map((f) => f.path);
150
+ const services = findRepoFiles(result, (rel) => !isNonSourcePath(rel) && (
151
+ /(backend|app|src)\/.*(service|services|application)/.test(rel) ||
152
+ /\/applications?\.py$/.test(rel) ||
153
+ (isRootPython(rel) && /(service|worker|manager|client|heartbeat|memory|chat|tool|orchestrator|pipeline)/i.test(rel))
154
+ ), 40).map((f) => f.path);
155
+ const models = findRepoFiles(result, (rel) => !isNonSourcePath(rel) && (
156
+ /(backend|app|src)\/.*models\.py$/.test(rel) ||
157
+ /(backend|app|src)\/.*schema/.test(rel) ||
158
+ /\/models\.py$/.test(rel) ||
159
+ (isRootPython(rel) && /(model|schema|entity|types?)/i.test(rel))
160
+ ), 30).map((f) => f.path);
161
+ const db = findRepoFiles(result, (rel) => !isNonSourcePath(rel) && (
162
+ /(backend|app|src)\/.*\/(db|database|alembic|migrations)[/.]/.test(rel) ||
163
+ (isPython(rel) && /(sqlite|qdrant|neo4j|database|storage|repository|vector|graph|migration)/i.test(rel))
164
+ ), 30).map((f) => f.path);
165
+ const config = findRepoFiles(result, (rel) => !isNonSourcePath(rel) && (
166
+ /(backend|app|src)\/.*(config|settings|env).*\.py$/.test(rel) ||
167
+ (isRootPython(rel) && /(config|settings|env)/i.test(rel)) ||
168
+ rel.endsWith("requirements.txt") ||
169
+ rel === "pyproject.toml"
170
+ ), 20).map((f) => f.path);
171
+ const auth = findRepoFiles(result, (rel, file) => !isNonSourcePath(rel) && (
172
+ /(backend|app|src)\/.*auth/.test(rel) ||
173
+ /\/security\.py$/.test(rel) ||
174
+ (isPython(rel) && /(auth|security|permission|token|session|oauth|jwt|middleware)/i.test(rel)) ||
175
+ (isRootPython(rel) && /(Depends|HTTPBearer|OAuth2|JWT|Authorization)/.test(pythonText(file)))
176
+ ), 30).map((f) => f.path);
177
+ return { api, services, models, db, config, auth };
178
+ }
179
+
180
+ function detectFrontendPaths(result) {
181
+ const appSurface = /(^|\/)(frontend\/)?src\/(app|pages|routes)\//;
182
+ const appFile = /(^|\/)(frontend\/)?src\/(App|main|index)\.(jsx?|tsx?|vue|svelte)$/;
183
+ const componentFile = /(^|\/)(frontend\/)?src\/(components|ui|widgets)\//;
184
+ const apiFile = /(^|\/)(frontend\/)?src\/(lib|hooks|composables|stores|services|api)\/.*(api|client|query|fetch|request|finops)/;
185
+ const stylingFile = /tailwind\.config|globals\.css|app\.css|\.module\.css|\.scss$|(^|\/)(frontend\/)?src\/styles?\//;
186
+ const stateFile = /providers?\.(tsx?|jsx?)|react-query|use[A-Z].*\.(ts|tsx|js|jsx)|(^|\/)(stores?|state|pinia|zustand|redux)\//;
187
+ return {
188
+ 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),
189
+ 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),
190
+ 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),
191
+ styling: findRepoFiles(result, (rel) => !isNonSourcePath(rel) && stylingFile.test(rel), 24).map((f) => f.path),
192
+ state: findRepoFiles(result, (rel) => !isNonSourcePath(rel) && stateFile.test(rel), 24).map((f) => f.path),
193
+ };
194
+ }
195
+
196
+ function moduleNameForPath(rel) {
197
+ return rel
198
+ .replace(/\.[^.]+$/, "")
199
+ .replace(/\/__init__$/, "")
200
+ .replace(/\/index$/, "")
201
+ .replace(/\//g, ".");
202
+ }
203
+
204
+ function basenameStem(rel) {
205
+ return path.basename(rel).replace(/\.[^.]+$/, "");
206
+ }
207
+
208
+ function extractPythonImports(text) {
209
+ const imports = new Set();
210
+ const importRe = /^\s*import\s+([a-zA-Z0-9_.,\s]+)/gm;
211
+ const fromRe = /^\s*from\s+([a-zA-Z0-9_.]+)\s+import\s+/gm;
212
+ let match;
213
+ while ((match = importRe.exec(text))) {
214
+ match[1].split(",").map((part) => part.trim().split(/\s+as\s+/)[0]).filter(Boolean).forEach((name) => imports.add(name));
215
+ }
216
+ while ((match = fromRe.exec(text))) imports.add(match[1]);
217
+ return imports;
218
+ }
219
+
220
+ function resolveJsImport(fromRel, specifier, sourcePaths) {
221
+ if (!specifier.startsWith(".")) return null;
222
+ const fromDir = path.posix.dirname(fromRel);
223
+ const base = path.posix.normalize(path.posix.join(fromDir, specifier));
224
+ const candidates = [
225
+ base,
226
+ `${base}.ts`,
227
+ `${base}.tsx`,
228
+ `${base}.js`,
229
+ `${base}.jsx`,
230
+ `${base}/index.ts`,
231
+ `${base}/index.tsx`,
232
+ `${base}/index.js`,
233
+ `${base}/index.jsx`,
234
+ ];
235
+ return candidates.find((candidate) => sourcePaths.has(candidate)) || null;
236
+ }
237
+
238
+ function extractJsImports(fromRel, text, sourcePaths) {
239
+ const imports = new Set();
240
+ const importRe = /\bfrom\s+["']([^"']+)["']|import\s*\(\s*["']([^"']+)["']\s*\)|require\s*\(\s*["']([^"']+)["']\s*\)/g;
241
+ let match;
242
+ while ((match = importRe.exec(text))) {
243
+ const specifier = match[1] || match[2] || match[3];
244
+ const resolved = resolveJsImport(fromRel, specifier, sourcePaths);
245
+ if (resolved) imports.add(resolved);
246
+ }
247
+ return imports;
248
+ }
249
+
250
+ function getGitActivity(root) {
251
+ const activity = new Map();
252
+ try {
253
+ const output = execFileSync("git", ["-C", root, "log", "--since=180 days ago", "--name-only", "--format=%ct"], {
254
+ encoding: "utf8",
255
+ maxBuffer: 8 * 1024 * 1024,
256
+ stdio: ["ignore", "pipe", "ignore"],
257
+ });
258
+ let currentTs = 0;
259
+ for (const rawLine of output.split(/\r?\n/)) {
260
+ const line = rawLine.trim();
261
+ if (!line) continue;
262
+ if (/^\d{9,}$/.test(line)) {
263
+ currentTs = Number(line);
264
+ continue;
265
+ }
266
+ const rel = line.replace(/\\/g, "/");
267
+ const previous = activity.get(rel) || { touches: 0, lastTouched: 0 };
268
+ previous.touches += 1;
269
+ previous.lastTouched = Math.max(previous.lastTouched, currentTs);
270
+ activity.set(rel, previous);
271
+ }
272
+ } catch {
273
+ return activity;
274
+ }
275
+ return activity;
276
+ }
277
+
278
+ function formatGitActivity(activity) {
279
+ if (!activity || !activity.touches) return "no recent git touches";
280
+ const date = activity.lastTouched ? new Date(activity.lastTouched * 1000).toISOString().slice(0, 10) : "unknown date";
281
+ return `${activity.touches} recent git touch${activity.touches === 1 ? "" : "es"}, last ${date}`;
282
+ }
283
+
284
+ function topLoadBearing(root, files, allFiles, gitActivity, limit = 8) {
285
+ const textFiles = (allFiles || []).filter((file) => !file.ignored && file.kind !== "binary" && /\.(py|tsx?|jsx?|mjs|cjs|vue|svelte)$/.test(file.path));
286
+ const sourcePaths = new Set(textFiles.map((file) => file.path));
287
+ const pythonModuleToPath = new Map();
288
+ for (const file of textFiles.filter((candidate) => candidate.path.endsWith(".py"))) {
289
+ pythonModuleToPath.set(moduleNameForPath(file.path), file.path);
290
+ pythonModuleToPath.set(basenameStem(file.path), file.path);
291
+ }
292
+
293
+ const importRefs = new Map();
294
+ const textRefs = new Map();
295
+ const corpus = textFiles.map((file) => {
296
+ const text = readIfText(path.join(root, file.path), 512 * 1024) || "";
297
+ if (file.path.endsWith(".py")) {
298
+ for (const imported of extractPythonImports(text)) {
299
+ const target = pythonModuleToPath.get(imported) || pythonModuleToPath.get(imported.split(".").pop());
300
+ if (target && target !== file.path) importRefs.set(target, (importRefs.get(target) || 0) + 1);
301
+ }
302
+ } else if (/\.(tsx?|jsx?|mjs|cjs|vue|svelte)$/.test(file.path)) {
303
+ for (const target of extractJsImports(file.path, text, sourcePaths)) {
304
+ if (target !== file.path) importRefs.set(target, (importRefs.get(target) || 0) + 1);
305
+ }
306
+ }
307
+ return `${file.path}\n${text}`;
308
+ });
309
+
310
+ for (const file of files || []) {
311
+ const base = basenameStem(file.path);
312
+ const importName = base.replace(/[-.]/g, "_");
313
+ const refs = corpus.reduce((sum, text) => sum + (text.includes(base) || text.includes(importName) ? 1 : 0), 0);
314
+ textRefs.set(file.path, refs);
315
+ }
316
+
317
+ return [...(files || [])]
318
+ .filter((file) => !file.ignored && file.kind !== "binary")
319
+ .map((file) => {
320
+ const imports = importRefs.get(file.path) || 0;
321
+ const refs = textRefs.get(file.path) || 0;
322
+ const git = gitActivity.get(file.path) || { touches: 0, lastTouched: 0 };
323
+ const score = imports * 1000 + refs * 25 + Math.min(git.touches, 20) * 10 + Math.log10(file.size + 1);
324
+ return { file, imports, refs, git, score };
325
+ })
326
+ .sort((a, b) => b.score - a.score || b.file.size - a.file.size)
327
+ .slice(0, limit)
328
+ .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)})`);
329
+ }
330
+
331
+ return {
332
+ detectBackendPaths,
333
+ detectEntrypoints,
334
+ detectFrameworks,
335
+ detectFrontendPaths,
336
+ getGitActivity,
337
+ hasPath,
338
+ isNonSourcePath,
339
+ topLevelDirectories,
340
+ topLoadBearing,
341
+ };
342
+ };