getprismo 0.1.2 → 0.1.3
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 +43 -9
- package/lib/prismo-dev-scan.js +622 -26
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
|
-
#
|
|
1
|
+
# PrismoDev
|
|
2
2
|
|
|
3
3
|
Prismo helps developers find and reduce token waste in AI coding workflows. It runs locally, supports Codex and Claude Code, and does not require API keys.
|
|
4
4
|
|
|
5
5
|
```bash
|
|
6
|
-
npx getprismo
|
|
6
|
+
npx getprismo scan --usage
|
|
7
|
+
npx getprismo setup
|
|
8
|
+
npx getprismo watch
|
|
7
9
|
```
|
|
8
10
|
|
|
9
11
|
## What It Does
|
|
@@ -17,20 +19,29 @@ npx getprismo dev
|
|
|
17
19
|
## Quick Start
|
|
18
20
|
|
|
19
21
|
```bash
|
|
20
|
-
npx getprismo
|
|
21
|
-
npx getprismo
|
|
22
|
+
npx getprismo scan --usage
|
|
23
|
+
npx getprismo setup
|
|
24
|
+
npx getprismo watch --once
|
|
22
25
|
```
|
|
23
26
|
|
|
24
|
-
|
|
27
|
+
Use this flow to see value immediately:
|
|
28
|
+
|
|
29
|
+
1. `scan --usage` finds repo/context risks and reads local Codex/Claude Code usage logs when available.
|
|
30
|
+
2. `setup` shows which tracking modes are possible, including Prismo proxy readiness.
|
|
31
|
+
3. `watch --once` shows the current live local session view with warnings and next action.
|
|
25
32
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
33
|
+
For a guided scan + context generation flow, run:
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
npx getprismo dev
|
|
37
|
+
```
|
|
29
38
|
|
|
30
39
|
## Common Commands
|
|
31
40
|
|
|
32
41
|
```bash
|
|
33
42
|
npx getprismo scan --usage
|
|
43
|
+
npx getprismo setup
|
|
44
|
+
npx getprismo watch
|
|
34
45
|
npx getprismo scan --fix
|
|
35
46
|
npx getprismo optimize
|
|
36
47
|
npx getprismo context frontend
|
|
@@ -41,7 +52,7 @@ npx getprismo usage claude
|
|
|
41
52
|
## Example Output
|
|
42
53
|
|
|
43
54
|
```text
|
|
44
|
-
|
|
55
|
+
PrismoDev
|
|
45
56
|
|
|
46
57
|
Score: 72/100 | Risk: Medium | Token leaks: 5
|
|
47
58
|
Estimated avoidable waste: 20-40%
|
|
@@ -59,6 +70,27 @@ Then: npx getprismo optimize
|
|
|
59
70
|
Then: npx getprismo context frontend
|
|
60
71
|
```
|
|
61
72
|
|
|
73
|
+
## Setup And Live Watch
|
|
74
|
+
|
|
75
|
+
```bash
|
|
76
|
+
npx getprismo setup
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
Setup is read-only. It detects Claude Code, Codex, Cursor, local logs, MCP/tool config, and whether the Prismo proxy is reachable for exact API tracking.
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
npx getprismo watch
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
Watch is the local live session view. It shows active session tokens, context risk, tool/output token spikes, largest context sources, top tools, warnings, and the next recommended action.
|
|
86
|
+
|
|
87
|
+
For machine-readable output:
|
|
88
|
+
|
|
89
|
+
```bash
|
|
90
|
+
npx getprismo setup --json
|
|
91
|
+
npx getprismo watch --once --json
|
|
92
|
+
```
|
|
93
|
+
|
|
62
94
|
## Local Usage Tracking
|
|
63
95
|
|
|
64
96
|
Prismo can show real token usage when local tool logs expose token fields.
|
|
@@ -105,6 +137,8 @@ Existing reports and suggestion files are backed up before replacement.
|
|
|
105
137
|
```bash
|
|
106
138
|
npx getprismo --help
|
|
107
139
|
npx getprismo scan --help
|
|
140
|
+
npx getprismo setup --help
|
|
141
|
+
npx getprismo watch --help
|
|
108
142
|
npx getprismo optimize --help
|
|
109
143
|
npx getprismo usage --help
|
|
110
144
|
```
|
package/lib/prismo-dev-scan.js
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
const fs = require("fs");
|
|
2
|
+
const http = require("http");
|
|
3
|
+
const https = require("https");
|
|
2
4
|
const os = require("os");
|
|
3
5
|
const path = require("path");
|
|
4
6
|
|
|
@@ -129,6 +131,7 @@ const DEFAULT_CLAUDEIGNORE = [
|
|
|
129
131
|
];
|
|
130
132
|
|
|
131
133
|
const NPX_COMMAND = "npx getprismo";
|
|
134
|
+
const DEFAULT_PRISMO_PROXY_URL = process.env.PRISMO_PROXY_URL || "http://localhost:8000";
|
|
132
135
|
|
|
133
136
|
function shouldUseColor() {
|
|
134
137
|
return process.stdout.isTTY && !process.env.NO_COLOR;
|
|
@@ -391,6 +394,270 @@ function scanCodexConfig(root) {
|
|
|
391
394
|
return { files: found, mcpServers };
|
|
392
395
|
}
|
|
393
396
|
|
|
397
|
+
function commandExists(command) {
|
|
398
|
+
const pathEntries = (process.env.PATH || "").split(path.delimiter).filter(Boolean);
|
|
399
|
+
const names = process.platform === "win32" ? [command, `${command}.cmd`, `${command}.exe`, `${command}.ps1`] : [command];
|
|
400
|
+
return pathEntries.some((entry) => names.some((name) => fs.existsSync(path.join(entry, name))));
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
function pathExistsAny(paths) {
|
|
404
|
+
return paths.some((candidate) => fs.existsSync(candidate));
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
function detectOptimizationStack(root, claudeConfig, codexConfig) {
|
|
408
|
+
const projectClaudePlugin = fs.existsSync(path.join(root, ".claude-plugin")) || fs.existsSync(path.join(root, ".claude", "settings.json"));
|
|
409
|
+
const projectMana = fs.existsSync(path.join(root, ".mana-mcp.json")) || fs.existsSync(path.join(os.homedir(), ".mana"));
|
|
410
|
+
const projectHeadroom = fs.existsSync(path.join(root, ".headroom")) || fs.existsSync(path.join(os.homedir(), ".headroom"));
|
|
411
|
+
const projectDistill = fs.existsSync(path.join(os.homedir(), ".config", "distill")) || commandExists("distill");
|
|
412
|
+
const projectRtk = fs.existsSync(path.join(root, ".rtk")) || commandExists("rtk");
|
|
413
|
+
|
|
414
|
+
const tools = {
|
|
415
|
+
rtk: { detected: projectRtk, source: projectRtk ? "binary-or-project-config" : "not-detected" },
|
|
416
|
+
headroom: { detected: projectHeadroom || commandExists("headroom"), source: projectHeadroom ? "local-config" : commandExists("headroom") ? "binary" : "not-detected" },
|
|
417
|
+
distill: { detected: projectDistill, source: projectDistill ? "binary-or-user-config" : "not-detected" },
|
|
418
|
+
mana: { detected: projectMana || commandExists("mana"), source: projectMana ? "local-config" : commandExists("mana") ? "binary" : "not-detected" },
|
|
419
|
+
};
|
|
420
|
+
|
|
421
|
+
return {
|
|
422
|
+
tools,
|
|
423
|
+
claudeHooks: claudeConfig.hooks,
|
|
424
|
+
claudeMcpServers: claudeConfig.mcpServers,
|
|
425
|
+
codexMcpServers: codexConfig.mcpServers,
|
|
426
|
+
claudePluginDetected: projectClaudePlugin,
|
|
427
|
+
mcpServerTotal: claudeConfig.mcpServers + codexConfig.mcpServers,
|
|
428
|
+
detectedTools: Object.entries(tools).filter(([, value]) => value.detected).map(([name]) => name),
|
|
429
|
+
};
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
function detectAgentReadiness(root, claudeConfig, codexConfig, realUsage) {
|
|
433
|
+
const claudeHome = process.env.PRISMO_CLAUDE_HOME || path.join(os.homedir(), ".claude");
|
|
434
|
+
const codexHome = process.env.PRISMO_CODEX_HOME || path.join(os.homedir(), ".codex");
|
|
435
|
+
const cursorPaths = [
|
|
436
|
+
path.join(root, ".cursor"),
|
|
437
|
+
path.join(root, ".cursorrules"),
|
|
438
|
+
path.join(os.homedir(), ".cursor"),
|
|
439
|
+
path.join(os.homedir(), ".config", "Cursor"),
|
|
440
|
+
];
|
|
441
|
+
const usageSources = new Set(realUsage && realUsage.sources ? realUsage.sources : []);
|
|
442
|
+
|
|
443
|
+
const claudeSessionFiles = getClaudeSessionFiles(root);
|
|
444
|
+
const codexSessionFiles = getCodexSessionFiles();
|
|
445
|
+
|
|
446
|
+
return {
|
|
447
|
+
claudeCode: {
|
|
448
|
+
detected: claudeConfig.files.length > 0 || fs.existsSync(claudeHome) || claudeSessionFiles.length > 0,
|
|
449
|
+
configFiles: claudeConfig.files,
|
|
450
|
+
localLogsFound: claudeSessionFiles.length > 0 || usageSources.has("claude-code"),
|
|
451
|
+
mcpServers: claudeConfig.mcpServers,
|
|
452
|
+
hooks: claudeConfig.hooks,
|
|
453
|
+
exactProxyTracking: "limited-for-subscription-mode",
|
|
454
|
+
recommendedMode: "local-log-and-repo-scan",
|
|
455
|
+
},
|
|
456
|
+
codex: {
|
|
457
|
+
detected: codexConfig.files.length > 0 || fs.existsSync(codexHome) || codexSessionFiles.length > 0,
|
|
458
|
+
configFiles: codexConfig.files,
|
|
459
|
+
localLogsFound: codexSessionFiles.length > 0 || usageSources.has("codex"),
|
|
460
|
+
mcpServers: codexConfig.mcpServers,
|
|
461
|
+
exactProxyTracking: "available-when-using-api-key-base-url-mode",
|
|
462
|
+
recommendedMode: "prismo-proxy-for-api-mode-or-local-log-watch",
|
|
463
|
+
},
|
|
464
|
+
cursor: {
|
|
465
|
+
detected: pathExistsAny(cursorPaths),
|
|
466
|
+
configFiles: cursorPaths.filter((candidate) => fs.existsSync(candidate)),
|
|
467
|
+
localLogsFound: false,
|
|
468
|
+
exactProxyTracking: "available-only-if-configured-for-openai-compatible-base-url",
|
|
469
|
+
recommendedMode: "repo-scan-and-prismo-proxy-when-supported",
|
|
470
|
+
},
|
|
471
|
+
localUsageLogsAvailable: Boolean((realUsage && realUsage.sessions.length) || claudeSessionFiles.length || codexSessionFiles.length),
|
|
472
|
+
exactProxyTrackingAvailable: true,
|
|
473
|
+
notes: [
|
|
474
|
+
"Exact tracking is available when a tool sends OpenAI/Anthropic API traffic through Prismo.",
|
|
475
|
+
"Subscription coding-agent sessions usually require local-log visibility unless the tool supports a custom base URL.",
|
|
476
|
+
],
|
|
477
|
+
};
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
function detectToolOutputRisk({ exposedLargeFiles, exposedHighRiskDirs, highRiskDirs }) {
|
|
481
|
+
const noisyDirs = highRiskDirs.filter((dir) => ["coverage", "test-results", "playwright-report", "logs", "dist", "build", ".next"].some((name) => dir.path.split("/").includes(name)));
|
|
482
|
+
const exposedNoisyDirs = exposedHighRiskDirs.filter((dir) => noisyDirs.some((candidate) => candidate.path === dir.path));
|
|
483
|
+
const noisyFiles = exposedLargeFiles.filter((file) => ["log", "json", "minified", "lock/generated"].includes(file.kind) || /\.(log|json|ndjson|out|trace|har)$/i.test(file.path));
|
|
484
|
+
const estimatedExposureTokens = estimateTokens(noisyFiles.reduce((sum, file) => sum + file.size, 0));
|
|
485
|
+
let level = "Low";
|
|
486
|
+
if (exposedNoisyDirs.length >= 3 || noisyFiles.length >= 3 || estimatedExposureTokens >= 250000) level = "High";
|
|
487
|
+
else if (exposedNoisyDirs.length || noisyFiles.length || estimatedExposureTokens >= 50000) level = "Medium";
|
|
488
|
+
|
|
489
|
+
return {
|
|
490
|
+
level,
|
|
491
|
+
exposedNoisyDirectories: exposedNoisyDirs.map((dir) => dir.path),
|
|
492
|
+
noisyDirectoriesDetected: noisyDirs.map((dir) => ({ path: dir.path, exposed: dir.exposed })),
|
|
493
|
+
exposedNoisyFiles: noisyFiles.map((file) => ({
|
|
494
|
+
path: file.path,
|
|
495
|
+
kind: file.kind,
|
|
496
|
+
sizeBytes: file.size,
|
|
497
|
+
estimatedTokensIfRead: estimateTokens(file.size),
|
|
498
|
+
})),
|
|
499
|
+
estimatedExposureTokens,
|
|
500
|
+
summary:
|
|
501
|
+
level === "High"
|
|
502
|
+
? "Large logs, test reports, build output, or generated files are exposed to coding-agent reads."
|
|
503
|
+
: level === "Medium"
|
|
504
|
+
? "Some noisy tool-output artifacts are present and may enter context during broad exploration."
|
|
505
|
+
: "No major exposed tool-output artifacts detected.",
|
|
506
|
+
};
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
function buildProxyTrackingReadiness({ codexConfig, claudeConfig, realUsage }) {
|
|
510
|
+
return {
|
|
511
|
+
exactApiTracking: {
|
|
512
|
+
available: true,
|
|
513
|
+
description: "Available for apps and coding tools that send OpenAI or Anthropic API traffic through the Prismo base URL.",
|
|
514
|
+
},
|
|
515
|
+
codingAgentBaseUrlMode: {
|
|
516
|
+
codex: codexConfig.files.length ? "possible-if-using-api-key-mode" : "not-detected",
|
|
517
|
+
claudeCode: "limited-for-subscription-sessions",
|
|
518
|
+
cursor: "possible-if-configured-for-openai-compatible-provider",
|
|
519
|
+
},
|
|
520
|
+
localEstimateTracking: {
|
|
521
|
+
available: true,
|
|
522
|
+
logsFound: Boolean(realUsage && realUsage.sessions.length),
|
|
523
|
+
description: "Available for subscription coding tools when local Codex/Claude Code logs exist; accuracy depends on token fields exposed by those tools.",
|
|
524
|
+
},
|
|
525
|
+
unsupported: [
|
|
526
|
+
"Exact billing for hidden subscription sessions without provider traffic, API keys, or local token fields.",
|
|
527
|
+
"Prompt interception or tool rewriting is not enabled by PrismoDev Scan.",
|
|
528
|
+
],
|
|
529
|
+
};
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
function checkUrlReachable(url, timeoutMs = 650) {
|
|
533
|
+
return new Promise((resolve) => {
|
|
534
|
+
let parsed;
|
|
535
|
+
try {
|
|
536
|
+
parsed = new URL(url);
|
|
537
|
+
} catch {
|
|
538
|
+
resolve({ url, reachable: false, error: "invalid-url" });
|
|
539
|
+
return;
|
|
540
|
+
}
|
|
541
|
+
const client = parsed.protocol === "https:" ? https : http;
|
|
542
|
+
const request = client.request(
|
|
543
|
+
{
|
|
544
|
+
method: "GET",
|
|
545
|
+
hostname: parsed.hostname,
|
|
546
|
+
port: parsed.port,
|
|
547
|
+
path: parsed.pathname === "/" ? "/health" : parsed.pathname,
|
|
548
|
+
timeout: timeoutMs,
|
|
549
|
+
},
|
|
550
|
+
(response) => {
|
|
551
|
+
response.resume();
|
|
552
|
+
resolve({ url, reachable: response.statusCode >= 200 && response.statusCode < 500, statusCode: response.statusCode });
|
|
553
|
+
}
|
|
554
|
+
);
|
|
555
|
+
request.on("timeout", () => {
|
|
556
|
+
request.destroy();
|
|
557
|
+
resolve({ url, reachable: false, error: "timeout" });
|
|
558
|
+
});
|
|
559
|
+
request.on("error", (error) => {
|
|
560
|
+
resolve({ url, reachable: false, error: error.code || error.message });
|
|
561
|
+
});
|
|
562
|
+
request.end();
|
|
563
|
+
});
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
async function runSetup(rootDir = process.cwd(), options = {}) {
|
|
567
|
+
const scan = scanRepo(rootDir, { includeUsage: true, usageLimit: options.limit || 3 });
|
|
568
|
+
const proxyUrl = options.proxyUrl || DEFAULT_PRISMO_PROXY_URL;
|
|
569
|
+
const proxy = options.skipProxyCheck
|
|
570
|
+
? { url: proxyUrl, reachable: false, skipped: true }
|
|
571
|
+
: await checkUrlReachable(proxyUrl, options.timeoutMs || 650);
|
|
572
|
+
|
|
573
|
+
const modes = [
|
|
574
|
+
{
|
|
575
|
+
id: "local-log-tracking",
|
|
576
|
+
label: "Local log tracking",
|
|
577
|
+
status: scan.agentReadiness.localUsageLogsAvailable ? "available" : "limited",
|
|
578
|
+
description: "Reads local Codex/Claude Code session logs when present. Good for subscription coding tools when exact proxy traffic is unavailable.",
|
|
579
|
+
},
|
|
580
|
+
{
|
|
581
|
+
id: "exact-api-proxy",
|
|
582
|
+
label: "Exact API proxy tracking",
|
|
583
|
+
status: proxy.reachable ? "available" : "proxy-not-running",
|
|
584
|
+
description: "Exact tokens, costs, routing, budgets, and analytics when app or coding-tool traffic uses the Prismo OpenAI/Anthropic base URL.",
|
|
585
|
+
},
|
|
586
|
+
{
|
|
587
|
+
id: "codex-base-url",
|
|
588
|
+
label: "Codex API/base-url mode",
|
|
589
|
+
status: scan.agentReadiness.codex.detected ? "possible" : "not-detected",
|
|
590
|
+
description: "Use Prismo for exact tracking when Codex is running with API-key/base-url compatible traffic.",
|
|
591
|
+
},
|
|
592
|
+
{
|
|
593
|
+
id: "claude-subscription",
|
|
594
|
+
label: "Claude subscription sessions",
|
|
595
|
+
status: scan.agentReadiness.claudeCode.detected ? "local-estimates-only" : "not-detected",
|
|
596
|
+
description: "Claude Code subscription traffic is not exact unless it can be routed through Prismo; use local logs and repo diagnostics otherwise.",
|
|
597
|
+
},
|
|
598
|
+
];
|
|
599
|
+
|
|
600
|
+
const recommended = [];
|
|
601
|
+
recommended.push(`${NPX_COMMAND} watch`);
|
|
602
|
+
if (!scan.hasClaudeIgnore) recommended.push(`${NPX_COMMAND} scan --fix`);
|
|
603
|
+
recommended.push(`${NPX_COMMAND} optimize`);
|
|
604
|
+
if (proxy.reachable) {
|
|
605
|
+
recommended.push(`OPENAI_BASE_URL=${proxyUrl.replace(/\/$/, "")}/v1 codex`);
|
|
606
|
+
} else {
|
|
607
|
+
recommended.push("Start the Prismo proxy, then route API-mode tools through its OpenAI/Anthropic base URL.");
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
return {
|
|
611
|
+
scannedPath: scan.root,
|
|
612
|
+
generatedAt: new Date().toISOString(),
|
|
613
|
+
prismoProxy: proxy,
|
|
614
|
+
detected: {
|
|
615
|
+
claudeCode: scan.agentReadiness.claudeCode,
|
|
616
|
+
codex: scan.agentReadiness.codex,
|
|
617
|
+
cursor: scan.agentReadiness.cursor,
|
|
618
|
+
optimizationStack: scan.optimizationStack,
|
|
619
|
+
localUsageLogsAvailable: scan.agentReadiness.localUsageLogsAvailable,
|
|
620
|
+
},
|
|
621
|
+
trackingModes: modes,
|
|
622
|
+
recommendedCommands: Array.from(new Set(recommended)).slice(0, 5),
|
|
623
|
+
caveats: [
|
|
624
|
+
"Prismo can track exact usage only when traffic flows through the Prismo proxy.",
|
|
625
|
+
"Subscription coding tools may expose local logs, but those are local visibility signals rather than provider billing records.",
|
|
626
|
+
"Setup is read-only and does not modify Claude, Codex, Cursor, MCP, shell, or Prismo config.",
|
|
627
|
+
],
|
|
628
|
+
};
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
function renderSetupTerminal(result) {
|
|
632
|
+
const lines = [];
|
|
633
|
+
lines.push("");
|
|
634
|
+
lines.push(color("PrismoDev Setup", "bold"));
|
|
635
|
+
lines.push("");
|
|
636
|
+
lines.push(`Repo: ${result.scannedPath}`);
|
|
637
|
+
lines.push(`Prismo proxy: ${result.prismoProxy.reachable ? "running" : "not reachable"} (${result.prismoProxy.url})`);
|
|
638
|
+
if (result.prismoProxy.statusCode) lines.push(`Proxy status: HTTP ${result.prismoProxy.statusCode}`);
|
|
639
|
+
lines.push("");
|
|
640
|
+
lines.push("Detected:");
|
|
641
|
+
lines.push(`- Claude Code: ${result.detected.claudeCode.detected ? "detected" : "not detected"}; logs: ${result.detected.claudeCode.localLogsFound ? "found" : "not found"}; MCP: ${result.detected.claudeCode.mcpServers}; hooks: ${result.detected.claudeCode.hooks}`);
|
|
642
|
+
lines.push(`- Codex: ${result.detected.codex.detected ? "detected" : "not detected"}; logs: ${result.detected.codex.localLogsFound ? "found" : "not found"}; MCP: ${result.detected.codex.mcpServers}`);
|
|
643
|
+
lines.push(`- Cursor: ${result.detected.cursor.detected ? "detected" : "not detected"}`);
|
|
644
|
+
const detectedTools = result.detected.optimizationStack.detectedTools;
|
|
645
|
+
lines.push(`- Optimization tools: ${detectedTools.length ? detectedTools.join(", ") : "none detected"}`);
|
|
646
|
+
lines.push("");
|
|
647
|
+
lines.push("Tracking Modes:");
|
|
648
|
+
result.trackingModes.forEach((mode, index) => {
|
|
649
|
+
lines.push(`${index + 1}. ${mode.label}: ${mode.status}`);
|
|
650
|
+
lines.push(` ${mode.description}`);
|
|
651
|
+
});
|
|
652
|
+
lines.push("");
|
|
653
|
+
lines.push("Recommended:");
|
|
654
|
+
result.recommendedCommands.forEach((command, index) => lines.push(`${index + 1}. ${command}`));
|
|
655
|
+
lines.push("");
|
|
656
|
+
lines.push("Notes:");
|
|
657
|
+
result.caveats.forEach((caveat) => lines.push(`- ${caveat}`));
|
|
658
|
+
return lines.join("\n");
|
|
659
|
+
}
|
|
660
|
+
|
|
394
661
|
function classifyLargeFiles(files) {
|
|
395
662
|
return files
|
|
396
663
|
.filter((file) => file.kind !== "binary" && file.size >= 500 * 1024)
|
|
@@ -433,7 +700,7 @@ function estimateMcpImpact(count) {
|
|
|
433
700
|
}
|
|
434
701
|
|
|
435
702
|
function severityWeight(severity) {
|
|
436
|
-
return severity === "critical" ?
|
|
703
|
+
return severity === "critical" ? 10 : severity === "high" ? 6 : severity === "medium" ? 4 : 2;
|
|
437
704
|
}
|
|
438
705
|
|
|
439
706
|
function severityRank(severity) {
|
|
@@ -451,7 +718,7 @@ function addIssue(issues, severity, category, title, description, recommendation
|
|
|
451
718
|
});
|
|
452
719
|
}
|
|
453
720
|
|
|
454
|
-
function buildRecommendations({ hasClaudeIgnore, gitignorePatterns, exposedHighRiskDirs, largeFiles, instructionFiles, claudeConfig }) {
|
|
721
|
+
function buildRecommendations({ hasClaudeIgnore, gitignorePatterns, exposedHighRiskDirs, largeFiles, instructionFiles, claudeConfig, toolOutputRisk, agentReadiness }) {
|
|
455
722
|
const recs = [];
|
|
456
723
|
if (!hasClaudeIgnore) {
|
|
457
724
|
recs.push("Create .claudeignore with generated/cache folders and large artifacts excluded.");
|
|
@@ -465,6 +732,9 @@ function buildRecommendations({ hasClaudeIgnore, gitignorePatterns, exposedHighR
|
|
|
465
732
|
if (largeFiles.some((file) => !file.ignored)) {
|
|
466
733
|
recs.push("Avoid loading large logs, JSON dumps, coverage reports, and minified assets into coding-agent context.");
|
|
467
734
|
}
|
|
735
|
+
if (toolOutputRisk && toolOutputRisk.level !== "Low") {
|
|
736
|
+
recs.push("Use command-output filtering or narrower shell commands for noisy tests, logs, diffs, and generated reports.");
|
|
737
|
+
}
|
|
468
738
|
if (instructionFiles.some((file) => file.isClaude && file.tokens > 500)) {
|
|
469
739
|
recs.push("Trim CLAUDE.md to project rules only; move long implementation notes into docs referenced on demand.");
|
|
470
740
|
}
|
|
@@ -473,17 +743,24 @@ function buildRecommendations({ hasClaudeIgnore, gitignorePatterns, exposedHighR
|
|
|
473
743
|
}
|
|
474
744
|
recs.push("Start fresh sessions for unrelated tasks and compact long sessions when context growth accelerates.");
|
|
475
745
|
recs.push("Use cheaper/faster models for mechanical edits, formatting, and low-risk refactors.");
|
|
746
|
+
if (agentReadiness && (agentReadiness.codex.detected || agentReadiness.claudeCode.detected)) {
|
|
747
|
+
recs.push("Run `npx getprismo watch` for local coding-session visibility while working.");
|
|
748
|
+
}
|
|
749
|
+
recs.push("Route API-mode coding tools through Prismo when they support custom OpenAI/Anthropic base URLs for exact cost tracking.");
|
|
476
750
|
return Array.from(new Set(recs));
|
|
477
751
|
}
|
|
478
752
|
|
|
479
|
-
function scoreScan(issues, stats) {
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
753
|
+
function scoreScan(issues, stats, context = {}) {
|
|
754
|
+
const issuePenalty = issues.reduce((sum, issue) => sum + severityWeight(issue.severity), 0);
|
|
755
|
+
const repoPenalty =
|
|
756
|
+
(stats.totalFiles > 2500 ? 4 : 0) +
|
|
757
|
+
(stats.exposedLargeFiles > 10 ? 4 : 0) +
|
|
758
|
+
(stats.exposedHighRiskDirs > 4 ? 4 : 0);
|
|
759
|
+
const toolOutputPenalty = context.toolOutputRisk && context.toolOutputRisk.level === "High" ? 8 : context.toolOutputRisk && context.toolOutputRisk.level === "Medium" ? 4 : 0;
|
|
760
|
+
const readinessCredit = context.agentReadiness && context.agentReadiness.localUsageLogsAvailable ? 3 : 0;
|
|
761
|
+
const proxyCredit = context.proxyTrackingReadiness && context.proxyTrackingReadiness.exactApiTracking.available ? 2 : 0;
|
|
762
|
+
|
|
763
|
+
let score = 100 - issuePenalty - repoPenalty - toolOutputPenalty + readinessCredit + proxyCredit;
|
|
487
764
|
score = Math.max(0, Math.min(100, score));
|
|
488
765
|
|
|
489
766
|
const risk = score >= 80 ? "Low" : score >= 55 ? "Medium" : "High";
|
|
@@ -516,6 +793,8 @@ function toJsonPayload(result) {
|
|
|
516
793
|
score: result.score,
|
|
517
794
|
riskLevel: result.risk,
|
|
518
795
|
estimatedAvoidableWasteRange: result.avoidableWaste,
|
|
796
|
+
detectedFrameworks: result.frameworks,
|
|
797
|
+
stats: result.stats,
|
|
519
798
|
issues: result.issues,
|
|
520
799
|
recommendations: result.recommendations,
|
|
521
800
|
largeFiles: result.largeFiles.map((file) => ({
|
|
@@ -551,6 +830,10 @@ function toJsonPayload(result) {
|
|
|
551
830
|
hasCodexDirectory: fs.existsSync(path.join(result.root, ".codex")),
|
|
552
831
|
hasOpenAiDirectory: fs.existsSync(path.join(result.root, ".openai")),
|
|
553
832
|
},
|
|
833
|
+
agentReadiness: result.agentReadiness,
|
|
834
|
+
optimizationStack: result.optimizationStack,
|
|
835
|
+
toolOutputRisk: result.toolOutputRisk,
|
|
836
|
+
proxyTrackingReadiness: result.proxyTrackingReadiness,
|
|
554
837
|
suggestedClaudeIgnore: result.recommendedClaudeIgnore,
|
|
555
838
|
nextCommands: getNextCommands(result),
|
|
556
839
|
generatedAt: result.generatedAt,
|
|
@@ -570,7 +853,7 @@ function addRealUsageIssues(issues, usage) {
|
|
|
570
853
|
if (total >= 1000000) {
|
|
571
854
|
addIssue(
|
|
572
855
|
issues,
|
|
573
|
-
total >=
|
|
856
|
+
total >= 10000000 ? "high" : "medium",
|
|
574
857
|
"repo_size",
|
|
575
858
|
`Recent local AI sessions used ${formatTokenCount(total)} tokens`,
|
|
576
859
|
exact ? "Prismo found exact token counts in local Codex/Claude Code session logs." : "Prismo estimated usage from local session text because exact token fields were unavailable.",
|
|
@@ -644,6 +927,7 @@ function scanRepo(rootDir = process.cwd(), options = {}) {
|
|
|
644
927
|
const combinedIgnorePatterns = Array.from(new Set([...gitignorePatterns, ...claudeIgnorePatterns]));
|
|
645
928
|
|
|
646
929
|
const { files, highRiskDirs } = walkRepo(root, combinedIgnorePatterns);
|
|
930
|
+
const frameworks = detectFrameworks(root, { files });
|
|
647
931
|
const instructionFiles = scanInstructionFiles(root);
|
|
648
932
|
const largeFiles = classifyLargeFiles(files);
|
|
649
933
|
const exposedLargeFiles = largeFiles.filter((file) => !file.ignored);
|
|
@@ -704,7 +988,7 @@ function scanRepo(rootDir = process.cwd(), options = {}) {
|
|
|
704
988
|
if (!hasClaudeIgnore) {
|
|
705
989
|
addIssue(
|
|
706
990
|
issues,
|
|
707
|
-
exposedHighRiskDirs.length
|
|
991
|
+
exposedHighRiskDirs.length > 5 && exposedLargeFiles.length > 3 ? "critical" : "high",
|
|
708
992
|
"ignore_file",
|
|
709
993
|
".claudeignore not found",
|
|
710
994
|
"Claude Code-style workflows may expose generated files, caches, and logs unless they are ignored.",
|
|
@@ -801,6 +1085,36 @@ function scanRepo(rootDir = process.cwd(), options = {}) {
|
|
|
801
1085
|
|
|
802
1086
|
const realUsage = options.includeUsage ? getUsageSummary({ tool: options.usageTool || "all", cwd: root, limit: options.usageLimit || 5 }) : null;
|
|
803
1087
|
addRealUsageIssues(issues, realUsage);
|
|
1088
|
+
const optimizationStack = detectOptimizationStack(root, claudeConfig, codexConfig);
|
|
1089
|
+
const agentReadiness = detectAgentReadiness(root, claudeConfig, codexConfig, realUsage);
|
|
1090
|
+
const toolOutputRisk = detectToolOutputRisk({ exposedLargeFiles, exposedHighRiskDirs, highRiskDirs });
|
|
1091
|
+
const proxyTrackingReadiness = buildProxyTrackingReadiness({ codexConfig, claudeConfig, realUsage });
|
|
1092
|
+
|
|
1093
|
+
if (toolOutputRisk.level !== "Low") {
|
|
1094
|
+
addIssue(
|
|
1095
|
+
issues,
|
|
1096
|
+
toolOutputRisk.level === "High" ? "high" : "medium",
|
|
1097
|
+
"large_file",
|
|
1098
|
+
`Tool output risk is ${toolOutputRisk.level}`,
|
|
1099
|
+
toolOutputRisk.summary,
|
|
1100
|
+
"Use narrower commands, summarize logs, and ignore generated test/build output before loading it into coding agents.",
|
|
1101
|
+
toolOutputRisk.estimatedExposureTokens
|
|
1102
|
+
? `Likely avoidable token exposure: up to ~${toolOutputRisk.estimatedExposureTokens.toLocaleString()} tokens from exposed noisy artifacts.`
|
|
1103
|
+
: "Potential savings estimate: prevents noisy command/file output from becoming recurring context."
|
|
1104
|
+
);
|
|
1105
|
+
}
|
|
1106
|
+
|
|
1107
|
+
if (optimizationStack.mcpServerTotal >= 8) {
|
|
1108
|
+
addIssue(
|
|
1109
|
+
issues,
|
|
1110
|
+
"medium",
|
|
1111
|
+
"mcp_tooling",
|
|
1112
|
+
`${optimizationStack.mcpServerTotal} total MCP/tool servers detected`,
|
|
1113
|
+
"Large tool surfaces can increase tool-choice overhead across Claude Code and Codex-style workflows.",
|
|
1114
|
+
"Disable MCP servers not needed for the current repo or current task.",
|
|
1115
|
+
estimateMcpImpact(optimizationStack.mcpServerTotal)
|
|
1116
|
+
);
|
|
1117
|
+
}
|
|
804
1118
|
|
|
805
1119
|
const sourceFiles = files.filter((file) => file.kind === "source").length;
|
|
806
1120
|
const stats = {
|
|
@@ -834,7 +1148,7 @@ function scanRepo(rootDir = process.cwd(), options = {}) {
|
|
|
834
1148
|
"Likely avoidable token exposure: large repos make repeated discovery more expensive."
|
|
835
1149
|
);
|
|
836
1150
|
}
|
|
837
|
-
const score = scoreScan(issues, stats);
|
|
1151
|
+
const score = scoreScan(issues, stats, { toolOutputRisk, agentReadiness, proxyTrackingReadiness });
|
|
838
1152
|
const largeFileSuggestions = exposedLargeFiles
|
|
839
1153
|
.filter((file) => file.size >= 1024 * 1024 || ["log", "json", "minified", "lock/generated"].includes(file.kind))
|
|
840
1154
|
.map((file) => file.path);
|
|
@@ -850,6 +1164,8 @@ function scanRepo(rootDir = process.cwd(), options = {}) {
|
|
|
850
1164
|
largeFiles,
|
|
851
1165
|
instructionFiles,
|
|
852
1166
|
claudeConfig,
|
|
1167
|
+
toolOutputRisk,
|
|
1168
|
+
agentReadiness,
|
|
853
1169
|
});
|
|
854
1170
|
buildRealUsageRecommendations(realUsage).forEach((rec) => recommendations.push(rec));
|
|
855
1171
|
|
|
@@ -861,6 +1177,11 @@ function scanRepo(rootDir = process.cwd(), options = {}) {
|
|
|
861
1177
|
issues,
|
|
862
1178
|
recommendations,
|
|
863
1179
|
realUsage,
|
|
1180
|
+
agentReadiness,
|
|
1181
|
+
optimizationStack,
|
|
1182
|
+
toolOutputRisk,
|
|
1183
|
+
proxyTrackingReadiness,
|
|
1184
|
+
frameworks,
|
|
864
1185
|
files,
|
|
865
1186
|
instructionFiles,
|
|
866
1187
|
largeFiles,
|
|
@@ -886,7 +1207,7 @@ function renderTerminalReport(result, options = {}) {
|
|
|
886
1207
|
const riskTone = result.risk === "High" ? "red" : result.risk === "Medium" ? "yellow" : "green";
|
|
887
1208
|
const lines = [];
|
|
888
1209
|
lines.push("");
|
|
889
|
-
lines.push(color("
|
|
1210
|
+
lines.push(color("PrismoDev", "bold", useColor));
|
|
890
1211
|
lines.push("");
|
|
891
1212
|
lines.push(`Score: ${color(`${result.score}/100`, riskTone, useColor)} | Risk: ${color(result.risk, riskTone, useColor)} | Token leaks: ${result.issues.length}`);
|
|
892
1213
|
lines.push(`Estimated avoidable waste: ${result.avoidableWaste}`);
|
|
@@ -917,6 +1238,31 @@ function renderTerminalReport(result, options = {}) {
|
|
|
917
1238
|
lines.push("- Real local usage: no matching local Codex/Claude Code sessions found for this repo");
|
|
918
1239
|
}
|
|
919
1240
|
lines.push("");
|
|
1241
|
+
lines.push(color("Coding Agent Readiness", "bold", useColor));
|
|
1242
|
+
lines.push(`- Claude Code: ${result.agentReadiness.claudeCode.detected ? "detected" : "not detected"}; logs: ${result.agentReadiness.claudeCode.localLogsFound ? "found" : "not found"}; MCP: ${result.agentReadiness.claudeCode.mcpServers}; hooks: ${result.agentReadiness.claudeCode.hooks}`);
|
|
1243
|
+
lines.push(`- Codex: ${result.agentReadiness.codex.detected ? "detected" : "not detected"}; logs: ${result.agentReadiness.codex.localLogsFound ? "found" : "not found"}; MCP: ${result.agentReadiness.codex.mcpServers}`);
|
|
1244
|
+
lines.push(`- Cursor: ${result.agentReadiness.cursor.detected ? "detected" : "not detected"}`);
|
|
1245
|
+
lines.push("");
|
|
1246
|
+
lines.push(color("Optimization Stack", "bold", useColor));
|
|
1247
|
+
const stack = result.optimizationStack;
|
|
1248
|
+
lines.push(`- RTK: ${stack.tools.rtk.detected ? "detected" : "not detected"}`);
|
|
1249
|
+
lines.push(`- Headroom: ${stack.tools.headroom.detected ? "detected" : "not detected"}`);
|
|
1250
|
+
lines.push(`- Distill: ${stack.tools.distill.detected ? "detected" : "not detected"}`);
|
|
1251
|
+
lines.push(`- Mana: ${stack.tools.mana.detected ? "detected" : "not detected"}`);
|
|
1252
|
+
lines.push(`- Claude hooks: ${stack.claudeHooks}; MCP servers: ${stack.mcpServerTotal}`);
|
|
1253
|
+
lines.push("");
|
|
1254
|
+
lines.push(color("Tool Output Risk", "bold", useColor));
|
|
1255
|
+
lines.push(`- Level: ${result.toolOutputRisk.level}`);
|
|
1256
|
+
lines.push(`- ${result.toolOutputRisk.summary}`);
|
|
1257
|
+
if (result.toolOutputRisk.exposedNoisyDirectories.length) lines.push(`- Exposed noisy dirs: ${result.toolOutputRisk.exposedNoisyDirectories.slice(0, 6).join(", ")}`);
|
|
1258
|
+
if (result.toolOutputRisk.exposedNoisyFiles.length) lines.push(`- Exposed noisy files: ${result.toolOutputRisk.exposedNoisyFiles.slice(0, 4).map((file) => file.path).join(", ")}`);
|
|
1259
|
+
lines.push("");
|
|
1260
|
+
lines.push(color("Prismo Proxy Tracking", "bold", useColor));
|
|
1261
|
+
lines.push("- Exact API tracking: available when traffic uses the Prismo OpenAI/Anthropic base URL");
|
|
1262
|
+
lines.push(`- Codex API/base-url mode: ${result.proxyTrackingReadiness.codingAgentBaseUrlMode.codex}`);
|
|
1263
|
+
lines.push(`- Claude Code subscription mode: ${result.proxyTrackingReadiness.codingAgentBaseUrlMode.claudeCode}`);
|
|
1264
|
+
lines.push("- Subscription sessions: local-log visibility when available, not guaranteed billing accuracy");
|
|
1265
|
+
lines.push("");
|
|
920
1266
|
lines.push(color("Issues", "bold", useColor));
|
|
921
1267
|
if (!result.issues.length) {
|
|
922
1268
|
lines.push("- [ok] No major token-waste risks detected.");
|
|
@@ -943,7 +1289,7 @@ function renderTerminalReport(result, options = {}) {
|
|
|
943
1289
|
|
|
944
1290
|
function renderMarkdownReport(result) {
|
|
945
1291
|
const lines = [];
|
|
946
|
-
lines.push("#
|
|
1292
|
+
lines.push("# PrismoDev Report");
|
|
947
1293
|
lines.push("");
|
|
948
1294
|
lines.push("## Executive Summary");
|
|
949
1295
|
lines.push("");
|
|
@@ -977,6 +1323,49 @@ function renderMarkdownReport(result) {
|
|
|
977
1323
|
lines.push(`- Token-bloat directories: ${result.stats.highRiskDirs}`);
|
|
978
1324
|
lines.push(`- Exposed token-bloat directories: ${result.stats.exposedHighRiskDirs}`);
|
|
979
1325
|
lines.push("");
|
|
1326
|
+
lines.push("## Coding Agent Readiness");
|
|
1327
|
+
lines.push("");
|
|
1328
|
+
lines.push(`- Claude Code detected: ${result.agentReadiness.claudeCode.detected ? "yes" : "no"}`);
|
|
1329
|
+
lines.push(` - Local logs found: ${result.agentReadiness.claudeCode.localLogsFound ? "yes" : "no"}`);
|
|
1330
|
+
lines.push(` - MCP servers: ${result.agentReadiness.claudeCode.mcpServers}; hooks: ${result.agentReadiness.claudeCode.hooks}`);
|
|
1331
|
+
lines.push(` - Exact proxy tracking: ${result.agentReadiness.claudeCode.exactProxyTracking}`);
|
|
1332
|
+
lines.push(`- Codex detected: ${result.agentReadiness.codex.detected ? "yes" : "no"}`);
|
|
1333
|
+
lines.push(` - Local logs found: ${result.agentReadiness.codex.localLogsFound ? "yes" : "no"}`);
|
|
1334
|
+
lines.push(` - MCP servers: ${result.agentReadiness.codex.mcpServers}`);
|
|
1335
|
+
lines.push(` - Exact proxy tracking: ${result.agentReadiness.codex.exactProxyTracking}`);
|
|
1336
|
+
lines.push(`- Cursor detected: ${result.agentReadiness.cursor.detected ? "yes" : "no"}`);
|
|
1337
|
+
lines.push(` - Exact proxy tracking: ${result.agentReadiness.cursor.exactProxyTracking}`);
|
|
1338
|
+
lines.push("");
|
|
1339
|
+
lines.push("## Optimization Stack");
|
|
1340
|
+
lines.push("");
|
|
1341
|
+
lines.push(`- RTK: ${result.optimizationStack.tools.rtk.detected ? "detected" : "not detected"}`);
|
|
1342
|
+
lines.push(`- Headroom: ${result.optimizationStack.tools.headroom.detected ? "detected" : "not detected"}`);
|
|
1343
|
+
lines.push(`- Distill: ${result.optimizationStack.tools.distill.detected ? "detected" : "not detected"}`);
|
|
1344
|
+
lines.push(`- Mana: ${result.optimizationStack.tools.mana.detected ? "detected" : "not detected"}`);
|
|
1345
|
+
lines.push(`- Claude hooks: ${result.optimizationStack.claudeHooks}`);
|
|
1346
|
+
lines.push(`- Total MCP/tool servers: ${result.optimizationStack.mcpServerTotal}`);
|
|
1347
|
+
lines.push("");
|
|
1348
|
+
lines.push("## Tool Output Risk");
|
|
1349
|
+
lines.push("");
|
|
1350
|
+
lines.push(`- Level: ${result.toolOutputRisk.level}`);
|
|
1351
|
+
lines.push(`- ${result.toolOutputRisk.summary}`);
|
|
1352
|
+
if (result.toolOutputRisk.exposedNoisyDirectories.length) {
|
|
1353
|
+
lines.push(`- Exposed noisy directories: ${result.toolOutputRisk.exposedNoisyDirectories.map((dir) => `\`${dir}/\``).join(", ")}`);
|
|
1354
|
+
}
|
|
1355
|
+
if (result.toolOutputRisk.exposedNoisyFiles.length) {
|
|
1356
|
+
result.toolOutputRisk.exposedNoisyFiles.slice(0, 20).forEach((file) => {
|
|
1357
|
+
lines.push(`- \`${file.path}\` - ${formatBytes(file.sizeBytes)} - ~${file.estimatedTokensIfRead.toLocaleString()} tokens if read`);
|
|
1358
|
+
});
|
|
1359
|
+
}
|
|
1360
|
+
lines.push("");
|
|
1361
|
+
lines.push("## Prismo Proxy Tracking Readiness");
|
|
1362
|
+
lines.push("");
|
|
1363
|
+
lines.push("- Exact API tracking: available when app/tool traffic uses the Prismo OpenAI/Anthropic base URL.");
|
|
1364
|
+
lines.push(`- Codex API/base-url mode: ${result.proxyTrackingReadiness.codingAgentBaseUrlMode.codex}.`);
|
|
1365
|
+
lines.push(`- Claude Code subscription mode: ${result.proxyTrackingReadiness.codingAgentBaseUrlMode.claudeCode}.`);
|
|
1366
|
+
lines.push("- Local estimate tracking: available from local Codex/Claude Code logs when those logs exist.");
|
|
1367
|
+
lines.push("- Unsupported: exact billing for hidden subscription sessions without provider traffic, API keys, or local token fields.");
|
|
1368
|
+
lines.push("");
|
|
980
1369
|
if (result.realUsage) {
|
|
981
1370
|
lines.push("## Real Local Usage");
|
|
982
1371
|
lines.push("");
|
|
@@ -1058,7 +1447,7 @@ function renderMarkdownReport(result) {
|
|
|
1058
1447
|
lines.push("");
|
|
1059
1448
|
lines.push("## Disclaimer");
|
|
1060
1449
|
lines.push("");
|
|
1061
|
-
lines.push("
|
|
1450
|
+
lines.push("PrismoDev is a fast local scanner. It does not connect to Anthropic, OpenAI, Claude Code, Codex, Cursor, or billing accounts. Token and savings estimates are heuristic and should be treated as directional diagnostics only.");
|
|
1062
1451
|
lines.push("");
|
|
1063
1452
|
return lines.join("\n");
|
|
1064
1453
|
}
|
|
@@ -1821,7 +2210,11 @@ function analyzeSessionFile(filePath, tool) {
|
|
|
1821
2210
|
|
|
1822
2211
|
session.turns = Math.max(session.userMessages, session.assistantMessages);
|
|
1823
2212
|
session.estimatedTotalTokens = session.estimatedInputTokens + session.estimatedOutputTokens + session.estimatedToolTokens;
|
|
1824
|
-
session.
|
|
2213
|
+
session.exactActiveTokens = session.exactAvailable
|
|
2214
|
+
? Math.max(session.exactInputTokens - session.exactCacheReadTokens, 0) + session.exactOutputTokens + (session.exactCacheCreationTokens || 0)
|
|
2215
|
+
: 0;
|
|
2216
|
+
session.contextTokens = session.exactAvailable ? session.exactTotalTokens : session.estimatedTotalTokens;
|
|
2217
|
+
session.displayTokens = session.exactAvailable ? session.exactActiveTokens : session.estimatedTotalTokens;
|
|
1825
2218
|
session.confidence = session.exactAvailable ? "exact-local-log" : "estimated-local-log";
|
|
1826
2219
|
session.contextRisk = getSessionRisk(session.displayTokens, session.estimatedToolTokens);
|
|
1827
2220
|
session.largestTextBlobs = session.largestTextBlobs.sort((a, b) => b.tokens - a.tokens).slice(0, 5);
|
|
@@ -1883,20 +2276,23 @@ function getUsageSummary(options = {}) {
|
|
|
1883
2276
|
const totals = selected.reduce(
|
|
1884
2277
|
(acc, session) => {
|
|
1885
2278
|
acc.displayTokens += session.displayTokens || 0;
|
|
2279
|
+
acc.contextTokens += session.contextTokens || 0;
|
|
1886
2280
|
acc.estimatedTokens += session.estimatedTotalTokens || 0;
|
|
1887
2281
|
acc.exactTokens += session.exactAvailable ? session.exactTotalTokens : 0;
|
|
1888
2282
|
acc.toolTokens += session.estimatedToolTokens || 0;
|
|
1889
2283
|
acc.sessions += 1;
|
|
1890
2284
|
return acc;
|
|
1891
2285
|
},
|
|
1892
|
-
{ sessions: 0, displayTokens: 0, estimatedTokens: 0, exactTokens: 0, toolTokens: 0 }
|
|
2286
|
+
{ sessions: 0, displayTokens: 0, contextTokens: 0, estimatedTokens: 0, exactTokens: 0, toolTokens: 0 }
|
|
1893
2287
|
);
|
|
2288
|
+
const sources = Array.from(new Set(selected.map((session) => session.tool).filter(Boolean)));
|
|
1894
2289
|
return {
|
|
1895
2290
|
generatedAt: new Date().toISOString(),
|
|
1896
2291
|
scannedPath: cwd,
|
|
1897
2292
|
tool,
|
|
1898
2293
|
confidence: selected.every((session) => session.exactAvailable) && selected.length ? "exact-local-log" : "mixed-or-estimated",
|
|
1899
2294
|
totals,
|
|
2295
|
+
sources,
|
|
1900
2296
|
sessions: selected,
|
|
1901
2297
|
};
|
|
1902
2298
|
}
|
|
@@ -1926,6 +2322,91 @@ function formatTokenCount(value) {
|
|
|
1926
2322
|
return String(Math.round(n));
|
|
1927
2323
|
}
|
|
1928
2324
|
|
|
2325
|
+
function getRiskRank(risk) {
|
|
2326
|
+
return { High: 3, Medium: 2, Low: 1 }[risk] || 0;
|
|
2327
|
+
}
|
|
2328
|
+
|
|
2329
|
+
function getTopToolNames(session, limit = 4) {
|
|
2330
|
+
return Object.entries(session && session.toolNames ? session.toolNames : {})
|
|
2331
|
+
.sort((a, b) => b[1] - a[1])
|
|
2332
|
+
.slice(0, limit)
|
|
2333
|
+
.map(([name, count]) => ({ name, count }));
|
|
2334
|
+
}
|
|
2335
|
+
|
|
2336
|
+
function buildLiveWarnings(activeSession, summary) {
|
|
2337
|
+
const warnings = [];
|
|
2338
|
+
if (!activeSession) {
|
|
2339
|
+
warnings.push("No active local session detected for this repo yet.");
|
|
2340
|
+
return warnings;
|
|
2341
|
+
}
|
|
2342
|
+
if (activeSession.contextRisk === "High") {
|
|
2343
|
+
warnings.push("Context risk is high; consider starting a fresh session at the next task boundary.");
|
|
2344
|
+
} else if (activeSession.contextRisk === "Medium") {
|
|
2345
|
+
warnings.push("Context risk is rising; keep reads and shell output narrow.");
|
|
2346
|
+
}
|
|
2347
|
+
if (activeSession.estimatedToolTokens >= 150000) {
|
|
2348
|
+
warnings.push("Tool/output tokens are dominating this session; summarize logs and test output before loading more.");
|
|
2349
|
+
} else if (activeSession.estimatedToolTokens >= 50000) {
|
|
2350
|
+
warnings.push("Tool/output tokens are elevated; prefer targeted commands and smaller file reads.");
|
|
2351
|
+
}
|
|
2352
|
+
if (activeSession.turns >= 30) {
|
|
2353
|
+
warnings.push("Long session detected; split unrelated follow-up work into a new session.");
|
|
2354
|
+
}
|
|
2355
|
+
if (!activeSession.exactAvailable) {
|
|
2356
|
+
warnings.push("Exact token fields were not found; usage is estimated from local session text.");
|
|
2357
|
+
}
|
|
2358
|
+
if (summary.totals.displayTokens >= 1000000) {
|
|
2359
|
+
warnings.push("Recent local usage is above 1M tokens; prioritize the largest session for cleanup.");
|
|
2360
|
+
}
|
|
2361
|
+
return Array.from(new Set(warnings)).slice(0, 5);
|
|
2362
|
+
}
|
|
2363
|
+
|
|
2364
|
+
function getRecommendedWatchAction(activeSession, warnings) {
|
|
2365
|
+
if (!activeSession) return `${NPX_COMMAND} setup`;
|
|
2366
|
+
if (warnings.some((warning) => warning.includes("Tool/output"))) return `${NPX_COMMAND} context`;
|
|
2367
|
+
if (activeSession.contextRisk === "High") return "Start a fresh coding-agent session before the next unrelated task.";
|
|
2368
|
+
if (activeSession.turns >= 20) return `${NPX_COMMAND} optimize`;
|
|
2369
|
+
return `${NPX_COMMAND} scan --usage`;
|
|
2370
|
+
}
|
|
2371
|
+
|
|
2372
|
+
function buildLiveSessionView(summary) {
|
|
2373
|
+
const activeSession = summary.sessions[0] || null;
|
|
2374
|
+
const warnings = buildLiveWarnings(activeSession, summary);
|
|
2375
|
+
const topTools = getTopToolNames(activeSession);
|
|
2376
|
+
const largestTextBlobs = activeSession ? activeSession.largestTextBlobs || [] : [];
|
|
2377
|
+
return {
|
|
2378
|
+
activeSession: activeSession
|
|
2379
|
+
? {
|
|
2380
|
+
tool: activeSession.tool,
|
|
2381
|
+
sessionId: activeSession.sessionId,
|
|
2382
|
+
title: activeSession.title,
|
|
2383
|
+
model: activeSession.model,
|
|
2384
|
+
cwd: activeSession.cwd,
|
|
2385
|
+
updatedAt: activeSession.updatedAt,
|
|
2386
|
+
tokens: activeSession.displayTokens,
|
|
2387
|
+
contextTokens: activeSession.contextTokens,
|
|
2388
|
+
exactAvailable: activeSession.exactAvailable,
|
|
2389
|
+
confidence: activeSession.confidence,
|
|
2390
|
+
contextRisk: activeSession.contextRisk,
|
|
2391
|
+
turns: activeSession.turns,
|
|
2392
|
+
toolCalls: activeSession.toolCalls,
|
|
2393
|
+
toolResults: activeSession.toolResults,
|
|
2394
|
+
estimatedToolTokens: activeSession.estimatedToolTokens,
|
|
2395
|
+
topTools,
|
|
2396
|
+
largestTextBlobs,
|
|
2397
|
+
}
|
|
2398
|
+
: null,
|
|
2399
|
+
highestRisk: summary.sessions.reduce((risk, session) => (getRiskRank(session.contextRisk) > getRiskRank(risk) ? session.contextRisk : risk), "Low"),
|
|
2400
|
+
warnings,
|
|
2401
|
+
recommendedAction: getRecommendedWatchAction(activeSession, warnings),
|
|
2402
|
+
nextCommands: Array.from(new Set([
|
|
2403
|
+
`${NPX_COMMAND} context`,
|
|
2404
|
+
`${NPX_COMMAND} optimize`,
|
|
2405
|
+
`${NPX_COMMAND} scan --usage`,
|
|
2406
|
+
])).slice(0, 3),
|
|
2407
|
+
};
|
|
2408
|
+
}
|
|
2409
|
+
|
|
1929
2410
|
function renderUsageTerminal(summary, title = "Prismo Usage") {
|
|
1930
2411
|
const lines = [];
|
|
1931
2412
|
lines.push("");
|
|
@@ -1954,16 +2435,67 @@ function renderUsageTerminal(summary, title = "Prismo Usage") {
|
|
|
1954
2435
|
return lines.join("\n");
|
|
1955
2436
|
}
|
|
1956
2437
|
|
|
2438
|
+
function renderWatchTerminal(summary) {
|
|
2439
|
+
const live = summary.live || buildLiveSessionView(summary);
|
|
2440
|
+
const active = live.activeSession;
|
|
2441
|
+
const lines = [];
|
|
2442
|
+
lines.push("");
|
|
2443
|
+
lines.push(color("Prismo Watch", "bold"));
|
|
2444
|
+
lines.push("");
|
|
2445
|
+
if (!active) {
|
|
2446
|
+
lines.push("Active Session");
|
|
2447
|
+
lines.push("- No local Codex/Claude Code session detected for this repo yet.");
|
|
2448
|
+
lines.push("");
|
|
2449
|
+
lines.push("Next Action");
|
|
2450
|
+
lines.push(`Run: ${live.recommendedAction}`);
|
|
2451
|
+
lines.push("");
|
|
2452
|
+
lines.push("Tip: start Codex or Claude Code in this repo, then keep this watch open.");
|
|
2453
|
+
return lines.join("\n");
|
|
2454
|
+
}
|
|
2455
|
+
lines.push("Active Session");
|
|
2456
|
+
lines.push(`Source: ${active.tool}`);
|
|
2457
|
+
lines.push(`Tokens: ${formatTokenCount(active.tokens)} (${active.confidence})`);
|
|
2458
|
+
if (active.contextTokens && active.contextTokens !== active.tokens) lines.push(`Context tokens observed: ${formatTokenCount(active.contextTokens)}`);
|
|
2459
|
+
lines.push(`Context Risk: ${active.contextRisk}`);
|
|
2460
|
+
lines.push(`Tool/output tokens: ${formatTokenCount(active.estimatedToolTokens)}`);
|
|
2461
|
+
lines.push(`Turns: ${active.turns} | Tool calls: ${active.toolCalls}`);
|
|
2462
|
+
if (active.model) lines.push(`Model: ${active.model}`);
|
|
2463
|
+
if (active.updatedAt) lines.push(`Updated: ${active.updatedAt}`);
|
|
2464
|
+
lines.push("");
|
|
2465
|
+
lines.push("Live Warnings");
|
|
2466
|
+
live.warnings.forEach((warning) => lines.push(`- ${warning}`));
|
|
2467
|
+
lines.push("");
|
|
2468
|
+
if (active.largestTextBlobs.length) {
|
|
2469
|
+
lines.push("Largest Context Sources");
|
|
2470
|
+
active.largestTextBlobs.slice(0, 4).forEach((blob) => lines.push(`- ${blob.label}: ~${blob.tokens.toLocaleString()} tokens`));
|
|
2471
|
+
lines.push("");
|
|
2472
|
+
}
|
|
2473
|
+
if (active.topTools.length) {
|
|
2474
|
+
lines.push("Top Tools");
|
|
2475
|
+
active.topTools.forEach((tool) => lines.push(`- ${tool.name}: ${tool.count}`));
|
|
2476
|
+
lines.push("");
|
|
2477
|
+
}
|
|
2478
|
+
lines.push("Next Action");
|
|
2479
|
+
lines.push(`Run: ${live.recommendedAction}`);
|
|
2480
|
+
lines.push("");
|
|
2481
|
+
lines.push("Useful Commands");
|
|
2482
|
+
live.nextCommands.forEach((command) => lines.push(`- ${command}`));
|
|
2483
|
+
lines.push("");
|
|
2484
|
+
lines.push("Local estimates come from available coding-agent session logs; exact billing requires traffic routed through Prismo.");
|
|
2485
|
+
return lines.join("\n");
|
|
2486
|
+
}
|
|
2487
|
+
|
|
1957
2488
|
async function watchUsage(options = {}) {
|
|
1958
2489
|
const intervalMs = options.intervalMs || 3000;
|
|
1959
2490
|
const iterations = options.once ? 1 : Number.POSITIVE_INFINITY;
|
|
1960
2491
|
for (let i = 0; i < iterations; i += 1) {
|
|
1961
2492
|
const summary = getUsageSummary(options);
|
|
2493
|
+
summary.live = buildLiveSessionView(summary);
|
|
1962
2494
|
if (options.json) {
|
|
1963
2495
|
console.log(JSON.stringify(summary, null, 2));
|
|
1964
2496
|
} else {
|
|
1965
2497
|
console.clear();
|
|
1966
|
-
console.log(
|
|
2498
|
+
console.log(renderWatchTerminal(summary));
|
|
1967
2499
|
console.log(`\nRefreshing every ${Math.round(intervalMs / 1000)}s. Press Ctrl+C to stop.`);
|
|
1968
2500
|
}
|
|
1969
2501
|
if (i + 1 >= iterations) break;
|
|
@@ -2090,6 +2622,34 @@ function createDemoResult() {
|
|
|
2090
2622
|
sessions: [{}, {}, {}],
|
|
2091
2623
|
},
|
|
2092
2624
|
stats: { totalFiles: 842, sourceFiles: 318, largeFiles: 4, exposedLargeFiles: 2, highRiskDirs: 7, exposedHighRiskDirs: 3 },
|
|
2625
|
+
agentReadiness: {
|
|
2626
|
+
claudeCode: { detected: true, localLogsFound: true, mcpServers: 4, hooks: 2, exactProxyTracking: "limited-for-subscription-mode" },
|
|
2627
|
+
codex: { detected: true, localLogsFound: true, mcpServers: 1, exactProxyTracking: "available-when-using-api-key-base-url-mode" },
|
|
2628
|
+
cursor: { detected: false, exactProxyTracking: "available-only-if-configured-for-openai-compatible-base-url" },
|
|
2629
|
+
localUsageLogsAvailable: true,
|
|
2630
|
+
},
|
|
2631
|
+
optimizationStack: {
|
|
2632
|
+
tools: {
|
|
2633
|
+
rtk: { detected: false },
|
|
2634
|
+
headroom: { detected: false },
|
|
2635
|
+
distill: { detected: false },
|
|
2636
|
+
mana: { detected: false },
|
|
2637
|
+
},
|
|
2638
|
+
claudeHooks: 2,
|
|
2639
|
+
mcpServerTotal: 5,
|
|
2640
|
+
},
|
|
2641
|
+
toolOutputRisk: {
|
|
2642
|
+
level: "High",
|
|
2643
|
+
summary: "Large logs, test reports, build output, or generated files are exposed to coding-agent reads.",
|
|
2644
|
+
exposedNoisyDirectories: ["logs", "coverage"],
|
|
2645
|
+
exposedNoisyFiles: [{ path: "logs/debug-output.json" }],
|
|
2646
|
+
},
|
|
2647
|
+
proxyTrackingReadiness: {
|
|
2648
|
+
codingAgentBaseUrlMode: {
|
|
2649
|
+
codex: "possible-if-using-api-key-mode",
|
|
2650
|
+
claudeCode: "limited-for-subscription-sessions",
|
|
2651
|
+
},
|
|
2652
|
+
},
|
|
2093
2653
|
topTokenLeaks: getTopTokenLeaks(issues),
|
|
2094
2654
|
hasClaudeIgnore: false,
|
|
2095
2655
|
repoDetected: true,
|
|
@@ -2132,7 +2692,7 @@ function runDevFlow(rootDir = process.cwd(), options = {}) {
|
|
|
2132
2692
|
function renderDevTerminal(result) {
|
|
2133
2693
|
const lines = [];
|
|
2134
2694
|
lines.push("");
|
|
2135
|
-
lines.push(color("
|
|
2695
|
+
lines.push(color("PrismoDev", "bold"));
|
|
2136
2696
|
lines.push("");
|
|
2137
2697
|
lines.push(`Score: ${result.scan.score}/100 | Risk: ${result.scan.risk} | Token leaks: ${result.scan.issues.length}`);
|
|
2138
2698
|
if (result.scan.realUsage && result.scan.realUsage.sessions.length) {
|
|
@@ -2195,6 +2755,7 @@ function printHelp() {
|
|
|
2195
2755
|
|
|
2196
2756
|
Usage:
|
|
2197
2757
|
prismo dev [path]
|
|
2758
|
+
prismo setup [--json] [--proxy-url URL] [path]
|
|
2198
2759
|
prismo scan [--fix] [--json] [--usage] [--no-report] [path]
|
|
2199
2760
|
prismo optimize [scope] [--json] [path]
|
|
2200
2761
|
prismo context [scope] [--json] [path]
|
|
@@ -2204,12 +2765,13 @@ Usage:
|
|
|
2204
2765
|
|
|
2205
2766
|
Commands:
|
|
2206
2767
|
dev Guided flow: scan, optimize, and print a paste-ready context prompt.
|
|
2207
|
-
scan Run
|
|
2768
|
+
scan Run PrismoDev for Claude Code, Codex, Cursor, and AI coding workflows.
|
|
2208
2769
|
optimize Generate lightweight AI-readable project context files in .prismo/.
|
|
2209
2770
|
context Print a copy-pasteable compact context prompt for AI coding tools.
|
|
2210
2771
|
usage Read local Codex/Claude Code session logs and summarize token usage.
|
|
2211
2772
|
watch Refresh local session usage in the terminal.
|
|
2212
2773
|
demo Show sample output without needing a messy repo.
|
|
2774
|
+
setup Detect coding tools, tracking modes, local logs, and Prismo proxy readiness.
|
|
2213
2775
|
|
|
2214
2776
|
Options:
|
|
2215
2777
|
--fix Safely create .claudeignore if missing and generate the report.
|
|
@@ -2218,6 +2780,7 @@ Options:
|
|
|
2218
2780
|
--no-report Do not write prismo-dev-report.md.
|
|
2219
2781
|
--limit N Number of recent local sessions to show.
|
|
2220
2782
|
--interval N Refresh interval in seconds for watch mode.
|
|
2783
|
+
--proxy-url URL Prismo proxy URL to check during setup.
|
|
2221
2784
|
--once Run watch mode once, useful for tests and scripts.
|
|
2222
2785
|
--help Show this help.
|
|
2223
2786
|
`);
|
|
@@ -2225,7 +2788,7 @@ Options:
|
|
|
2225
2788
|
|
|
2226
2789
|
function printCommandHelp(command) {
|
|
2227
2790
|
const help = {
|
|
2228
|
-
scan: `
|
|
2791
|
+
scan: `PrismoDev
|
|
2229
2792
|
|
|
2230
2793
|
Usage:
|
|
2231
2794
|
prismo scan [--fix] [--json] [--usage] [--no-report] [--limit N] [path]
|
|
@@ -2278,7 +2841,7 @@ Usage:
|
|
|
2278
2841
|
Examples:
|
|
2279
2842
|
prismo watch codex
|
|
2280
2843
|
prismo watch claude --once --json`,
|
|
2281
|
-
dev: `
|
|
2844
|
+
dev: `PrismoDev
|
|
2282
2845
|
|
|
2283
2846
|
Usage:
|
|
2284
2847
|
prismo dev [--json] [--limit N] [path]
|
|
@@ -2287,12 +2850,25 @@ Flow:
|
|
|
2287
2850
|
1. Scan repo and local usage.
|
|
2288
2851
|
2. Generate .prismo/ optimized context files.
|
|
2289
2852
|
3. Print a paste-ready prompt.`,
|
|
2853
|
+
setup: `PrismoDev Setup
|
|
2854
|
+
|
|
2855
|
+
Usage:
|
|
2856
|
+
prismo setup [--json] [--proxy-url URL] [--limit N] [path]
|
|
2857
|
+
|
|
2858
|
+
Examples:
|
|
2859
|
+
prismo setup
|
|
2860
|
+
prismo setup --json
|
|
2861
|
+
prismo setup --proxy-url http://localhost:8000
|
|
2862
|
+
|
|
2863
|
+
Output:
|
|
2864
|
+
Detects Claude Code, Codex, Cursor, local logs, optimization stack, and Prismo proxy readiness.
|
|
2865
|
+
This command is read-only and does not modify coding-tool config.`,
|
|
2290
2866
|
demo: `Prismo Demo
|
|
2291
2867
|
|
|
2292
2868
|
Usage:
|
|
2293
2869
|
prismo demo
|
|
2294
2870
|
|
|
2295
|
-
Shows sample
|
|
2871
|
+
Shows sample PrismoDev output without reading local files.`,
|
|
2296
2872
|
};
|
|
2297
2873
|
console.log(help[command] || "Unknown command. Try: prismo --help");
|
|
2298
2874
|
}
|
|
@@ -2307,8 +2883,8 @@ async function runCli(argv) {
|
|
|
2307
2883
|
printCommandHelp(command);
|
|
2308
2884
|
return;
|
|
2309
2885
|
}
|
|
2310
|
-
if (!["dev", "scan", "optimize", "context", "usage", "watch", "demo"].includes(command)) {
|
|
2311
|
-
throw new Error(`Unknown command: ${command}. Try: prismo dev, prismo scan, prismo optimize, prismo context, or prismo usage`);
|
|
2886
|
+
if (!["dev", "setup", "scan", "optimize", "context", "usage", "watch", "demo"].includes(command)) {
|
|
2887
|
+
throw new Error(`Unknown command: ${command}. Try: prismo dev, prismo setup, prismo scan, prismo optimize, prismo context, or prismo usage`);
|
|
2312
2888
|
}
|
|
2313
2889
|
|
|
2314
2890
|
if (command === "demo") {
|
|
@@ -2342,6 +2918,24 @@ async function runCli(argv) {
|
|
|
2342
2918
|
return;
|
|
2343
2919
|
}
|
|
2344
2920
|
|
|
2921
|
+
if (command === "setup") {
|
|
2922
|
+
const json = rest.includes("--json");
|
|
2923
|
+
const limitIndex = rest.indexOf("--limit");
|
|
2924
|
+
const proxyIndex = rest.indexOf("--proxy-url");
|
|
2925
|
+
const target = getPositionals(rest, new Set(["--limit", "--proxy-url"]))[0] || process.cwd();
|
|
2926
|
+
const result = await runSetup(target, {
|
|
2927
|
+
limit: parsePositiveInt(limitIndex >= 0 ? rest[limitIndex + 1] : null, 3),
|
|
2928
|
+
proxyUrl: proxyIndex >= 0 ? rest[proxyIndex + 1] : DEFAULT_PRISMO_PROXY_URL,
|
|
2929
|
+
skipProxyCheck: rest.includes("--skip-proxy-check"),
|
|
2930
|
+
});
|
|
2931
|
+
if (json) {
|
|
2932
|
+
console.log(JSON.stringify(result, null, 2));
|
|
2933
|
+
return;
|
|
2934
|
+
}
|
|
2935
|
+
console.log(renderSetupTerminal(result));
|
|
2936
|
+
return;
|
|
2937
|
+
}
|
|
2938
|
+
|
|
2345
2939
|
if (command === "usage" || command === "watch") {
|
|
2346
2940
|
const json = rest.includes("--json");
|
|
2347
2941
|
const knownTools = new Set(["codex", "claude", "all"]);
|
|
@@ -2468,7 +3062,9 @@ module.exports = {
|
|
|
2468
3062
|
estimateTokens,
|
|
2469
3063
|
renderMarkdownReport,
|
|
2470
3064
|
renderUsageTerminal,
|
|
3065
|
+
renderWatchTerminal,
|
|
2471
3066
|
renderTerminalReport,
|
|
3067
|
+
runSetup,
|
|
2472
3068
|
runOptimize,
|
|
2473
3069
|
runCli,
|
|
2474
3070
|
scanRepo,
|