getprismo 0.1.10 → 0.1.13
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 +61 -1
- package/lib/prismo-dev/constants.js +12 -0
- package/lib/prismo-dev/context-optimize.js +30 -9
- package/lib/prismo-dev/firewall.js +8 -0
- package/lib/prismo-dev/report.js +19 -0
- package/lib/prismo-dev/scan.js +76 -2
- package/lib/prismo-dev/shield.js +384 -0
- package/lib/prismo-dev-scan.js +72 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -33,6 +33,7 @@ after you code npx getprismo cc timeline
|
|
|
33
33
|
**doctor** diagnoses the repo, applies safe fixes, and shows the before/after score.
|
|
34
34
|
**watch** monitors context pressure live and warns when things go wrong.
|
|
35
35
|
**cc timeline** reconstructs what happened in the session so you learn from it.
|
|
36
|
+
**shield** runs noisy commands without dumping full output back into the agent context.
|
|
36
37
|
|
|
37
38
|
---
|
|
38
39
|
|
|
@@ -41,6 +42,7 @@ after you code npx getprismo cc timeline
|
|
|
41
42
|
- missing `.claudeignore` / `.cursorignore` (the biggest single fix for most repos)
|
|
42
43
|
- lockfiles entering context (`package-lock.json`, `yarn.lock`, `pnpm-lock.yaml`)
|
|
43
44
|
- generated artifacts leaking in (`__pycache__`, `dist/`, `coverage/`, `.next/`)
|
|
45
|
+
- operational source-stream dumps leaking in (`events/`, `source-streams/`, inbox/calendar/GitHub JSONL exports)
|
|
44
46
|
- oversized instruction files (`CLAUDE.md` or `AGENTS.md` over 500 tokens)
|
|
45
47
|
- tool output dominating sessions (repeated reads, large command output)
|
|
46
48
|
- long-running sessions with stale context accumulation
|
|
@@ -136,6 +138,51 @@ watch caught lockfiles entering context, a file being read 286 times, and tool o
|
|
|
136
138
|
|
|
137
139
|
---
|
|
138
140
|
|
|
141
|
+
## new: context shield
|
|
142
|
+
|
|
143
|
+
if you know a command may dump huge output, run it through prismo:
|
|
144
|
+
|
|
145
|
+
```bash
|
|
146
|
+
npx getprismo shield -- npm test
|
|
147
|
+
npx getprismo shield -- pytest -q
|
|
148
|
+
npx getprismo shield -- npm run build
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
shield executes the command locally, stores full stdout/stderr under `.prismo/shield/runs/`, indexes the output in `.prismo/shield/shield.sqlite` using SQLite FTS5 when available, and prints only a compact summary plus useful error lines.
|
|
152
|
+
|
|
153
|
+
this is the lightweight context-sandbox layer: the full output stays on disk until you explicitly inspect it, instead of being pasted into the model context and re-sent every turn.
|
|
154
|
+
|
|
155
|
+
example:
|
|
156
|
+
|
|
157
|
+
```text
|
|
158
|
+
Prismo Shield
|
|
159
|
+
|
|
160
|
+
Command: npm test
|
|
161
|
+
Exit: 1
|
|
162
|
+
Captured: 186 KB (~46,500 tokens kept out of chat)
|
|
163
|
+
|
|
164
|
+
Full Output Stored:
|
|
165
|
+
- .prismo/shield/runs/2026-05-20T.../stdout.txt
|
|
166
|
+
- .prismo/shield/runs/2026-05-20T.../stderr.txt
|
|
167
|
+
- .prismo/shield/shield.sqlite
|
|
168
|
+
|
|
169
|
+
Summary Returned To Context:
|
|
170
|
+
- ERROR: auth.test.ts expected 200 received 401
|
|
171
|
+
- FAIL src/auth/session.test.ts
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
search previous shield output without reloading whole logs:
|
|
175
|
+
|
|
176
|
+
```bash
|
|
177
|
+
npx getprismo shield last
|
|
178
|
+
npx getprismo shield search "auth expected 200"
|
|
179
|
+
npx getprismo shield search "AUTH_FAILURE" --json
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
this is intentionally not magic interception yet. it is a safe local-first primitive you can tell agents to use for noisy commands.
|
|
183
|
+
|
|
184
|
+
---
|
|
185
|
+
|
|
139
186
|
## new: live guardrails mode
|
|
140
187
|
|
|
141
188
|
the easiest proactive mode is:
|
|
@@ -396,6 +443,8 @@ if an existing `.claudeignore` or `.cursorignore` already covers prismo's recomm
|
|
|
396
443
|
|
|
397
444
|
backend and frontend summaries include load-bearing candidates ranked by import references, text-reference signals, recent git touches when available, and file size, not just directory listings.
|
|
398
445
|
|
|
446
|
+
prismo also flags source-stream dumps separately from normal build artifacts. large inbox/calendar/github/event payload files are treated as operational noise because they often get summarized once, written near the repo, and then accidentally re-read by later coding sessions.
|
|
447
|
+
|
|
399
448
|
what doctor never touches:
|
|
400
449
|
|
|
401
450
|
- your real `CLAUDE.md`
|
|
@@ -482,6 +531,7 @@ no install needed. npx runs it directly.
|
|
|
482
531
|
| `scan --ci` | fail CI when token-risk gates fail |
|
|
483
532
|
| `optimize` | generate `.prismo/` context packs |
|
|
484
533
|
| `context` | print paste-ready prompt for agents |
|
|
534
|
+
| `shield` | run noisy commands while keeping full output out of chat |
|
|
485
535
|
| `setup` | detect tools, logs, proxy readiness |
|
|
486
536
|
| `usage` | show raw session token usage |
|
|
487
537
|
| `init` | add npm scripts and .prismo/README.md |
|
|
@@ -523,6 +573,16 @@ npx getprismo watch codex # only codex sessions
|
|
|
523
573
|
npx getprismo watch claude # only claude code sessions
|
|
524
574
|
```
|
|
525
575
|
|
|
576
|
+
### shield mode
|
|
577
|
+
|
|
578
|
+
```bash
|
|
579
|
+
npx getprismo shield -- npm test
|
|
580
|
+
npx getprismo shield -- pytest -q
|
|
581
|
+
npx getprismo shield --json -- npm run build
|
|
582
|
+
npx getprismo shield last
|
|
583
|
+
npx getprismo shield search "auth failure"
|
|
584
|
+
```
|
|
585
|
+
|
|
526
586
|
---
|
|
527
587
|
|
|
528
588
|
## cc modes
|
|
@@ -656,7 +716,7 @@ then your team can run `npm run ai:doctor` without remembering the full command.
|
|
|
656
716
|
- openai codex
|
|
657
717
|
- cursor
|
|
658
718
|
- any tool that respects `.claudeignore` or `.cursorignore`
|
|
659
|
-
- any repo (node, python, go, rust, monorepos, whatever)
|
|
719
|
+
- any repo (node, python, go, rust, vue, svelte, astro, monorepos, whatever)
|
|
660
720
|
|
|
661
721
|
---
|
|
662
722
|
|
|
@@ -84,6 +84,10 @@ const SOURCE_EXTENSIONS = new Set([
|
|
|
84
84
|
".html",
|
|
85
85
|
".vue",
|
|
86
86
|
".svelte",
|
|
87
|
+
".mjs",
|
|
88
|
+
".cjs",
|
|
89
|
+
".mts",
|
|
90
|
+
".cts",
|
|
87
91
|
]);
|
|
88
92
|
|
|
89
93
|
const INSTRUCTION_FILES = [
|
|
@@ -120,8 +124,15 @@ const DEFAULT_CLAUDEIGNORE = [
|
|
|
120
124
|
"package-lock.json",
|
|
121
125
|
"yarn.lock",
|
|
122
126
|
"pnpm-lock.yaml",
|
|
127
|
+
"bun.lockb",
|
|
123
128
|
"test-results/",
|
|
124
129
|
"playwright-report/",
|
|
130
|
+
"events/",
|
|
131
|
+
"event-dumps/",
|
|
132
|
+
"session-dumps/",
|
|
133
|
+
"source-streams/",
|
|
134
|
+
"inbox-dumps/",
|
|
135
|
+
"calendar-dumps/",
|
|
125
136
|
"models/",
|
|
126
137
|
"state-backups/",
|
|
127
138
|
"backups/",
|
|
@@ -158,6 +169,7 @@ const GENERATED_ARTIFACT_PATTERNS = [
|
|
|
158
169
|
/(^|\/)coverage\//,
|
|
159
170
|
/(^|\/)playwright-report\//,
|
|
160
171
|
/(^|\/)test-results\//,
|
|
172
|
+
/(^|\/)(events|event-dumps|session-dumps|source-streams|inbox-dumps|calendar-dumps)\//,
|
|
161
173
|
/(^|\/)__pycache__\//,
|
|
162
174
|
/(^|\/)\.pytest_cache\//,
|
|
163
175
|
/(^|\/)\.cache\//,
|
|
@@ -26,6 +26,11 @@ function detectFrameworks(root, result) {
|
|
|
26
26
|
const deps = { ...(pkg && pkg.dependencies), ...(pkg && pkg.devDependencies) };
|
|
27
27
|
if (deps.next) frameworks.add("Next.js");
|
|
28
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");
|
|
29
34
|
if (deps.express) frameworks.add("Express");
|
|
30
35
|
if (deps["@nestjs/core"]) frameworks.add("NestJS");
|
|
31
36
|
if (deps.prisma || deps["@prisma/client"]) frameworks.add("Prisma");
|
|
@@ -75,6 +80,11 @@ function detectFrameworks(root, result) {
|
|
|
75
80
|
if ([...textFiles.keys()].some((rel) => rel.includes("prisma/schema.prisma"))) frameworks.add("Prisma");
|
|
76
81
|
if ([...textFiles.keys()].some((rel) => rel.includes("next.config."))) frameworks.add("Next.js");
|
|
77
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");
|
|
78
88
|
if ([...textFiles.keys()].some((rel) => rel.includes("tailwind.config."))) frameworks.add("Tailwind");
|
|
79
89
|
if ([...textFiles.keys()].some((rel) => rel.endsWith("tsconfig.json"))) frameworks.add("TypeScript");
|
|
80
90
|
if ([...textFiles.keys()].some((rel) => rel.includes("alembic/") || rel.endsWith("alembic.ini"))) frameworks.add("Alembic");
|
|
@@ -109,6 +119,11 @@ function detectEntrypoints(result) {
|
|
|
109
119
|
"src/app/page.tsx",
|
|
110
120
|
"src/main.tsx",
|
|
111
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",
|
|
112
127
|
"server.js",
|
|
113
128
|
"index.js",
|
|
114
129
|
"docker/docker-compose.yml",
|
|
@@ -173,12 +188,18 @@ function detectBackendPaths(result) {
|
|
|
173
188
|
}
|
|
174
189
|
|
|
175
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)\//;
|
|
176
197
|
return {
|
|
177
|
-
app: findRepoFiles(result, (rel) => !isNonSourcePath(rel) && (
|
|
178
|
-
components: findRepoFiles(result, (rel) => !isNonSourcePath(rel) && (
|
|
179
|
-
apiClient: findRepoFiles(result, (rel) => !isNonSourcePath(rel) && (
|
|
180
|
-
styling: findRepoFiles(result, (rel) => !isNonSourcePath(rel) &&
|
|
181
|
-
state: findRepoFiles(result, (rel) => !isNonSourcePath(rel) &&
|
|
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),
|
|
182
203
|
};
|
|
183
204
|
}
|
|
184
205
|
|
|
@@ -194,7 +215,7 @@ function createOptimizeContext(rootDir = process.cwd(), scope = null) {
|
|
|
194
215
|
frameworks.some((name) => ["FastAPI", "Django", "Flask"].includes(name)) ||
|
|
195
216
|
Boolean(backend.api.length || backend.services.length || backend.db.length || backend.auth.length);
|
|
196
217
|
const frontendDetected = folders.includes("frontend") ||
|
|
197
|
-
frameworks.some((name) => ["Next.js", "React", "Vite"].includes(name)) ||
|
|
218
|
+
frameworks.some((name) => ["Next.js", "React", "Vite", "Vue", "Svelte", "SvelteKit", "Solid", "Astro", "Nuxt"].includes(name)) ||
|
|
198
219
|
Boolean(frontend.app.length || frontend.components.length || frontend.apiClient.length || frontend.state.length);
|
|
199
220
|
const warnings = [];
|
|
200
221
|
if (scan.exposedLargeFiles.length) warnings.push(`${scan.exposedLargeFiles.length} exposed large file(s) may bloat AI context.`);
|
|
@@ -321,7 +342,7 @@ function formatGitActivity(activity) {
|
|
|
321
342
|
}
|
|
322
343
|
|
|
323
344
|
function topLoadBearing(root, files, allFiles, gitActivity, limit = 8) {
|
|
324
|
-
const textFiles = (allFiles || []).filter((file) => !file.ignored && file.kind !== "binary" && /\.(py|tsx?|jsx
|
|
345
|
+
const textFiles = (allFiles || []).filter((file) => !file.ignored && file.kind !== "binary" && /\.(py|tsx?|jsx?|mjs|cjs|vue|svelte)$/.test(file.path));
|
|
325
346
|
const sourcePaths = new Set(textFiles.map((file) => file.path));
|
|
326
347
|
const pythonModuleToPath = new Map();
|
|
327
348
|
for (const file of textFiles.filter((candidate) => candidate.path.endsWith(".py"))) {
|
|
@@ -338,7 +359,7 @@ function topLoadBearing(root, files, allFiles, gitActivity, limit = 8) {
|
|
|
338
359
|
const target = pythonModuleToPath.get(imported) || pythonModuleToPath.get(imported.split(".").pop());
|
|
339
360
|
if (target && target !== file.path) importRefs.set(target, (importRefs.get(target) || 0) + 1);
|
|
340
361
|
}
|
|
341
|
-
} else if (/\.(tsx?|jsx
|
|
362
|
+
} else if (/\.(tsx?|jsx?|mjs|cjs|vue|svelte)$/.test(file.path)) {
|
|
342
363
|
for (const target of extractJsImports(file.path, text, sourcePaths)) {
|
|
343
364
|
if (target !== file.path) importRefs.set(target, (importRefs.get(target) || 0) + 1);
|
|
344
365
|
}
|
|
@@ -520,7 +541,7 @@ function renderBackendSummary(ctx) {
|
|
|
520
541
|
}
|
|
521
542
|
|
|
522
543
|
function renderFrontendSummary(ctx) {
|
|
523
|
-
const frontendCandidates = topLoadBearing(ctx.root, ctx.scan.files.filter((file) => /\.(tsx?|jsx
|
|
544
|
+
const frontendCandidates = topLoadBearing(ctx.root, ctx.scan.files.filter((file) => /\.(tsx?|jsx?|mjs|cjs|vue|svelte)$/.test(file.path) && /(^|\/)(frontend|src|app|pages|routes|components|ui|hooks|composables|stores)\//.test(file.path)), ctx.scan.files, ctx.gitActivity, 8);
|
|
524
545
|
return [
|
|
525
546
|
"# Frontend Summary",
|
|
526
547
|
"",
|
|
@@ -78,7 +78,15 @@ function buildBlockedContext(ctx) {
|
|
|
78
78
|
"playwright-report/**",
|
|
79
79
|
"test-results/**",
|
|
80
80
|
"logs/**",
|
|
81
|
+
"events/**",
|
|
82
|
+
"event-dumps/**",
|
|
83
|
+
"session-dumps/**",
|
|
84
|
+
"source-streams/**",
|
|
85
|
+
"inbox-dumps/**",
|
|
86
|
+
"calendar-dumps/**",
|
|
81
87
|
"**/*.log",
|
|
88
|
+
"**/*.jsonl",
|
|
89
|
+
"**/*.ndjson",
|
|
82
90
|
"**/*.map",
|
|
83
91
|
"**/__pycache__/**",
|
|
84
92
|
"package-lock.json",
|
package/lib/prismo-dev/report.js
CHANGED
|
@@ -68,6 +68,13 @@ function renderTerminalReport(result, options = {}) {
|
|
|
68
68
|
if (result.toolOutputRisk.exposedNoisyDirectories.length) lines.push(`- Exposed noisy dirs: ${result.toolOutputRisk.exposedNoisyDirectories.slice(0, 6).join(", ")}`);
|
|
69
69
|
if (result.toolOutputRisk.exposedNoisyFiles.length) lines.push(`- Exposed noisy files: ${result.toolOutputRisk.exposedNoisyFiles.slice(0, 4).map((file) => file.path).join(", ")}`);
|
|
70
70
|
lines.push("");
|
|
71
|
+
if (result.operationalNoise && result.operationalNoise.level !== "Low") {
|
|
72
|
+
lines.push(color("Operational Noise Risk", "bold", useColor));
|
|
73
|
+
lines.push(`- Level: ${result.operationalNoise.level}`);
|
|
74
|
+
lines.push(`- ${result.operationalNoise.summary}`);
|
|
75
|
+
result.operationalNoise.files.slice(0, 4).forEach((file) => lines.push(`- ${file.path}: ${file.signals.join(", ")}`));
|
|
76
|
+
lines.push("");
|
|
77
|
+
}
|
|
71
78
|
lines.push(color("Prismo Proxy Tracking", "bold", useColor));
|
|
72
79
|
lines.push("- Exact API tracking: available when traffic uses the Prismo OpenAI/Anthropic base URL");
|
|
73
80
|
lines.push(`- Codex API/base-url mode: ${result.proxyTrackingReadiness.codingAgentBaseUrlMode.codex}`);
|
|
@@ -233,6 +240,18 @@ function renderMarkdownReport(result) {
|
|
|
233
240
|
});
|
|
234
241
|
}
|
|
235
242
|
lines.push("");
|
|
243
|
+
lines.push("## Operational Source-Stream Noise");
|
|
244
|
+
lines.push("");
|
|
245
|
+
if (result.operationalNoise && result.operationalNoise.files.length) {
|
|
246
|
+
lines.push(`- Level: ${result.operationalNoise.level}`);
|
|
247
|
+
lines.push(`- ${result.operationalNoise.summary}`);
|
|
248
|
+
result.operationalNoise.files.slice(0, 20).forEach((file) => {
|
|
249
|
+
lines.push(`- \`${file.path}\` - ${formatBytes(file.sizeBytes)} - ${file.signals.join(", ")} - ~${file.estimatedTokensIfRead.toLocaleString()} tokens if read`);
|
|
250
|
+
});
|
|
251
|
+
} else {
|
|
252
|
+
lines.push("- No obvious inbox/calendar/GitHub/source-stream dumps detected.");
|
|
253
|
+
}
|
|
254
|
+
lines.push("");
|
|
236
255
|
lines.push("## Prismo Proxy Tracking Readiness");
|
|
237
256
|
lines.push("");
|
|
238
257
|
lines.push("- Exact API tracking: available when app/tool traffic uses the Prismo OpenAI/Anthropic base URL.");
|
package/lib/prismo-dev/scan.js
CHANGED
|
@@ -364,6 +364,57 @@ function detectToolOutputRisk({ exposedLargeFiles, exposedHighRiskDirs, highRisk
|
|
|
364
364
|
};
|
|
365
365
|
}
|
|
366
366
|
|
|
367
|
+
function detectOperationalNoise(files) {
|
|
368
|
+
const candidates = files
|
|
369
|
+
.filter((file) => !file.ignored && file.size >= 16 * 1024 && file.kind !== "binary")
|
|
370
|
+
.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))
|
|
371
|
+
.slice(0, 120);
|
|
372
|
+
const findings = [];
|
|
373
|
+
|
|
374
|
+
for (const file of candidates) {
|
|
375
|
+
const text = readIfText(file.fullPath, 512 * 1024) || "";
|
|
376
|
+
if (!text) continue;
|
|
377
|
+
const signals = [];
|
|
378
|
+
const timestampCount = (text.match(/\b20\d{2}-\d{2}-\d{2}[T ][0-2]\d:/g) || []).length;
|
|
379
|
+
const emailCount = (text.match(/[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}/gi) || []).length;
|
|
380
|
+
const eventKeyCount = (text.match(/"(event|type|timestamp|created_at|updated_at|attendees|organizer|sender|subject|body|issue|pull_request|repository|payload)"\s*:/gi) || []).length;
|
|
381
|
+
const markdownEventCount = (text.match(/\b(calendar|inbox|email|github|issue|pull request|notification|attendee|heartbeat|poll(ed|ing)?)\b/gi) || []).length;
|
|
382
|
+
const repeatedObjectCount = (text.match(/^\s*\{.*\}\s*$/gm) || []).length;
|
|
383
|
+
|
|
384
|
+
if (timestampCount >= 12) signals.push(`${timestampCount} timestamps`);
|
|
385
|
+
if (emailCount >= 8) signals.push(`${emailCount} email-like strings`);
|
|
386
|
+
if (eventKeyCount >= 30) signals.push(`${eventKeyCount} event-shaped JSON keys`);
|
|
387
|
+
if (markdownEventCount >= 20) signals.push(`${markdownEventCount} operational keywords`);
|
|
388
|
+
if (repeatedObjectCount >= 15) signals.push(`${repeatedObjectCount} JSONL-style objects`);
|
|
389
|
+
|
|
390
|
+
if (signals.length >= 2 || eventKeyCount >= 60 || (timestampCount >= 20 && markdownEventCount >= 15)) {
|
|
391
|
+
findings.push({
|
|
392
|
+
path: file.path,
|
|
393
|
+
sizeBytes: file.size,
|
|
394
|
+
estimatedTokensIfRead: estimateTokens(file.size),
|
|
395
|
+
signals: signals.slice(0, 4),
|
|
396
|
+
});
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
const estimatedExposureTokens = findings.reduce((sum, item) => sum + item.estimatedTokensIfRead, 0);
|
|
401
|
+
let level = "Low";
|
|
402
|
+
if (findings.length >= 3 || estimatedExposureTokens >= 150000) level = "High";
|
|
403
|
+
else if (findings.length || estimatedExposureTokens >= 25000) level = "Medium";
|
|
404
|
+
|
|
405
|
+
return {
|
|
406
|
+
level,
|
|
407
|
+
files: findings.slice(0, 12),
|
|
408
|
+
estimatedExposureTokens,
|
|
409
|
+
summary:
|
|
410
|
+
level === "High"
|
|
411
|
+
? "Operational source-stream dumps may be leaking inbox/calendar/GitHub-style noise back into coding context."
|
|
412
|
+
: level === "Medium"
|
|
413
|
+
? "Possible operational source-stream dumps detected; these can become second-order context leaks."
|
|
414
|
+
: "No obvious operational source-stream dumps detected.",
|
|
415
|
+
};
|
|
416
|
+
}
|
|
417
|
+
|
|
367
418
|
function buildProxyTrackingReadiness({ codexConfig, claudeConfig, realUsage }) {
|
|
368
419
|
return {
|
|
369
420
|
exactApiTracking: {
|
|
@@ -578,7 +629,7 @@ function addIssue(issues, severity, category, title, description, recommendation
|
|
|
578
629
|
});
|
|
579
630
|
}
|
|
580
631
|
|
|
581
|
-
function buildRecommendations({ hasClaudeIgnore, gitignorePatterns, exposedHighRiskDirs, largeFiles, instructionFiles, claudeConfig, toolOutputRisk, agentReadiness }) {
|
|
632
|
+
function buildRecommendations({ hasClaudeIgnore, gitignorePatterns, exposedHighRiskDirs, largeFiles, instructionFiles, claudeConfig, toolOutputRisk, operationalNoise, agentReadiness }) {
|
|
582
633
|
const recs = [];
|
|
583
634
|
if (!hasClaudeIgnore) {
|
|
584
635
|
recs.push("Create .claudeignore with generated/cache folders and large artifacts excluded.");
|
|
@@ -595,6 +646,9 @@ function buildRecommendations({ hasClaudeIgnore, gitignorePatterns, exposedHighR
|
|
|
595
646
|
if (toolOutputRisk && toolOutputRisk.level !== "Low") {
|
|
596
647
|
recs.push("Use command-output filtering or narrower shell commands for noisy tests, logs, diffs, and generated reports.");
|
|
597
648
|
}
|
|
649
|
+
if (operationalNoise && operationalNoise.level !== "Low") {
|
|
650
|
+
recs.push("Keep inbox/calendar/GitHub polling dumps out of coding-agent context; summarize them outside the repo or add them to AI ignore files.");
|
|
651
|
+
}
|
|
598
652
|
if (instructionFiles.some((file) => file.isClaude && file.tokens > 1500)) {
|
|
599
653
|
recs.push("Review CLAUDE.md for content that could move to linked docs; keep persistent instructions focused on durable rules.");
|
|
600
654
|
}
|
|
@@ -665,7 +719,7 @@ function calculateReductionPercent(beforeTokens, afterTokens) {
|
|
|
665
719
|
|
|
666
720
|
function chooseRecommendedScope(ctx) {
|
|
667
721
|
if (ctx.scope) return ctx.scope;
|
|
668
|
-
if (ctx.frameworks.some((name) => ["Next.js", "React", "Vite"].includes(name))) return "frontend";
|
|
722
|
+
if (ctx.frameworks.some((name) => ["Next.js", "React", "Vite", "Vue", "Svelte", "SvelteKit", "Solid", "Astro", "Nuxt"].includes(name))) return "frontend";
|
|
669
723
|
if (ctx.frameworks.some((name) => ["FastAPI", "Django", "Flask", "Python"].includes(name))) return "backend";
|
|
670
724
|
return null;
|
|
671
725
|
}
|
|
@@ -720,6 +774,7 @@ function toJsonPayload(result) {
|
|
|
720
774
|
agentReadiness: result.agentReadiness,
|
|
721
775
|
optimizationStack: result.optimizationStack,
|
|
722
776
|
toolOutputRisk: result.toolOutputRisk,
|
|
777
|
+
operationalNoise: result.operationalNoise,
|
|
723
778
|
proxyTrackingReadiness: result.proxyTrackingReadiness,
|
|
724
779
|
suggestedClaudeIgnore: result.recommendedClaudeIgnore,
|
|
725
780
|
suggestedCursorIgnore: result.recommendedCursorIgnore,
|
|
@@ -1010,6 +1065,7 @@ function scanRepo(rootDir = process.cwd(), options = {}) {
|
|
|
1010
1065
|
const optimizationStack = detectOptimizationStack(root, claudeConfig, codexConfig);
|
|
1011
1066
|
const agentReadiness = detectAgentReadiness(root, claudeConfig, codexConfig, realUsage);
|
|
1012
1067
|
const toolOutputRisk = detectToolOutputRisk({ exposedLargeFiles, exposedHighRiskDirs, highRiskDirs });
|
|
1068
|
+
const operationalNoise = detectOperationalNoise(files);
|
|
1013
1069
|
const proxyTrackingReadiness = buildProxyTrackingReadiness({ codexConfig, claudeConfig, realUsage });
|
|
1014
1070
|
|
|
1015
1071
|
if (toolOutputRisk.level !== "Low") {
|
|
@@ -1026,6 +1082,20 @@ function scanRepo(rootDir = process.cwd(), options = {}) {
|
|
|
1026
1082
|
);
|
|
1027
1083
|
}
|
|
1028
1084
|
|
|
1085
|
+
if (operationalNoise.level !== "Low") {
|
|
1086
|
+
addIssue(
|
|
1087
|
+
issues,
|
|
1088
|
+
operationalNoise.level === "High" ? "high" : "medium",
|
|
1089
|
+
"operational_noise",
|
|
1090
|
+
`${operationalNoise.files.length} source-stream dump${operationalNoise.files.length === 1 ? "" : "s"} may leak into context`,
|
|
1091
|
+
operationalNoise.files.slice(0, 5).map((file) => `${file.path} (${file.signals.join(", ")})`).join(", "),
|
|
1092
|
+
"Do not feed raw inbox/calendar/GitHub/event dumps back into coding sessions; summarize externally or add them to .claudeignore/.cursorignore.",
|
|
1093
|
+
operationalNoise.estimatedExposureTokens
|
|
1094
|
+
? `Likely avoidable token exposure: up to ~${operationalNoise.estimatedExposureTokens.toLocaleString()} tokens from operational noise files.`
|
|
1095
|
+
: "Potential savings estimate: prevents source-stream dumps from becoming recurring context."
|
|
1096
|
+
);
|
|
1097
|
+
}
|
|
1098
|
+
|
|
1029
1099
|
if (optimizationStack.mcpServerTotal >= 8) {
|
|
1030
1100
|
addIssue(
|
|
1031
1101
|
issues,
|
|
@@ -1074,10 +1144,12 @@ function scanRepo(rootDir = process.cwd(), options = {}) {
|
|
|
1074
1144
|
const largeFileSuggestions = exposedLargeFiles
|
|
1075
1145
|
.filter((file) => file.size >= 1024 * 1024 || ["log", "json", "minified", "lock/generated"].includes(file.kind))
|
|
1076
1146
|
.map((file) => file.path);
|
|
1147
|
+
const operationalNoiseSuggestions = operationalNoise.files.map((file) => file.path);
|
|
1077
1148
|
const recommendedClaudeIgnore = Array.from(new Set([
|
|
1078
1149
|
...DEFAULT_CLAUDEIGNORE,
|
|
1079
1150
|
...gitignorePatterns.filter((line) => !line.startsWith("!")),
|
|
1080
1151
|
...largeFileSuggestions,
|
|
1152
|
+
...operationalNoiseSuggestions,
|
|
1081
1153
|
]));
|
|
1082
1154
|
const recommendedCursorIgnore = Array.from(new Set([
|
|
1083
1155
|
...recommendedClaudeIgnore,
|
|
@@ -1094,6 +1166,7 @@ function scanRepo(rootDir = process.cwd(), options = {}) {
|
|
|
1094
1166
|
instructionFiles,
|
|
1095
1167
|
claudeConfig,
|
|
1096
1168
|
toolOutputRisk,
|
|
1169
|
+
operationalNoise,
|
|
1097
1170
|
agentReadiness,
|
|
1098
1171
|
});
|
|
1099
1172
|
buildRealUsageRecommendations(realUsage).forEach((rec) => recommendations.push(rec));
|
|
@@ -1109,6 +1182,7 @@ function scanRepo(rootDir = process.cwd(), options = {}) {
|
|
|
1109
1182
|
agentReadiness,
|
|
1110
1183
|
optimizationStack,
|
|
1111
1184
|
toolOutputRisk,
|
|
1185
|
+
operationalNoise,
|
|
1112
1186
|
proxyTrackingReadiness,
|
|
1113
1187
|
frameworks,
|
|
1114
1188
|
files,
|
|
@@ -0,0 +1,384 @@
|
|
|
1
|
+
module.exports = function createShield(deps) {
|
|
2
|
+
const {
|
|
3
|
+
fs,
|
|
4
|
+
path,
|
|
5
|
+
estimateTokens,
|
|
6
|
+
formatBytes,
|
|
7
|
+
color,
|
|
8
|
+
} = deps;
|
|
9
|
+
const { spawnSync } = require("child_process");
|
|
10
|
+
|
|
11
|
+
const SHIELD_SCHEMA_VERSION = 1;
|
|
12
|
+
|
|
13
|
+
function nowId() {
|
|
14
|
+
return new Date().toISOString().replace(/[:.]/g, "-");
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function ensureDir(dir) {
|
|
18
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function pickInterestingLines(text, limit = 24) {
|
|
22
|
+
const lines = String(text || "").split(/\r?\n/).filter(Boolean);
|
|
23
|
+
const interesting = [];
|
|
24
|
+
const patterns = [
|
|
25
|
+
/\b(error|failed|failure|exception|traceback|fatal|panic|segmentation fault)\b/i,
|
|
26
|
+
/\b(warn|warning|deprecated|timeout|denied|unauthorized|forbidden)\b/i,
|
|
27
|
+
/\b(assert|expected|received|diff|not found|cannot find|module not found)\b/i,
|
|
28
|
+
];
|
|
29
|
+
for (const line of lines) {
|
|
30
|
+
if (patterns.some((pattern) => pattern.test(line))) interesting.push(line.slice(0, 500));
|
|
31
|
+
if (interesting.length >= limit) break;
|
|
32
|
+
}
|
|
33
|
+
if (interesting.length) return interesting;
|
|
34
|
+
return lines.slice(Math.max(0, lines.length - Math.min(limit, 12))).map((line) => line.slice(0, 500));
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function summarizeOutput(stdout, stderr) {
|
|
38
|
+
const stdoutText = String(stdout || "");
|
|
39
|
+
const stderrText = String(stderr || "");
|
|
40
|
+
const combined = [stderrText, stdoutText].filter(Boolean).join("\n");
|
|
41
|
+
const totalBytes = Buffer.byteLength(stdoutText) + Buffer.byteLength(stderrText);
|
|
42
|
+
const totalTokens = estimateTokens(combined);
|
|
43
|
+
return {
|
|
44
|
+
stdoutBytes: Buffer.byteLength(stdoutText),
|
|
45
|
+
stderrBytes: Buffer.byteLength(stderrText),
|
|
46
|
+
totalBytes,
|
|
47
|
+
estimatedTokens: totalTokens,
|
|
48
|
+
interestingLines: pickInterestingLines(combined),
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function renderStoredReadCommand(pathValue) {
|
|
53
|
+
return `sed -n '1,160p' ${JSON.stringify(pathValue)}`;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function shieldRoot(root) {
|
|
57
|
+
return path.join(root, ".prismo", "shield");
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function shieldDbPath(root) {
|
|
61
|
+
return path.join(shieldRoot(root), "shield.sqlite");
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function indexPath(root) {
|
|
65
|
+
return path.join(shieldRoot(root), "index.jsonl");
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function sqlString(value) {
|
|
69
|
+
return `'${String(value == null ? "" : value).replace(/'/g, "''")}'`;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function sqliteAvailable() {
|
|
73
|
+
const result = spawnSync("sqlite3", ["--version"], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] });
|
|
74
|
+
return result.status === 0;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function sqliteExec(dbPath, sql, json = false) {
|
|
78
|
+
const args = json ? ["-json", dbPath, sql] : [dbPath, sql];
|
|
79
|
+
const result = spawnSync("sqlite3", args, { encoding: "utf8", maxBuffer: 20 * 1024 * 1024 });
|
|
80
|
+
if (result.status !== 0) {
|
|
81
|
+
throw new Error((result.stderr || result.error?.message || "sqlite3 failed").trim());
|
|
82
|
+
}
|
|
83
|
+
if (!json) return null;
|
|
84
|
+
const out = String(result.stdout || "").trim();
|
|
85
|
+
return out ? JSON.parse(out) : [];
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function ensureSqliteIndex(root) {
|
|
89
|
+
if (!sqliteAvailable()) return { available: false, reason: "sqlite3-not-found" };
|
|
90
|
+
ensureDir(shieldRoot(root));
|
|
91
|
+
const dbPath = shieldDbPath(root);
|
|
92
|
+
sqliteExec(dbPath, `
|
|
93
|
+
PRAGMA journal_mode=WAL;
|
|
94
|
+
CREATE TABLE IF NOT EXISTS shield_runs (
|
|
95
|
+
id TEXT PRIMARY KEY,
|
|
96
|
+
command TEXT NOT NULL,
|
|
97
|
+
cwd TEXT NOT NULL,
|
|
98
|
+
exit_code INTEGER NOT NULL,
|
|
99
|
+
started_at TEXT NOT NULL,
|
|
100
|
+
finished_at TEXT NOT NULL,
|
|
101
|
+
duration_ms INTEGER NOT NULL,
|
|
102
|
+
stdout_path TEXT NOT NULL,
|
|
103
|
+
stderr_path TEXT NOT NULL,
|
|
104
|
+
total_bytes INTEGER NOT NULL,
|
|
105
|
+
estimated_tokens INTEGER NOT NULL,
|
|
106
|
+
summary_json TEXT NOT NULL
|
|
107
|
+
);
|
|
108
|
+
CREATE VIRTUAL TABLE IF NOT EXISTS shield_output USING fts5(
|
|
109
|
+
run_id UNINDEXED,
|
|
110
|
+
command,
|
|
111
|
+
stream UNINDEXED,
|
|
112
|
+
content,
|
|
113
|
+
tokenize='unicode61'
|
|
114
|
+
);
|
|
115
|
+
`);
|
|
116
|
+
return { available: true, path: path.relative(root, dbPath).replace(/\\/g, "/") };
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function indexShieldRun(root, payload, stdout, stderr) {
|
|
120
|
+
const sqlite = ensureSqliteIndex(root);
|
|
121
|
+
if (!sqlite.available) return { mode: "jsonl", sqlite };
|
|
122
|
+
const dbPath = shieldDbPath(root);
|
|
123
|
+
const runId = path.basename(payload.stored.directory);
|
|
124
|
+
sqliteExec(dbPath, `
|
|
125
|
+
INSERT OR REPLACE INTO shield_runs (
|
|
126
|
+
id, command, cwd, exit_code, started_at, finished_at, duration_ms,
|
|
127
|
+
stdout_path, stderr_path, total_bytes, estimated_tokens, summary_json
|
|
128
|
+
) VALUES (
|
|
129
|
+
${sqlString(runId)},
|
|
130
|
+
${sqlString(payload.command)},
|
|
131
|
+
${sqlString(payload.cwd)},
|
|
132
|
+
${Number(payload.exitCode || 0)},
|
|
133
|
+
${sqlString(payload.startedAt)},
|
|
134
|
+
${sqlString(payload.finishedAt)},
|
|
135
|
+
${Number(payload.durationMs || 0)},
|
|
136
|
+
${sqlString(payload.stored.stdout)},
|
|
137
|
+
${sqlString(payload.stored.stderr)},
|
|
138
|
+
${Number(payload.output.totalBytes || 0)},
|
|
139
|
+
${Number(payload.output.estimatedTokens || 0)},
|
|
140
|
+
${sqlString(JSON.stringify(payload))}
|
|
141
|
+
);
|
|
142
|
+
DELETE FROM shield_output WHERE run_id = ${sqlString(runId)};
|
|
143
|
+
INSERT INTO shield_output (run_id, command, stream, content)
|
|
144
|
+
VALUES (${sqlString(runId)}, ${sqlString(payload.command)}, 'stdout', ${sqlString(stdout)});
|
|
145
|
+
INSERT INTO shield_output (run_id, command, stream, content)
|
|
146
|
+
VALUES (${sqlString(runId)}, ${sqlString(payload.command)}, 'stderr', ${sqlString(stderr)});
|
|
147
|
+
`);
|
|
148
|
+
return { mode: "sqlite-fts5", sqlite };
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function readJsonlIndex(root) {
|
|
152
|
+
const filePath = indexPath(root);
|
|
153
|
+
if (!fs.existsSync(filePath)) return [];
|
|
154
|
+
return fs.readFileSync(filePath, "utf8")
|
|
155
|
+
.split(/\r?\n/)
|
|
156
|
+
.filter(Boolean)
|
|
157
|
+
.map((line) => {
|
|
158
|
+
try {
|
|
159
|
+
return JSON.parse(line);
|
|
160
|
+
} catch {
|
|
161
|
+
return null;
|
|
162
|
+
}
|
|
163
|
+
})
|
|
164
|
+
.filter(Boolean);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function readStoredText(root, relPath) {
|
|
168
|
+
const fullPath = path.join(root, relPath);
|
|
169
|
+
try {
|
|
170
|
+
return fs.readFileSync(fullPath, "utf8");
|
|
171
|
+
} catch {
|
|
172
|
+
return "";
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function fallbackSearch(root, query, limit = 10) {
|
|
177
|
+
const needle = String(query || "").toLowerCase();
|
|
178
|
+
if (!needle) return [];
|
|
179
|
+
const rows = readJsonlIndex(root).reverse();
|
|
180
|
+
const results = [];
|
|
181
|
+
for (const row of rows) {
|
|
182
|
+
for (const stream of ["stderr", "stdout"]) {
|
|
183
|
+
const relPath = row.stored?.[stream];
|
|
184
|
+
const text = readStoredText(root, relPath || "");
|
|
185
|
+
const lower = text.toLowerCase();
|
|
186
|
+
const index = lower.indexOf(needle);
|
|
187
|
+
if (index < 0) continue;
|
|
188
|
+
const start = Math.max(0, index - 180);
|
|
189
|
+
const end = Math.min(text.length, index + needle.length + 320);
|
|
190
|
+
results.push({
|
|
191
|
+
runId: path.basename(row.stored.directory),
|
|
192
|
+
command: row.command,
|
|
193
|
+
stream,
|
|
194
|
+
path: relPath,
|
|
195
|
+
exitCode: row.exitCode,
|
|
196
|
+
finishedAt: row.finishedAt,
|
|
197
|
+
snippet: text.slice(start, end).replace(/\s+/g, " ").trim(),
|
|
198
|
+
});
|
|
199
|
+
if (results.length >= limit) return results;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
return results;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function runShieldSearch(rootDir = process.cwd(), query = "", options = {}) {
|
|
206
|
+
const root = path.resolve(rootDir);
|
|
207
|
+
const limit = options.limit || 10;
|
|
208
|
+
if (!String(query || "").trim()) throw new Error("No search query provided. Use: prismo shield search \"error text\"");
|
|
209
|
+
const sqlite = ensureSqliteIndex(root);
|
|
210
|
+
if (sqlite.available && fs.existsSync(shieldDbPath(root))) {
|
|
211
|
+
try {
|
|
212
|
+
const phrase = `"${String(query).replace(/"/g, '""')}"`;
|
|
213
|
+
const rows = sqliteExec(shieldDbPath(root), `
|
|
214
|
+
SELECT
|
|
215
|
+
shield_output.run_id AS runId,
|
|
216
|
+
shield_output.command AS command,
|
|
217
|
+
shield_output.stream AS stream,
|
|
218
|
+
CASE shield_output.stream
|
|
219
|
+
WHEN 'stderr' THEN shield_runs.stderr_path
|
|
220
|
+
ELSE shield_runs.stdout_path
|
|
221
|
+
END AS path,
|
|
222
|
+
shield_runs.exit_code AS exitCode,
|
|
223
|
+
shield_runs.finished_at AS finishedAt,
|
|
224
|
+
snippet(shield_output, 3, '[', ']', ' ... ', 24) AS snippet
|
|
225
|
+
FROM shield_output
|
|
226
|
+
JOIN shield_runs ON shield_runs.id = shield_output.run_id
|
|
227
|
+
WHERE shield_output MATCH ${sqlString(phrase)}
|
|
228
|
+
ORDER BY rank
|
|
229
|
+
LIMIT ${Number(limit)};
|
|
230
|
+
`, true);
|
|
231
|
+
return { query, mode: "sqlite-fts5", results: rows };
|
|
232
|
+
} catch {
|
|
233
|
+
// FTS queries can reject punctuation-heavy input; fall back to JSONL/plain text scan.
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
return { query, mode: "jsonl-fallback", results: fallbackSearch(root, query, limit) };
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function runShieldLast(rootDir = process.cwd(), options = {}) {
|
|
240
|
+
const root = path.resolve(rootDir);
|
|
241
|
+
const limit = options.limit || 5;
|
|
242
|
+
return {
|
|
243
|
+
mode: fs.existsSync(shieldDbPath(root)) ? "sqlite-fts5" : "jsonl-fallback",
|
|
244
|
+
runs: readJsonlIndex(root).reverse().slice(0, limit),
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function runShield(rootDir = process.cwd(), commandArgs = [], options = {}) {
|
|
249
|
+
const root = path.resolve(rootDir);
|
|
250
|
+
if (!commandArgs.length) {
|
|
251
|
+
throw new Error("No command provided. Use: prismo shield -- npm test");
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
const startedAt = new Date();
|
|
255
|
+
const id = nowId();
|
|
256
|
+
const runsDir = path.join(root, ".prismo", "shield", "runs", id);
|
|
257
|
+
ensureDir(runsDir);
|
|
258
|
+
|
|
259
|
+
const command = commandArgs[0];
|
|
260
|
+
const args = commandArgs.slice(1);
|
|
261
|
+
const result = spawnSync(command, args, {
|
|
262
|
+
cwd: root,
|
|
263
|
+
encoding: "utf8",
|
|
264
|
+
maxBuffer: options.maxBuffer || 30 * 1024 * 1024,
|
|
265
|
+
shell: false,
|
|
266
|
+
env: process.env,
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
const finishedAt = new Date();
|
|
270
|
+
const stdout = result.stdout || "";
|
|
271
|
+
const stderr = result.stderr || "";
|
|
272
|
+
const stdoutPath = path.join(runsDir, "stdout.txt");
|
|
273
|
+
const stderrPath = path.join(runsDir, "stderr.txt");
|
|
274
|
+
const summary = summarizeOutput(stdout, stderr);
|
|
275
|
+
fs.writeFileSync(stdoutPath, stdout, "utf8");
|
|
276
|
+
fs.writeFileSync(stderrPath, stderr, "utf8");
|
|
277
|
+
|
|
278
|
+
const payload = {
|
|
279
|
+
schemaVersion: 1,
|
|
280
|
+
command: commandArgs.join(" "),
|
|
281
|
+
cwd: root,
|
|
282
|
+
exitCode: typeof result.status === "number" ? result.status : 1,
|
|
283
|
+
signal: result.signal || null,
|
|
284
|
+
error: result.error ? result.error.message : null,
|
|
285
|
+
startedAt: startedAt.toISOString(),
|
|
286
|
+
finishedAt: finishedAt.toISOString(),
|
|
287
|
+
durationMs: finishedAt.getTime() - startedAt.getTime(),
|
|
288
|
+
output: summary,
|
|
289
|
+
stored: {
|
|
290
|
+
stdout: path.relative(root, stdoutPath).replace(/\\/g, "/"),
|
|
291
|
+
stderr: path.relative(root, stderrPath).replace(/\\/g, "/"),
|
|
292
|
+
directory: path.relative(root, runsDir).replace(/\\/g, "/"),
|
|
293
|
+
},
|
|
294
|
+
next: [
|
|
295
|
+
"Feed the summary to the agent first; inspect full output only if needed.",
|
|
296
|
+
`Read stdout: ${renderStoredReadCommand(path.relative(root, stdoutPath).replace(/\\/g, "/"))}`,
|
|
297
|
+
`Read stderr: ${renderStoredReadCommand(path.relative(root, stderrPath).replace(/\\/g, "/"))}`,
|
|
298
|
+
],
|
|
299
|
+
};
|
|
300
|
+
fs.writeFileSync(path.join(runsDir, "summary.json"), JSON.stringify(payload, null, 2), "utf8");
|
|
301
|
+
fs.mkdirSync(path.join(root, ".prismo", "shield"), { recursive: true });
|
|
302
|
+
fs.appendFileSync(path.join(root, ".prismo", "shield", "index.jsonl"), `${JSON.stringify(payload)}\n`, "utf8");
|
|
303
|
+
payload.index = indexShieldRun(root, payload, stdout, stderr);
|
|
304
|
+
fs.writeFileSync(path.join(runsDir, "summary.json"), JSON.stringify(payload, null, 2), "utf8");
|
|
305
|
+
return payload;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
function renderShieldTerminal(result) {
|
|
309
|
+
const lines = [];
|
|
310
|
+
const exitTone = result.exitCode === 0 ? "green" : "red";
|
|
311
|
+
lines.push("");
|
|
312
|
+
lines.push(color("Prismo Shield", "bold"));
|
|
313
|
+
lines.push("");
|
|
314
|
+
lines.push(`Command: ${result.command}`);
|
|
315
|
+
lines.push(`Exit: ${color(String(result.exitCode), exitTone)}`);
|
|
316
|
+
lines.push(`Duration: ${result.durationMs}ms`);
|
|
317
|
+
lines.push(`Captured: ${formatBytes(result.output.totalBytes)} (~${result.output.estimatedTokens.toLocaleString()} tokens kept out of chat)`);
|
|
318
|
+
lines.push("");
|
|
319
|
+
lines.push("Full Output Stored:");
|
|
320
|
+
lines.push(`- ${result.stored.stdout}`);
|
|
321
|
+
lines.push(`- ${result.stored.stderr}`);
|
|
322
|
+
lines.push(`- ${result.stored.directory}/summary.json`);
|
|
323
|
+
lines.push("");
|
|
324
|
+
lines.push("Summary Returned To Context:");
|
|
325
|
+
result.output.interestingLines.slice(0, 24).forEach((line) => lines.push(`- ${line}`));
|
|
326
|
+
lines.push("");
|
|
327
|
+
lines.push("Next:");
|
|
328
|
+
lines.push("1. Give the agent this summary first.");
|
|
329
|
+
lines.push("2. Inspect the stored full output only if the summary is not enough.");
|
|
330
|
+
lines.push(`3. ${renderStoredReadCommand(result.stored.stderr)}`);
|
|
331
|
+
return lines.join("\n");
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
function renderShieldSearchTerminal(result) {
|
|
335
|
+
const lines = [];
|
|
336
|
+
lines.push("");
|
|
337
|
+
lines.push(color("Prismo Shield Search", "bold"));
|
|
338
|
+
lines.push("");
|
|
339
|
+
lines.push(`Query: ${result.query}`);
|
|
340
|
+
lines.push(`Index: ${result.mode}`);
|
|
341
|
+
lines.push(`Results: ${result.results.length}`);
|
|
342
|
+
lines.push("");
|
|
343
|
+
if (!result.results.length) {
|
|
344
|
+
lines.push("No matching shield output found.");
|
|
345
|
+
return lines.join("\n");
|
|
346
|
+
}
|
|
347
|
+
result.results.forEach((item, index) => {
|
|
348
|
+
lines.push(`${index + 1}. ${item.path} (${item.stream}, exit ${item.exitCode})`);
|
|
349
|
+
lines.push(` ${item.command}`);
|
|
350
|
+
lines.push(` ${String(item.snippet || "").slice(0, 700)}`);
|
|
351
|
+
});
|
|
352
|
+
lines.push("");
|
|
353
|
+
lines.push("Next:");
|
|
354
|
+
lines.push("Use the stored path above only if the snippet is not enough.");
|
|
355
|
+
return lines.join("\n");
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
function renderShieldLastTerminal(result) {
|
|
359
|
+
const lines = [];
|
|
360
|
+
lines.push("");
|
|
361
|
+
lines.push(color("Prismo Shield Last", "bold"));
|
|
362
|
+
lines.push("");
|
|
363
|
+
lines.push(`Index: ${result.mode}`);
|
|
364
|
+
if (!result.runs.length) {
|
|
365
|
+
lines.push("No shield runs found.");
|
|
366
|
+
return lines.join("\n");
|
|
367
|
+
}
|
|
368
|
+
result.runs.forEach((run, index) => {
|
|
369
|
+
lines.push(`${index + 1}. ${run.finishedAt} exit ${run.exitCode} ${run.command}`);
|
|
370
|
+
lines.push(` ${run.stored.directory}/summary.json`);
|
|
371
|
+
lines.push(` ${formatBytes(run.output.totalBytes)} (~${run.output.estimatedTokens.toLocaleString()} tokens kept out of chat)`);
|
|
372
|
+
});
|
|
373
|
+
return lines.join("\n");
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
return {
|
|
377
|
+
renderShieldLastTerminal,
|
|
378
|
+
renderShieldSearchTerminal,
|
|
379
|
+
renderShieldTerminal,
|
|
380
|
+
runShieldLast,
|
|
381
|
+
runShieldSearch,
|
|
382
|
+
runShield,
|
|
383
|
+
};
|
|
384
|
+
};
|
package/lib/prismo-dev-scan.js
CHANGED
|
@@ -267,6 +267,21 @@ const {
|
|
|
267
267
|
writeGeneratedFile,
|
|
268
268
|
});
|
|
269
269
|
|
|
270
|
+
const {
|
|
271
|
+
renderShieldLastTerminal,
|
|
272
|
+
renderShieldSearchTerminal,
|
|
273
|
+
renderShieldTerminal,
|
|
274
|
+
runShieldLast,
|
|
275
|
+
runShieldSearch,
|
|
276
|
+
runShield,
|
|
277
|
+
} = require("./prismo-dev/shield")({
|
|
278
|
+
fs,
|
|
279
|
+
path,
|
|
280
|
+
estimateTokens,
|
|
281
|
+
formatBytes: (...args) => formatBytes(...args),
|
|
282
|
+
color,
|
|
283
|
+
});
|
|
284
|
+
|
|
270
285
|
function printHelp() {
|
|
271
286
|
console.log(`Prismo CLI
|
|
272
287
|
|
|
@@ -275,6 +290,9 @@ Usage:
|
|
|
275
290
|
prismo init [--json] [--dry-run] [path]
|
|
276
291
|
prismo doctor [--json] [--dry-run] [--apply-ignores-only] [--no-context-packs] [--limit N] [path]
|
|
277
292
|
prismo firewall [task] [--json] [--dry-run] [path]
|
|
293
|
+
prismo shield [--json] [path] -- <command ...>
|
|
294
|
+
prismo shield last [--json] [--limit N] [path]
|
|
295
|
+
prismo shield search <query> [--json] [--limit N] [path]
|
|
278
296
|
prismo setup [--json] [--proxy-url URL] [path]
|
|
279
297
|
prismo scan [--fix] [--ci] [--json] [--usage] [--simple] [--no-report] [path]
|
|
280
298
|
prismo optimize [scope] [--json] [path]
|
|
@@ -289,6 +307,7 @@ Commands:
|
|
|
289
307
|
init Add local PrismoDev helper docs and npm scripts when package.json exists.
|
|
290
308
|
doctor Diagnose, safely optimize, re-scan, and show before/after payoff.
|
|
291
309
|
firewall Generate allowed/blocked context policy files for an AI coding task.
|
|
310
|
+
shield Run a noisy command, store full output locally, and return a compact summary.
|
|
292
311
|
scan Run PrismoDev for Claude Code, Codex, Cursor, and AI coding workflows.
|
|
293
312
|
optimize Generate lightweight AI-readable project context files in .prismo/.
|
|
294
313
|
context Print a copy-pasteable compact context prompt for AI coding tools.
|
|
@@ -475,6 +494,23 @@ Examples:
|
|
|
475
494
|
Output:
|
|
476
495
|
Writes .prismo/context-firewall.md, .prismo/allowed-context.txt, .prismo/blocked-context.txt, and .prismo/firewall-prompt.md.
|
|
477
496
|
The firewall is a local policy file for the agent to follow before reading files; it does not enforce filesystem access by itself.`,
|
|
497
|
+
shield: `Prismo Shield
|
|
498
|
+
|
|
499
|
+
Usage:
|
|
500
|
+
prismo shield [--json] [path] -- <command ...>
|
|
501
|
+
prismo shield last [--json] [--limit N] [path]
|
|
502
|
+
prismo shield search <query> [--json] [--limit N] [path]
|
|
503
|
+
|
|
504
|
+
Examples:
|
|
505
|
+
prismo shield -- npm test
|
|
506
|
+
prismo shield -- pytest -q
|
|
507
|
+
prismo shield --json -- npm run build
|
|
508
|
+
prismo shield last
|
|
509
|
+
prismo shield search "auth failure"
|
|
510
|
+
|
|
511
|
+
Output:
|
|
512
|
+
Runs the command locally, stores full stdout/stderr under .prismo/shield/runs/, indexes output in SQLite FTS5 when available, and prints only a compact summary plus useful error lines.
|
|
513
|
+
Search and last retrieve prior shield runs without re-feeding full output into agent context.`,
|
|
478
514
|
ci: `Prismo CI
|
|
479
515
|
|
|
480
516
|
Usage:
|
|
@@ -521,8 +557,8 @@ async function runCli(argv) {
|
|
|
521
557
|
printCommandHelp(command);
|
|
522
558
|
return;
|
|
523
559
|
}
|
|
524
|
-
if (!["dev", "init", "doctor", "firewall", "setup", "scan", "optimize", "context", "cc", "usage", "watch", "demo"].includes(command)) {
|
|
525
|
-
throw new Error(`Unknown command: ${command}. Try: prismo doctor, prismo watch, prismo firewall, prismo init, prismo scan, prismo optimize, prismo context, prismo cc, or prismo usage`);
|
|
560
|
+
if (!["dev", "init", "doctor", "firewall", "shield", "setup", "scan", "optimize", "context", "cc", "usage", "watch", "demo"].includes(command)) {
|
|
561
|
+
throw new Error(`Unknown command: ${command}. Try: prismo doctor, prismo watch, prismo shield, prismo firewall, prismo init, prismo scan, prismo optimize, prismo context, prismo cc, or prismo usage`);
|
|
526
562
|
}
|
|
527
563
|
|
|
528
564
|
if (command === "demo") {
|
|
@@ -604,6 +640,40 @@ async function runCli(argv) {
|
|
|
604
640
|
return;
|
|
605
641
|
}
|
|
606
642
|
|
|
643
|
+
if (command === "shield") {
|
|
644
|
+
const json = rest.includes("--json");
|
|
645
|
+
const limitIndex = rest.indexOf("--limit");
|
|
646
|
+
const limit = parsePositiveInt(limitIndex >= 0 ? rest[limitIndex + 1] : null, 5);
|
|
647
|
+
const positional = getPositionals(rest, new Set(["--limit"]));
|
|
648
|
+
if (positional[0] === "last") {
|
|
649
|
+
const target = positional[1] || process.cwd();
|
|
650
|
+
const result = runShieldLast(target, { limit });
|
|
651
|
+
if (json) console.log(JSON.stringify(result, null, 2));
|
|
652
|
+
else console.log(renderShieldLastTerminal(result));
|
|
653
|
+
return;
|
|
654
|
+
}
|
|
655
|
+
if (positional[0] === "search") {
|
|
656
|
+
const query = positional[1];
|
|
657
|
+
const target = positional[2] || process.cwd();
|
|
658
|
+
const result = runShieldSearch(target, query, { limit });
|
|
659
|
+
if (json) console.log(JSON.stringify(result, null, 2));
|
|
660
|
+
else console.log(renderShieldSearchTerminal(result));
|
|
661
|
+
return;
|
|
662
|
+
}
|
|
663
|
+
const separatorIndex = rest.indexOf("--");
|
|
664
|
+
const beforeSeparator = separatorIndex >= 0 ? rest.slice(0, separatorIndex) : [];
|
|
665
|
+
const commandArgs = separatorIndex >= 0 ? rest.slice(separatorIndex + 1) : getPositionals(rest);
|
|
666
|
+
const target = getPositionals(beforeSeparator)[0] || process.cwd();
|
|
667
|
+
const result = runShield(target, commandArgs);
|
|
668
|
+
if (json) {
|
|
669
|
+
console.log(JSON.stringify(result, null, 2));
|
|
670
|
+
} else {
|
|
671
|
+
console.log(renderShieldTerminal(result));
|
|
672
|
+
}
|
|
673
|
+
process.exitCode = result.exitCode;
|
|
674
|
+
return;
|
|
675
|
+
}
|
|
676
|
+
|
|
607
677
|
if (command === "setup") {
|
|
608
678
|
const json = rest.includes("--json");
|
|
609
679
|
const limitIndex = rest.indexOf("--limit");
|
package/package.json
CHANGED