getprismo 0.1.4 → 0.1.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/NOTICE +16 -0
- package/README.md +614 -88
- package/docs/prismodev-user-testing.md +10 -10
- package/lib/prismo-dev/constants.js +173 -0
- package/lib/prismo-dev/context-optimize.js +629 -0
- package/lib/prismo-dev/doctor.js +453 -0
- package/lib/prismo-dev/firewall.js +215 -0
- package/lib/prismo-dev/fixes.js +109 -0
- package/lib/prismo-dev/report.js +360 -0
- package/lib/prismo-dev/scan.js +1126 -0
- package/lib/prismo-dev/usage-watch.js +1852 -0
- package/lib/prismo-dev-scan.js +468 -2685
- package/package.json +24 -7
|
@@ -0,0 +1,453 @@
|
|
|
1
|
+
module.exports = function createDoctor(deps) {
|
|
2
|
+
const {
|
|
3
|
+
fs,
|
|
4
|
+
path,
|
|
5
|
+
NPX_COMMAND,
|
|
6
|
+
color,
|
|
7
|
+
applyFixes,
|
|
8
|
+
calculateReductionPercent,
|
|
9
|
+
chooseRecommendedScope,
|
|
10
|
+
createOptimizeContext,
|
|
11
|
+
estimateExposedContextTokens,
|
|
12
|
+
formatTokenCount,
|
|
13
|
+
getContextFileForScope,
|
|
14
|
+
getNextCommands,
|
|
15
|
+
getTopTokenLeaks,
|
|
16
|
+
renderContextCommand,
|
|
17
|
+
renderStarterPrompt,
|
|
18
|
+
runOptimize,
|
|
19
|
+
safeReadJson,
|
|
20
|
+
scanRepo,
|
|
21
|
+
writeGeneratedFile,
|
|
22
|
+
backupIfExists,
|
|
23
|
+
printStep,
|
|
24
|
+
} = deps;
|
|
25
|
+
|
|
26
|
+
function createDemoResult() {
|
|
27
|
+
const issues = [
|
|
28
|
+
{
|
|
29
|
+
severity: "critical",
|
|
30
|
+
category: "repo_size",
|
|
31
|
+
title: "Recent local AI sessions used 2.40M tokens",
|
|
32
|
+
description: "Prismo found exact token counts in local coding-agent logs.",
|
|
33
|
+
recommendation: "Use compact context packs and split long sessions at task boundaries.",
|
|
34
|
+
estimatedTokenImpact: "Actual local usage observed: 2,400,000 tokens across 3 recent sessions.",
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
severity: "high",
|
|
38
|
+
category: "large_file",
|
|
39
|
+
title: "Large exposed file detected",
|
|
40
|
+
description: "logs/debug-output.json (6.8 MB)",
|
|
41
|
+
recommendation: "Ignore or summarize large logs before loading them into an agent.",
|
|
42
|
+
estimatedTokenImpact: "Likely avoidable token exposure: up to ~1,700,000 tokens if read into context.",
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
severity: "medium",
|
|
46
|
+
category: "instruction_file",
|
|
47
|
+
title: "CLAUDE.md is ~1,900 tokens",
|
|
48
|
+
description: "Persistent instructions are larger than the recommended baseline.",
|
|
49
|
+
recommendation: "Trim CLAUDE.md under 500 tokens and link to details only when needed.",
|
|
50
|
+
estimatedTokenImpact: "Potential savings estimate: about 1,400 persistent instruction tokens per turn.",
|
|
51
|
+
},
|
|
52
|
+
];
|
|
53
|
+
return {
|
|
54
|
+
root: "/demo/prismo-app",
|
|
55
|
+
score: 58,
|
|
56
|
+
risk: "Medium",
|
|
57
|
+
avoidableWaste: "20-40%",
|
|
58
|
+
issues,
|
|
59
|
+
recommendations: [
|
|
60
|
+
"Create .claudeignore with generated/cache folders and large artifacts excluded.",
|
|
61
|
+
"Run `prismo optimize` to generate compact context files.",
|
|
62
|
+
"Start coding sessions from `.prismo/architecture-summary.md` instead of broad repo exploration.",
|
|
63
|
+
"Split long-running coding sessions at task boundaries.",
|
|
64
|
+
],
|
|
65
|
+
realUsage: {
|
|
66
|
+
tool: "all",
|
|
67
|
+
confidence: "exact-local-log",
|
|
68
|
+
totals: { sessions: 3, displayTokens: 2400000, estimatedTokens: 180000, exactTokens: 2400000, toolTokens: 95000 },
|
|
69
|
+
sessions: [{}, {}, {}],
|
|
70
|
+
},
|
|
71
|
+
stats: { totalFiles: 842, sourceFiles: 318, largeFiles: 4, exposedLargeFiles: 2, highRiskDirs: 7, exposedHighRiskDirs: 3 },
|
|
72
|
+
agentReadiness: {
|
|
73
|
+
claudeCode: { detected: true, localLogsFound: true, mcpServers: 4, hooks: 2, exactProxyTracking: "limited-for-subscription-mode" },
|
|
74
|
+
codex: { detected: true, localLogsFound: true, mcpServers: 1, exactProxyTracking: "available-when-using-api-key-base-url-mode" },
|
|
75
|
+
cursor: { detected: false, exactProxyTracking: "available-only-if-configured-for-openai-compatible-base-url" },
|
|
76
|
+
localUsageLogsAvailable: true,
|
|
77
|
+
},
|
|
78
|
+
optimizationStack: {
|
|
79
|
+
tools: {
|
|
80
|
+
rtk: { detected: false },
|
|
81
|
+
headroom: { detected: false },
|
|
82
|
+
distill: { detected: false },
|
|
83
|
+
mana: { detected: false },
|
|
84
|
+
},
|
|
85
|
+
claudeHooks: 2,
|
|
86
|
+
mcpServerTotal: 5,
|
|
87
|
+
},
|
|
88
|
+
toolOutputRisk: {
|
|
89
|
+
level: "High",
|
|
90
|
+
summary: "Large logs, test reports, build output, or generated files are exposed to coding-agent reads.",
|
|
91
|
+
exposedNoisyDirectories: ["logs", "coverage"],
|
|
92
|
+
exposedNoisyFiles: [{ path: "logs/debug-output.json" }],
|
|
93
|
+
},
|
|
94
|
+
proxyTrackingReadiness: {
|
|
95
|
+
codingAgentBaseUrlMode: {
|
|
96
|
+
codex: "possible-if-using-api-key-mode",
|
|
97
|
+
claudeCode: "limited-for-subscription-sessions",
|
|
98
|
+
},
|
|
99
|
+
},
|
|
100
|
+
topTokenLeaks: getTopTokenLeaks(issues),
|
|
101
|
+
hasClaudeIgnore: false,
|
|
102
|
+
repoDetected: true,
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function renderDemoTerminal() {
|
|
107
|
+
return [
|
|
108
|
+
color("PrismoDev Product Loop", "bold"),
|
|
109
|
+
"",
|
|
110
|
+
"Before session",
|
|
111
|
+
`${NPX_COMMAND} doctor`,
|
|
112
|
+
"- Diagnose token/context waste",
|
|
113
|
+
"- Apply safe ignore/context fixes",
|
|
114
|
+
"- Show before/after score",
|
|
115
|
+
"",
|
|
116
|
+
"During session",
|
|
117
|
+
`${NPX_COMMAND} watch`,
|
|
118
|
+
"- Context Pressure: HIGH",
|
|
119
|
+
"- Recent Growth: +380k",
|
|
120
|
+
"- package-lock.json likely entered context",
|
|
121
|
+
"- Suggested Action: start from a scoped context pack",
|
|
122
|
+
"",
|
|
123
|
+
"After session",
|
|
124
|
+
`${NPX_COMMAND} cc timeline`,
|
|
125
|
+
"- Context spikes",
|
|
126
|
+
"- Repeated command loops",
|
|
127
|
+
"- Artifact leaks",
|
|
128
|
+
"- Better next action",
|
|
129
|
+
"",
|
|
130
|
+
"Local-only: PrismoDev reads repo files and local coding-agent logs. It does not upload data.",
|
|
131
|
+
"",
|
|
132
|
+
"Try it on your repo:",
|
|
133
|
+
`1. ${NPX_COMMAND} doctor`,
|
|
134
|
+
`2. ${NPX_COMMAND} watch --once`,
|
|
135
|
+
`3. ${NPX_COMMAND} cc timeline`,
|
|
136
|
+
].join("\n");
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function runDevFlow(rootDir = process.cwd(), options = {}) {
|
|
140
|
+
const root = path.resolve(rootDir);
|
|
141
|
+
const scanDone = printStep("Scanning repo and local usage", options.json);
|
|
142
|
+
const scan = scanRepo(root, { includeUsage: true, usageLimit: options.limit || 3 });
|
|
143
|
+
scanDone();
|
|
144
|
+
const initialContext = createOptimizeContext(root);
|
|
145
|
+
const scope = initialContext.frameworks.some((name) => ["Next.js", "React", "Vite"].includes(name)) ? "frontend" : null;
|
|
146
|
+
const optimizeDone = printStep("Generating compact context files", options.json);
|
|
147
|
+
const optimize = runOptimize(root, { scope });
|
|
148
|
+
optimizeDone();
|
|
149
|
+
const ctx = createOptimizeContext(root, scope);
|
|
150
|
+
const prompt = renderContextCommand(ctx, scope);
|
|
151
|
+
return {
|
|
152
|
+
scan,
|
|
153
|
+
optimize,
|
|
154
|
+
scope,
|
|
155
|
+
prompt,
|
|
156
|
+
nextCommands: getNextCommands(scan, scope),
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function runDoctor(rootDir = process.cwd(), options = {}) {
|
|
161
|
+
const root = path.resolve(rootDir);
|
|
162
|
+
const scanDone = printStep("Scanning repo and local usage", options.json);
|
|
163
|
+
const before = scanRepo(root, {
|
|
164
|
+
includeUsage: true,
|
|
165
|
+
usageLimit: options.limit || 3,
|
|
166
|
+
usageTool: options.usageTool || "all",
|
|
167
|
+
});
|
|
168
|
+
scanDone();
|
|
169
|
+
|
|
170
|
+
const fixDone = printStep(options.dryRun ? "Planning safe AI-context fixes" : "Applying safe AI-context fixes", options.json);
|
|
171
|
+
const applyIgnoresOnly = Boolean(options.applyIgnoresOnly);
|
|
172
|
+
const noContextPacks = Boolean(options.noContextPacks || applyIgnoresOnly);
|
|
173
|
+
const fixActions = applyFixes(before, { dryRun: options.dryRun, ignoresOnly: applyIgnoresOnly });
|
|
174
|
+
fixDone();
|
|
175
|
+
|
|
176
|
+
const initialContext = createOptimizeContext(root);
|
|
177
|
+
const scope = options.scope || chooseRecommendedScope(initialContext);
|
|
178
|
+
let optimize = { root, scope, generatedFiles: [], skipped: true };
|
|
179
|
+
if (!noContextPacks) {
|
|
180
|
+
const optimizeDone = printStep(options.dryRun ? "Planning compact context packs" : "Generating compact context packs", options.json);
|
|
181
|
+
optimize = runOptimize(root, { scope, dryRun: options.dryRun });
|
|
182
|
+
optimizeDone();
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
let after = before;
|
|
186
|
+
if (!options.dryRun) {
|
|
187
|
+
const rescanDone = printStep("Re-scanning optimized repo", options.json);
|
|
188
|
+
after = scanRepo(root, {
|
|
189
|
+
includeUsage: true,
|
|
190
|
+
usageLimit: options.limit || 3,
|
|
191
|
+
usageTool: options.usageTool || "all",
|
|
192
|
+
});
|
|
193
|
+
rescanDone();
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const ctx = createOptimizeContext(root, scope);
|
|
197
|
+
const beforeExposedTokens = estimateExposedContextTokens(before);
|
|
198
|
+
const afterExposedTokens = estimateExposedContextTokens(after);
|
|
199
|
+
const exposedTokenReductionPercent = calculateReductionPercent(beforeExposedTokens, afterExposedTokens);
|
|
200
|
+
const contextCommand = `${NPX_COMMAND} context${scope ? ` ${scope}` : ""}`;
|
|
201
|
+
|
|
202
|
+
return {
|
|
203
|
+
root,
|
|
204
|
+
before,
|
|
205
|
+
after,
|
|
206
|
+
scope,
|
|
207
|
+
fixActions,
|
|
208
|
+
optimize,
|
|
209
|
+
generatedFiles: optimize.generatedFiles,
|
|
210
|
+
beforeExposedTokens,
|
|
211
|
+
afterExposedTokens,
|
|
212
|
+
exposedTokenReductionPercent,
|
|
213
|
+
contextFile: getContextFileForScope(ctx, scope),
|
|
214
|
+
contextCommand,
|
|
215
|
+
starterPrompt: renderStarterPrompt(ctx, scope),
|
|
216
|
+
nextCommands: Array.from(new Set([
|
|
217
|
+
contextCommand,
|
|
218
|
+
`${NPX_COMMAND} watch --auto`,
|
|
219
|
+
before.realUsage && before.realUsage.sessions.length ? `${NPX_COMMAND} cc` : `${NPX_COMMAND} scan --usage`,
|
|
220
|
+
])),
|
|
221
|
+
dryRun: Boolean(options.dryRun),
|
|
222
|
+
applyIgnoresOnly,
|
|
223
|
+
noContextPacks,
|
|
224
|
+
generatedAt: new Date().toISOString(),
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function renderPrismoReadme() {
|
|
229
|
+
return [
|
|
230
|
+
"# PrismoDev",
|
|
231
|
+
"",
|
|
232
|
+
"Local AI coding workflow helpers for this repo.",
|
|
233
|
+
"",
|
|
234
|
+
"## Core Loop",
|
|
235
|
+
"",
|
|
236
|
+
"- Before a session: `npx getprismo doctor`",
|
|
237
|
+
"- During a session: `npx getprismo watch`",
|
|
238
|
+
"- After a session: `npx getprismo cc`",
|
|
239
|
+
"- For scoped context: `npx getprismo context <scope>`",
|
|
240
|
+
"",
|
|
241
|
+
"PrismoDev is local-first. It reads repo files and local coding-agent logs when available; it does not upload data.",
|
|
242
|
+
"",
|
|
243
|
+
].join("\n");
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function runInit(rootDir = process.cwd(), options = {}) {
|
|
247
|
+
const root = path.resolve(rootDir);
|
|
248
|
+
const actions = [];
|
|
249
|
+
if (!fs.existsSync(root) || !fs.statSync(root).isDirectory()) {
|
|
250
|
+
throw new Error(`Path not found: ${root}`);
|
|
251
|
+
}
|
|
252
|
+
const readmePath = path.join(root, ".prismo", "README.md");
|
|
253
|
+
if (options.dryRun) {
|
|
254
|
+
actions.push("Would create .prismo/README.md");
|
|
255
|
+
} else {
|
|
256
|
+
const written = writeGeneratedFile(root, path.join(".prismo", "README.md"), renderPrismoReadme());
|
|
257
|
+
actions.push(`Created ${written.path}`);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
const packagePath = path.join(root, "package.json");
|
|
261
|
+
if (fs.existsSync(packagePath)) {
|
|
262
|
+
const pkg = safeReadJson(packagePath);
|
|
263
|
+
if (pkg && typeof pkg === "object") {
|
|
264
|
+
pkg.scripts = pkg.scripts && typeof pkg.scripts === "object" ? pkg.scripts : {};
|
|
265
|
+
const scripts = {
|
|
266
|
+
"ai:doctor": "prismo doctor",
|
|
267
|
+
"ai:watch": "prismo watch",
|
|
268
|
+
"ai:context": "prismo context",
|
|
269
|
+
"ai:scan": "prismo scan --usage",
|
|
270
|
+
};
|
|
271
|
+
const added = [];
|
|
272
|
+
for (const [name, command] of Object.entries(scripts)) {
|
|
273
|
+
if (!pkg.scripts[name]) {
|
|
274
|
+
pkg.scripts[name] = command;
|
|
275
|
+
added.push(name);
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
if (added.length) {
|
|
279
|
+
if (options.dryRun) {
|
|
280
|
+
actions.push(`Would add npm scripts: ${added.join(", ")}`);
|
|
281
|
+
} else {
|
|
282
|
+
const backupPath = backupIfExists(packagePath);
|
|
283
|
+
fs.writeFileSync(packagePath, `${JSON.stringify(pkg, null, 2)}\n`, "utf8");
|
|
284
|
+
actions.push(`Added npm scripts: ${added.join(", ")}`);
|
|
285
|
+
if (backupPath) actions.push(`Backed up package.json to ${path.basename(backupPath)}`);
|
|
286
|
+
}
|
|
287
|
+
} else {
|
|
288
|
+
actions.push("Prismo npm scripts already exist");
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
} else {
|
|
292
|
+
actions.push("No package.json found; skipped npm scripts");
|
|
293
|
+
}
|
|
294
|
+
return {
|
|
295
|
+
root,
|
|
296
|
+
actions,
|
|
297
|
+
dryRun: Boolean(options.dryRun),
|
|
298
|
+
generatedAt: new Date().toISOString(),
|
|
299
|
+
};
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
function renderInitTerminal(result) {
|
|
303
|
+
const lines = [];
|
|
304
|
+
lines.push("");
|
|
305
|
+
lines.push("Prismo Init");
|
|
306
|
+
if (result.dryRun) lines.push("Mode: dry run (no files changed)");
|
|
307
|
+
lines.push("");
|
|
308
|
+
result.actions.forEach((action) => lines.push(`- ${action}`));
|
|
309
|
+
lines.push("");
|
|
310
|
+
lines.push("Next:");
|
|
311
|
+
lines.push(`${NPX_COMMAND} doctor`);
|
|
312
|
+
return lines.join("\n");
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
function toDoctorJsonPayload(result) {
|
|
316
|
+
const usage = result.before.realUsage;
|
|
317
|
+
const compactRealUsage = usage
|
|
318
|
+
? {
|
|
319
|
+
confidence: usage.confidence,
|
|
320
|
+
totals: usage.totals,
|
|
321
|
+
sources: usage.sources,
|
|
322
|
+
}
|
|
323
|
+
: null;
|
|
324
|
+
return {
|
|
325
|
+
schemaVersion: 1,
|
|
326
|
+
scannedPath: result.root,
|
|
327
|
+
before: {
|
|
328
|
+
score: result.before.score,
|
|
329
|
+
riskLevel: result.before.risk,
|
|
330
|
+
tokenLeaks: result.before.issues.length,
|
|
331
|
+
estimatedExposedContextTokens: result.beforeExposedTokens,
|
|
332
|
+
realUsage: compactRealUsage,
|
|
333
|
+
},
|
|
334
|
+
after: {
|
|
335
|
+
score: result.after.score,
|
|
336
|
+
riskLevel: result.after.risk,
|
|
337
|
+
tokenLeaks: result.after.issues.length,
|
|
338
|
+
estimatedExposedContextTokens: result.afterExposedTokens,
|
|
339
|
+
},
|
|
340
|
+
scoreDelta: result.after.score - result.before.score,
|
|
341
|
+
exposedTokenReductionPercent: result.exposedTokenReductionPercent,
|
|
342
|
+
fixActions: result.fixActions,
|
|
343
|
+
generatedFiles: result.generatedFiles,
|
|
344
|
+
filesCreatedOrPlanned: [...result.fixActions, ...result.generatedFiles.map((file) => `${result.dryRun ? "Would generate" : "Generated"} ${file}`)],
|
|
345
|
+
stillRisky: result.after.issues.slice(0, 5).map((issue) => ({
|
|
346
|
+
severity: issue.severity,
|
|
347
|
+
category: issue.category,
|
|
348
|
+
title: issue.title,
|
|
349
|
+
recommendation: issue.recommendation,
|
|
350
|
+
})),
|
|
351
|
+
scope: result.scope,
|
|
352
|
+
contextFile: result.contextFile,
|
|
353
|
+
contextCommand: result.contextCommand,
|
|
354
|
+
starterPrompt: result.starterPrompt,
|
|
355
|
+
nextCommands: result.nextCommands,
|
|
356
|
+
dryRun: result.dryRun,
|
|
357
|
+
applyIgnoresOnly: result.applyIgnoresOnly,
|
|
358
|
+
noContextPacks: result.noContextPacks,
|
|
359
|
+
generatedAt: result.generatedAt,
|
|
360
|
+
};
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
function renderDoctorTerminal(result) {
|
|
364
|
+
const scoreDelta = result.after.score - result.before.score;
|
|
365
|
+
const scoreDeltaLabel = scoreDelta > 0 ? `+${scoreDelta}` : String(scoreDelta);
|
|
366
|
+
const lines = [];
|
|
367
|
+
lines.push("");
|
|
368
|
+
lines.push(color("PrismoDev Doctor", "bold"));
|
|
369
|
+
if (result.dryRun) lines.push("Mode: dry run (no files changed)");
|
|
370
|
+
lines.push("");
|
|
371
|
+
lines.push(`Before: ${result.before.score}/100 - ${result.before.risk} risk - ${result.before.issues.length} token leak${result.before.issues.length === 1 ? "" : "s"}`);
|
|
372
|
+
if (result.dryRun) lines.push("After: run without --dry-run to apply safe fixes and re-scan");
|
|
373
|
+
else lines.push(`After: ${result.after.score}/100 - ${result.after.risk} risk - ${result.after.issues.length} token leak${result.after.issues.length === 1 ? "" : "s"} (${scoreDeltaLabel})`);
|
|
374
|
+
if (result.before.realUsage && result.before.realUsage.sessions.length) {
|
|
375
|
+
lines.push(`Local usage: ${formatTokenCount(result.before.realUsage.totals.displayTokens)} tokens across ${result.before.realUsage.sessions.length} recent session(s)`);
|
|
376
|
+
}
|
|
377
|
+
lines.push(`Estimated exposed context reduction: ${result.exposedTokenReductionPercent}%`);
|
|
378
|
+
if (!result.dryRun) {
|
|
379
|
+
lines.push(`Payoff: ${scoreDelta > 0 ? `repo is ${scoreDelta} points cleaner for AI coding sessions` : "safe fixes applied; remaining risk needs manual cleanup"}`);
|
|
380
|
+
}
|
|
381
|
+
lines.push("");
|
|
382
|
+
lines.push(result.dryRun ? "Would Fix:" : "Fixed:");
|
|
383
|
+
if (result.fixActions.length) result.fixActions.forEach((action) => lines.push(`- ${action}`));
|
|
384
|
+
else lines.push("- No safe fix files needed");
|
|
385
|
+
if (result.noContextPacks) {
|
|
386
|
+
lines.push("- Context pack generation skipped");
|
|
387
|
+
} else {
|
|
388
|
+
result.generatedFiles.slice(0, 8).forEach((file) => lines.push(`- ${result.dryRun ? "Would generate" : "Generated"} ${file}`));
|
|
389
|
+
if (result.generatedFiles.length > 8) lines.push(`- ${result.dryRun ? "Would generate" : "Generated"} ${result.generatedFiles.length - 8} more .prismo file(s)`);
|
|
390
|
+
}
|
|
391
|
+
const stillRisky = result.after.issues.slice(0, 3);
|
|
392
|
+
if (!result.dryRun && stillRisky.length) {
|
|
393
|
+
lines.push("");
|
|
394
|
+
lines.push("Still Risky:");
|
|
395
|
+
stillRisky.forEach((issue) => lines.push(`- ${issue.title} -> ${issue.recommendation}`));
|
|
396
|
+
}
|
|
397
|
+
if (!result.dryRun && !stillRisky.length) {
|
|
398
|
+
lines.push("");
|
|
399
|
+
lines.push("Still Risky:");
|
|
400
|
+
lines.push("- No major token-waste risks remain.");
|
|
401
|
+
}
|
|
402
|
+
lines.push("");
|
|
403
|
+
lines.push("Recommended starting context:");
|
|
404
|
+
lines.push(result.noContextPacks ? "Run doctor again without --no-context-packs when you want .prismo context files." : result.contextFile);
|
|
405
|
+
lines.push("");
|
|
406
|
+
lines.push("Next:");
|
|
407
|
+
result.nextCommands.forEach((command, index) => lines.push(`${index + 1}. ${command}`));
|
|
408
|
+
lines.push("");
|
|
409
|
+
lines.push("Next live session:");
|
|
410
|
+
lines.push(`${NPX_COMMAND} watch --auto`);
|
|
411
|
+
lines.push("Tell your agent: Follow .prismo/live-guardrails.md during this session.");
|
|
412
|
+
lines.push("");
|
|
413
|
+
lines.push("Doctor only creates safe recommendation/context files. It does not overwrite CLAUDE.md, AGENTS.md, .gitignore, or source code.");
|
|
414
|
+
return lines.join("\n");
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
function renderDevTerminal(result) {
|
|
418
|
+
const lines = [];
|
|
419
|
+
lines.push("");
|
|
420
|
+
lines.push(color("PrismoDev", "bold"));
|
|
421
|
+
lines.push("");
|
|
422
|
+
lines.push(`Score: ${result.scan.score}/100 | Risk: ${result.scan.risk} | Token leaks: ${result.scan.issues.length}`);
|
|
423
|
+
if (result.scan.realUsage && result.scan.realUsage.sessions.length) {
|
|
424
|
+
lines.push(`Real local usage: ${formatTokenCount(result.scan.realUsage.totals.displayTokens)} tokens (${result.scan.realUsage.confidence})`);
|
|
425
|
+
} else if (result.scan.realUsage) {
|
|
426
|
+
lines.push("Real local usage: no matching local sessions found for this repo");
|
|
427
|
+
}
|
|
428
|
+
lines.push("");
|
|
429
|
+
lines.push("Generated:");
|
|
430
|
+
result.optimize.generatedFiles.slice(0, 8).forEach((file) => lines.push(`- ${file}`));
|
|
431
|
+
lines.push("");
|
|
432
|
+
lines.push("Next Commands:");
|
|
433
|
+
result.nextCommands.forEach((cmd, index) => lines.push(`${index + 1}. ${cmd}`));
|
|
434
|
+
lines.push("");
|
|
435
|
+
lines.push("Paste-ready prompt:");
|
|
436
|
+
lines.push(renderStarterPrompt(createOptimizeContext(result.optimize.root, result.scope), result.scope));
|
|
437
|
+
lines.push("");
|
|
438
|
+
return lines.join("\n");
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
return {
|
|
442
|
+
createDemoResult,
|
|
443
|
+
renderDemoTerminal,
|
|
444
|
+
renderDevTerminal,
|
|
445
|
+
renderDoctorTerminal,
|
|
446
|
+
renderInitTerminal,
|
|
447
|
+
renderPrismoReadme,
|
|
448
|
+
runDevFlow,
|
|
449
|
+
runDoctor,
|
|
450
|
+
runInit,
|
|
451
|
+
toDoctorJsonPayload,
|
|
452
|
+
};
|
|
453
|
+
};
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
module.exports = function createFirewall(deps) {
|
|
2
|
+
const {
|
|
3
|
+
fs,
|
|
4
|
+
path,
|
|
5
|
+
NPX_COMMAND,
|
|
6
|
+
createOptimizeContext,
|
|
7
|
+
writeGeneratedFile,
|
|
8
|
+
} = deps;
|
|
9
|
+
|
|
10
|
+
function uniq(items) {
|
|
11
|
+
return Array.from(new Set((items || []).filter(Boolean)));
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function inferTaskScope(task = "", ctx) {
|
|
15
|
+
const text = String(task || "").toLowerCase();
|
|
16
|
+
if (/auth|login|session|oauth|jwt|middleware/.test(text)) return "auth";
|
|
17
|
+
if (/front|ui|react|next|page|component|css|style/.test(text)) return "frontend";
|
|
18
|
+
if (/back|api|server|route|db|database|model|schema|worker/.test(text)) return "backend";
|
|
19
|
+
if (ctx.scope) return ctx.scope;
|
|
20
|
+
if (ctx.frontendDetected && !ctx.backendDetected) return "frontend";
|
|
21
|
+
if (ctx.backendDetected && !ctx.frontendDetected) return "backend";
|
|
22
|
+
return "general";
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function globForFile(rel) {
|
|
26
|
+
const normalized = String(rel || "").replace(/\\/g, "/");
|
|
27
|
+
if (!normalized || !normalized.includes("/")) return normalized;
|
|
28
|
+
const parts = normalized.split("/");
|
|
29
|
+
if (parts.length <= 2) return normalized;
|
|
30
|
+
return `${parts.slice(0, -1).join("/")}/*`;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function buildAllowedContext(ctx, taskScope) {
|
|
34
|
+
const allowed = [];
|
|
35
|
+
allowed.push(".prismo/architecture-summary.md");
|
|
36
|
+
allowed.push("package.json");
|
|
37
|
+
allowed.push("README.md");
|
|
38
|
+
if (ctx.frameworks.includes("TypeScript")) allowed.push("tsconfig.json");
|
|
39
|
+
|
|
40
|
+
if (taskScope === "frontend" || taskScope === "general") {
|
|
41
|
+
allowed.push(".prismo/frontend-summary.md");
|
|
42
|
+
allowed.push(...ctx.frontend.app.slice(0, 8).map(globForFile));
|
|
43
|
+
allowed.push(...ctx.frontend.components.slice(0, 8).map(globForFile));
|
|
44
|
+
allowed.push(...ctx.frontend.apiClient.slice(0, 6).map(globForFile));
|
|
45
|
+
allowed.push(...ctx.frontend.styling.slice(0, 4).map(globForFile));
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (taskScope === "backend" || taskScope === "general") {
|
|
49
|
+
allowed.push(".prismo/backend-summary.md");
|
|
50
|
+
allowed.push(...ctx.backend.api.slice(0, 8).map(globForFile));
|
|
51
|
+
allowed.push(...ctx.backend.services.slice(0, 8).map(globForFile));
|
|
52
|
+
allowed.push(...ctx.backend.models.slice(0, 6).map(globForFile));
|
|
53
|
+
allowed.push(...ctx.backend.config.slice(0, 4).map(globForFile));
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (taskScope === "auth") {
|
|
57
|
+
allowed.push(".prismo/backend-summary.md");
|
|
58
|
+
allowed.push(".prismo/frontend-summary.md");
|
|
59
|
+
allowed.push(...ctx.backend.auth.slice(0, 10).map(globForFile));
|
|
60
|
+
allowed.push(...ctx.backend.api.filter((p) => /auth|user|session|account/i.test(p)).slice(0, 8).map(globForFile));
|
|
61
|
+
allowed.push(...ctx.frontend.app.filter((p) => /auth|login|account|dashboard|middleware/i.test(p)).slice(0, 8).map(globForFile));
|
|
62
|
+
allowed.push(...ctx.frontend.apiClient.filter((p) => /auth|user|session|account/i.test(p)).slice(0, 8).map(globForFile));
|
|
63
|
+
allowed.push("middleware.ts");
|
|
64
|
+
allowed.push("src/middleware.ts");
|
|
65
|
+
allowed.push("frontend/src/middleware.ts");
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return uniq(allowed).slice(0, 60);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function buildBlockedContext(ctx) {
|
|
72
|
+
const blocked = [
|
|
73
|
+
"node_modules/**",
|
|
74
|
+
".next/**",
|
|
75
|
+
"dist/**",
|
|
76
|
+
"build/**",
|
|
77
|
+
"coverage/**",
|
|
78
|
+
"playwright-report/**",
|
|
79
|
+
"test-results/**",
|
|
80
|
+
"logs/**",
|
|
81
|
+
"**/*.log",
|
|
82
|
+
"**/*.map",
|
|
83
|
+
"**/__pycache__/**",
|
|
84
|
+
"package-lock.json",
|
|
85
|
+
"yarn.lock",
|
|
86
|
+
"pnpm-lock.yaml",
|
|
87
|
+
"bun.lockb",
|
|
88
|
+
];
|
|
89
|
+
for (const dir of ctx.scan.exposedHighRiskDirs || []) blocked.push(`${dir.path}/**`);
|
|
90
|
+
for (const file of ctx.scan.exposedLargeFiles || []) blocked.push(file.path);
|
|
91
|
+
return uniq(blocked).slice(0, 80);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function renderLines(title, items) {
|
|
95
|
+
return [`# ${title}`, "", ...items.map((item) => item)].join("\n") + "\n";
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function renderFirewallPolicy(result) {
|
|
99
|
+
return [
|
|
100
|
+
"# Prismo Context Firewall",
|
|
101
|
+
"",
|
|
102
|
+
`Generated: ${result.generatedAt}`,
|
|
103
|
+
`Task: ${result.task || "general"}`,
|
|
104
|
+
`Scope: ${result.scope}`,
|
|
105
|
+
"",
|
|
106
|
+
"This is an AI coding context policy. It does not enforce filesystem access by itself; give it to your agent and require the agent to follow it before reading files.",
|
|
107
|
+
"",
|
|
108
|
+
"## Allowed Context",
|
|
109
|
+
"",
|
|
110
|
+
...result.allowed.map((item) => `- ${item}`),
|
|
111
|
+
"",
|
|
112
|
+
"## Blocked Unless Explicitly Justified",
|
|
113
|
+
"",
|
|
114
|
+
...result.blocked.map((item) => `- ${item}`),
|
|
115
|
+
"",
|
|
116
|
+
"## Rules",
|
|
117
|
+
"",
|
|
118
|
+
"- Start from allowed context only.",
|
|
119
|
+
"- If another file is needed, explain why before reading it.",
|
|
120
|
+
"- Prefer summaries and targeted ranges over full-file reads.",
|
|
121
|
+
"- Do not read blocked paths unless the user explicitly asks or the task cannot proceed without them.",
|
|
122
|
+
"- If context pressure gets high, stop and run `npx getprismo watch --auto`.",
|
|
123
|
+
"",
|
|
124
|
+
].join("\n");
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function renderFirewallPrompt(result) {
|
|
128
|
+
return [
|
|
129
|
+
"# Prismo Firewall Prompt",
|
|
130
|
+
"",
|
|
131
|
+
"Use this at the start of an AI coding session:",
|
|
132
|
+
"",
|
|
133
|
+
"```text",
|
|
134
|
+
`Follow .prismo/context-firewall.md for this ${result.task || result.scope} task.`,
|
|
135
|
+
"Only read allowed context first.",
|
|
136
|
+
"Do not read blocked paths unless you explain why they are required.",
|
|
137
|
+
"Keep command output short and summarize before expanding context.",
|
|
138
|
+
"If you need more context, ask for approval or name the exact file and reason.",
|
|
139
|
+
"```",
|
|
140
|
+
"",
|
|
141
|
+
].join("\n");
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function runFirewall(rootDir = process.cwd(), options = {}) {
|
|
145
|
+
const ctx = createOptimizeContext(rootDir, options.scope || null);
|
|
146
|
+
const task = options.task || options.scope || "general";
|
|
147
|
+
const scope = inferTaskScope(task, ctx);
|
|
148
|
+
const allowed = buildAllowedContext(ctx, scope);
|
|
149
|
+
const blocked = buildBlockedContext(ctx);
|
|
150
|
+
const result = {
|
|
151
|
+
root: ctx.root,
|
|
152
|
+
task,
|
|
153
|
+
scope,
|
|
154
|
+
allowed,
|
|
155
|
+
blocked,
|
|
156
|
+
generatedAt: new Date().toISOString(),
|
|
157
|
+
generatedFiles: [
|
|
158
|
+
".prismo/context-firewall.md",
|
|
159
|
+
".prismo/allowed-context.txt",
|
|
160
|
+
".prismo/blocked-context.txt",
|
|
161
|
+
".prismo/firewall-prompt.md",
|
|
162
|
+
],
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
if (!options.dryRun) {
|
|
166
|
+
const write = (relPath, contents) => {
|
|
167
|
+
if (!options.live) return writeGeneratedFile(ctx.root, relPath, contents);
|
|
168
|
+
const fullPath = path.join(ctx.root, relPath);
|
|
169
|
+
fs.mkdirSync(path.dirname(fullPath), { recursive: true });
|
|
170
|
+
fs.writeFileSync(fullPath, contents, "utf8");
|
|
171
|
+
return { path: relPath, backupPath: null };
|
|
172
|
+
};
|
|
173
|
+
write(".prismo/context-firewall.md", renderFirewallPolicy(result));
|
|
174
|
+
write(".prismo/allowed-context.txt", renderLines("Allowed Context", allowed));
|
|
175
|
+
write(".prismo/blocked-context.txt", renderLines("Blocked Context", blocked));
|
|
176
|
+
write(".prismo/firewall-prompt.md", renderFirewallPrompt(result));
|
|
177
|
+
}
|
|
178
|
+
result.dryRun = Boolean(options.dryRun);
|
|
179
|
+
return result;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function renderFirewallTerminal(result) {
|
|
183
|
+
const lines = [];
|
|
184
|
+
lines.push("");
|
|
185
|
+
lines.push("Prismo Context Firewall");
|
|
186
|
+
lines.push("");
|
|
187
|
+
lines.push(`Task: ${result.task}`);
|
|
188
|
+
lines.push(`Scope: ${result.scope}`);
|
|
189
|
+
lines.push("");
|
|
190
|
+
lines.push(result.dryRun ? "Would write:" : "Wrote:");
|
|
191
|
+
result.generatedFiles.forEach((file) => lines.push(`- ${file}`));
|
|
192
|
+
lines.push("");
|
|
193
|
+
lines.push("Allowed first:");
|
|
194
|
+
result.allowed.slice(0, 10).forEach((item) => lines.push(`- ${item}`));
|
|
195
|
+
if (result.allowed.length > 10) lines.push(`- ${result.allowed.length - 10} more`);
|
|
196
|
+
lines.push("");
|
|
197
|
+
lines.push("Blocked unless justified:");
|
|
198
|
+
result.blocked.slice(0, 10).forEach((item) => lines.push(`- ${item}`));
|
|
199
|
+
if (result.blocked.length > 10) lines.push(`- ${result.blocked.length - 10} more`);
|
|
200
|
+
lines.push("");
|
|
201
|
+
lines.push("Tell your agent:");
|
|
202
|
+
lines.push("Follow .prismo/context-firewall.md before reading files.");
|
|
203
|
+
lines.push("");
|
|
204
|
+
return lines.join("\n");
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
return {
|
|
208
|
+
buildAllowedContext,
|
|
209
|
+
buildBlockedContext,
|
|
210
|
+
renderFirewallPolicy,
|
|
211
|
+
renderFirewallPrompt,
|
|
212
|
+
renderFirewallTerminal,
|
|
213
|
+
runFirewall,
|
|
214
|
+
};
|
|
215
|
+
};
|