getprismo 0.1.53 → 0.1.55
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/lib/prismo-dev/cli.js +33 -1
- package/lib/prismo-dev/help.js +8 -0
- package/lib/prismo-dev/mcp.js +123 -0
- package/lib/prismo-dev/sessions-report.js +196 -0
- package/lib/prismo-dev-scan.js +16 -0
- package/package.json +1 -1
package/lib/prismo-dev/cli.js
CHANGED
|
@@ -3,7 +3,7 @@ const { printHelp, printCommandHelp } = require("./help");
|
|
|
3
3
|
|
|
4
4
|
const VALID_COMMANDS = new Set([
|
|
5
5
|
"dev", "init", "doctor", "firewall", "benchmark", "shield", "mcp",
|
|
6
|
-
"connect", "sync", "status", "disconnect", "agent", "connector", "setup", "scan", "digest",
|
|
6
|
+
"connect", "sync", "status", "disconnect", "agent", "connector", "setup", "scan", "digest", "sessions", "report",
|
|
7
7
|
"optimize", "context", "cc", "cursor", "receipt", "instructions",
|
|
8
8
|
"timeline", "replay", "boundaries", "usage", "guard", "watch", "demo", "repair",
|
|
9
9
|
"enforce", "bridge", "hook", "protect",
|
|
@@ -69,6 +69,10 @@ function createCli(deps) {
|
|
|
69
69
|
runDisconnect,
|
|
70
70
|
runStatus,
|
|
71
71
|
runSync,
|
|
72
|
+
buildSessionsView,
|
|
73
|
+
renderSessionsTerminal,
|
|
74
|
+
buildLocalReport,
|
|
75
|
+
renderLocalReportTerminal,
|
|
72
76
|
renderGuardTerminal,
|
|
73
77
|
runGuard,
|
|
74
78
|
REPAIR_CAUSES,
|
|
@@ -474,6 +478,34 @@ function createCli(deps) {
|
|
|
474
478
|
return;
|
|
475
479
|
}
|
|
476
480
|
|
|
481
|
+
if (command === "sessions") {
|
|
482
|
+
const json = rest.includes("--json");
|
|
483
|
+
const limitIndex = rest.indexOf("--limit");
|
|
484
|
+
const toolIndex = rest.indexOf("--tool");
|
|
485
|
+
const target = getPositionals(rest, new Set(["--limit", "--tool"]))[0] || process.cwd();
|
|
486
|
+
const view = buildSessionsView(target, {
|
|
487
|
+
limit: parsePositiveInt(limitIndex >= 0 ? rest[limitIndex + 1] : null, 10),
|
|
488
|
+
tool: toolIndex >= 0 ? rest[toolIndex + 1] : "all",
|
|
489
|
+
allRepos: rest.includes("--all-repos"),
|
|
490
|
+
});
|
|
491
|
+
if (json) console.log(JSON.stringify(view, null, 2));
|
|
492
|
+
else console.log(renderSessionsTerminal(view));
|
|
493
|
+
return;
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
if (command === "report") {
|
|
497
|
+
const json = rest.includes("--json");
|
|
498
|
+
const limitIndex = rest.indexOf("--limit");
|
|
499
|
+
const target = getPositionals(rest, new Set(["--limit"]))[0] || process.cwd();
|
|
500
|
+
const report = buildLocalReport(target, {
|
|
501
|
+
limit: parsePositiveInt(limitIndex >= 0 ? rest[limitIndex + 1] : null, 10),
|
|
502
|
+
allRepos: rest.includes("--all-repos"),
|
|
503
|
+
});
|
|
504
|
+
if (json) console.log(JSON.stringify(report, null, 2));
|
|
505
|
+
else console.log(renderLocalReportTerminal(report));
|
|
506
|
+
return;
|
|
507
|
+
}
|
|
508
|
+
|
|
477
509
|
if (command === "digest") {
|
|
478
510
|
const json = rest.includes("--json");
|
|
479
511
|
const daysIndex = rest.indexOf("--days");
|
package/lib/prismo-dev/help.js
CHANGED
|
@@ -17,6 +17,8 @@ Usage:
|
|
|
17
17
|
prismo bridge [--json] [path]
|
|
18
18
|
prismo sync [--json] [--dry-run] [--watch] [--all-repos] [--interval N] [--limit N] [--tool all|codex|claude|cursor] [path]
|
|
19
19
|
prismo status [--json]
|
|
20
|
+
prismo sessions [--json] [--all-repos] [--limit N] [--tool all|codex|claude|cursor] [path]
|
|
21
|
+
prismo report [--json] [--all-repos] [--limit N] [path]
|
|
20
22
|
prismo digest [--json] [--days N]
|
|
21
23
|
prismo disconnect [--json]
|
|
22
24
|
prismo agent [--json] [--once] [--watch] [--interval N] [--sync-interval N] [--limit N] [--mode MODE] [path]
|
|
@@ -54,6 +56,8 @@ Commands:
|
|
|
54
56
|
bridge Explain optional agent bridge mode and live interception levels.
|
|
55
57
|
sync Send safe aggregate local agent telemetry to Prismo; use --watch for background-style sync.
|
|
56
58
|
status Show local PrismoDev connection and last sync state.
|
|
59
|
+
sessions List recent local agent sessions with tokens, waste, risk, and top cause (add --all-repos for every repo).
|
|
60
|
+
report Local waste snapshot: observed/wasted tokens, top causes, repeated reads/commands, and the next action. No connection needed.
|
|
57
61
|
digest Print the launch report: verified saved tokens/dollars first, with live prevention labeled estimated.
|
|
58
62
|
disconnect Remove the local PrismoDev cloud connection.
|
|
59
63
|
agent Claim and execute safe workspace actions queued from Prismo Cloud.
|
|
@@ -475,9 +479,13 @@ Tools exposed:
|
|
|
475
479
|
prismo_firewall
|
|
476
480
|
prismo_cc_timeline
|
|
477
481
|
prismo_cursor_sessions
|
|
482
|
+
prismo_should_shield (agent-native: should I shield this command?)
|
|
483
|
+
prismo_loop_check (agent-native: am I looping?)
|
|
484
|
+
prismo_context_guard (agent-native: what repo am I in / what to avoid rereading?)
|
|
478
485
|
|
|
479
486
|
Output:
|
|
480
487
|
Starts a local JSON-RPC MCP server over stdio. Use it from MCP-compatible clients so agents can scan context waste, search shielded command output, and request scoped context without loading huge logs into chat.
|
|
488
|
+
The agent-native tools answer single mid-session questions directly so Claude Code, Codex, and Cursor can call Prismo while they work, not just after.
|
|
481
489
|
|
|
482
490
|
prismo mcp doctor validates the local MCP tool surface and prints a ready-to-use client config snippet.`,
|
|
483
491
|
connect: `PrismoDev Connect
|
package/lib/prismo-dev/mcp.js
CHANGED
|
@@ -1,3 +1,40 @@
|
|
|
1
|
+
const path = require("path");
|
|
2
|
+
|
|
3
|
+
// Commands whose output reliably floods agent context and should run through
|
|
4
|
+
// shield (full output to disk, compact summary to the model).
|
|
5
|
+
const NOISY_COMMAND_PATTERNS = [
|
|
6
|
+
{ re: /(^|\s)(jest|vitest|mocha|pytest|rspec|phpunit|ava|tap)(\s|$)|(\b(npm|yarn|pnpm|bun)\s+(run\s+)?test\b)|\b(go|cargo)\s+test\b/, why: "test runs emit large pass/fail output" },
|
|
7
|
+
{ re: /(^|\s)(webpack|rollup|esbuild|vite|tsc)(\s|$)|\b(next|nuxt|astro)\s+build\b|\b(npm|yarn|pnpm|bun)\s+(run\s+)?build\b|\b(go|cargo)\s+build\b|(^|\s)make(\s|$)/, why: "builds emit long compiler/bundler output" },
|
|
8
|
+
{ re: /\b(npm|pnpm|bun)\s+(install|ci|i)\b|\byarn(\s+install)?\b|\bpip\s+install\b|\bbundle\s+install\b/, why: "installs print large dependency trees" },
|
|
9
|
+
{ re: /(^|\s)(eslint|ruff|flake8|mypy|pylint)(\s|$)|\b(npm|yarn|pnpm)\s+(run\s+)?lint\b|tsc\s+--noEmit/, why: "linters/type-checkers emit many diagnostics" },
|
|
10
|
+
{ re: /\b(playwright|cypress|e2e)\b/, why: "e2e runs dump verbose logs" },
|
|
11
|
+
{ re: /\bdocker\s+build\b|\bterraform\b|\bkubectl\s+logs\b|\bcoverage\b|--verbose\b/, why: "infra/coverage/verbose commands flood output" },
|
|
12
|
+
];
|
|
13
|
+
const QUICK_COMMAND_PATTERNS = [
|
|
14
|
+
/(^|\s)(git\s+(status|log|diff|branch|show)|ls|pwd|whoami|which|echo|cat|head|tail|wc|env|date|node\s+-v|--version)(\s|$)/,
|
|
15
|
+
];
|
|
16
|
+
|
|
17
|
+
function classifyCommandForShield(command, repeatedCommands = []) {
|
|
18
|
+
const cmd = String(command || "").trim();
|
|
19
|
+
if (!cmd) return { shouldShield: false, confidence: "low", reason: "No command provided." };
|
|
20
|
+
const normalized = cmd.replace(/\s+/g, " ");
|
|
21
|
+
const repeated = (repeatedCommands || []).some((item) => {
|
|
22
|
+
const value = String(item.value || "").replace(/\s+/g, " ");
|
|
23
|
+
return value && (normalized.includes(value) || value.includes(normalized)) && Number(item.count || 0) >= 3;
|
|
24
|
+
});
|
|
25
|
+
if (repeated) {
|
|
26
|
+
return { shouldShield: true, confidence: "high", reason: "This command has already repeated several times in recent sessions; shield it so each retry costs a summary, not full output." };
|
|
27
|
+
}
|
|
28
|
+
const noisy = NOISY_COMMAND_PATTERNS.find((entry) => entry.re.test(normalized));
|
|
29
|
+
if (noisy) {
|
|
30
|
+
return { shouldShield: true, confidence: "high", reason: `Shield recommended: ${noisy.why}.` };
|
|
31
|
+
}
|
|
32
|
+
if (QUICK_COMMAND_PATTERNS.some((re) => re.test(normalized))) {
|
|
33
|
+
return { shouldShield: false, confidence: "high", reason: "Quick, low-output command; shielding is unnecessary." };
|
|
34
|
+
}
|
|
35
|
+
return { shouldShield: false, confidence: "low", reason: "No known flooding pattern; shield only if you expect large output." };
|
|
36
|
+
}
|
|
37
|
+
|
|
1
38
|
function createTextResult(payload) {
|
|
2
39
|
const text = typeof payload === "string" ? payload : JSON.stringify(payload, null, 2);
|
|
3
40
|
return {
|
|
@@ -147,6 +184,20 @@ function createMcpTools(deps) {
|
|
|
147
184
|
tool: { type: "string", enum: ["all", "codex", "claude", "cursor"], description: "Which local session logs to inspect." },
|
|
148
185
|
limit: limitProperty,
|
|
149
186
|
}),
|
|
187
|
+
// Agent-native, mid-loop decision tools: tight yes/no answers an agent can
|
|
188
|
+
// act on during a session, not analysis dumps.
|
|
189
|
+
makeTool("prismo_should_shield", "Decide whether a shell command should be run through Prismo shield (full output to disk, compact summary to context) before you run it. Call this before any command that might print a lot.", {
|
|
190
|
+
command: { type: "string", description: "The exact shell command you are about to run." },
|
|
191
|
+
path: pathProperty,
|
|
192
|
+
}, ["command"]),
|
|
193
|
+
makeTool("prismo_loop_check", "Check whether the current coding session is stuck in a loop (repeating the same command or failing repeatedly). Call this when you have retried something more than once.", {
|
|
194
|
+
path: pathProperty,
|
|
195
|
+
tool: { type: "string", enum: ["all", "codex", "claude", "cursor"], description: "Which local session logs to inspect." },
|
|
196
|
+
}),
|
|
197
|
+
makeTool("prismo_context_guard", "Get what repo you are in, which paths to avoid rereading, files you have already read repeatedly, and the compact context pack to start from. Call this at the start of a session or before broad exploration.", {
|
|
198
|
+
path: pathProperty,
|
|
199
|
+
scope: scopeProperty,
|
|
200
|
+
}),
|
|
150
201
|
];
|
|
151
202
|
|
|
152
203
|
function resolveRoot(args) {
|
|
@@ -292,6 +343,75 @@ function createMcpTools(deps) {
|
|
|
292
343
|
}));
|
|
293
344
|
}
|
|
294
345
|
|
|
346
|
+
if (name === "prismo_should_shield") {
|
|
347
|
+
let repeatedCommands = [];
|
|
348
|
+
try {
|
|
349
|
+
const summary = getUsageSummary({ cwd: target, limit: 3, tool: "all" });
|
|
350
|
+
const latest = (summary.sessions || [])[0];
|
|
351
|
+
repeatedCommands = (latest && latest.repeatedCommands) || [];
|
|
352
|
+
} catch { /* command classification still works without session context */ }
|
|
353
|
+
const decision = classifyCommandForShield(args.command, repeatedCommands);
|
|
354
|
+
return createTextResult({
|
|
355
|
+
schemaVersion: 1,
|
|
356
|
+
command: args.command,
|
|
357
|
+
shouldShield: decision.shouldShield,
|
|
358
|
+
confidence: decision.confidence,
|
|
359
|
+
reason: decision.reason,
|
|
360
|
+
recommended: decision.shouldShield
|
|
361
|
+
? `npx -y getprismo@latest shield -- ${String(args.command || "").trim()}`
|
|
362
|
+
: null,
|
|
363
|
+
note: "Shield stores full stdout/stderr locally and returns a compact summary, so the output never floods context.",
|
|
364
|
+
});
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
if (name === "prismo_loop_check") {
|
|
368
|
+
const summary = getUsageSummary({ cwd: target, limit: 3, tool: args.tool || "all" });
|
|
369
|
+
const latest = (summary.sessions || [])[0] || null;
|
|
370
|
+
const repeatedCommands = (latest && latest.repeatedCommands) || [];
|
|
371
|
+
const looping = Boolean(latest && latest.loopSuspicion) || repeatedCommands.some((c) => Number(c.count || 0) >= 4);
|
|
372
|
+
const topRepeat = repeatedCommands[0] || null;
|
|
373
|
+
return createTextResult({
|
|
374
|
+
schemaVersion: 1,
|
|
375
|
+
looping,
|
|
376
|
+
confidence: latest ? (latest.loopConfidence || (looping ? "medium" : "low")) : "low",
|
|
377
|
+
signals: {
|
|
378
|
+
repeatedCommands: repeatedCommands.slice(0, 5),
|
|
379
|
+
failureMentions: latest ? Number(latest.failureMentions || 0) : 0,
|
|
380
|
+
turns: latest ? Number(latest.turns || 0) : 0,
|
|
381
|
+
},
|
|
382
|
+
advice: looping
|
|
383
|
+
? `You appear to be looping${topRepeat ? ` on \`${topRepeat.value}\` (${topRepeat.count}x)` : ""}. Stop retrying, change the approach, and capture the command once with \`npx -y getprismo@latest shield -- <command>\` instead of re-running it.`
|
|
384
|
+
: "No loop detected. Keep going.",
|
|
385
|
+
});
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
if (name === "prismo_context_guard") {
|
|
389
|
+
const scope = args.scope || null;
|
|
390
|
+
const scan = scanRepo(target, { includeUsage: true, usageLimit: 3 });
|
|
391
|
+
const ctx = createOptimizeContext(target, scope);
|
|
392
|
+
const usage = scan.realUsage && scan.realUsage.sessions ? scan.realUsage.sessions : [];
|
|
393
|
+
const repeatedlyRead = [];
|
|
394
|
+
for (const session of usage) {
|
|
395
|
+
for (const item of session.repeatedPathMentions || []) {
|
|
396
|
+
if (Number(item.count || 0) >= 4) repeatedlyRead.push({ path: item.value, reads: item.count });
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
const blocked = (scan.recommendedClaudeIgnore || []).slice(0, 24);
|
|
400
|
+
return createTextResult({
|
|
401
|
+
schemaVersion: 1,
|
|
402
|
+
repo: path.basename(path.resolve(target)),
|
|
403
|
+
avoidRereading: {
|
|
404
|
+
blockedContext: blocked,
|
|
405
|
+
alreadyReadRepeatedly: repeatedlyRead.slice(0, 12),
|
|
406
|
+
},
|
|
407
|
+
startFrom: {
|
|
408
|
+
contextFile: scope ? `.prismo/${scope}-summary.md` : ".prismo/architecture-summary.md",
|
|
409
|
+
starterPrompt: renderStarterPrompt(ctx, scope),
|
|
410
|
+
},
|
|
411
|
+
advice: "Read the start-from context pack first. Do not reread blocked paths or files you have already read several times; quote what you already saw instead.",
|
|
412
|
+
});
|
|
413
|
+
}
|
|
414
|
+
|
|
295
415
|
throw new Error(`Unknown MCP tool: ${name}`);
|
|
296
416
|
}
|
|
297
417
|
|
|
@@ -397,6 +517,9 @@ async function runMcpDoctor(deps) {
|
|
|
397
517
|
"prismo_timeline",
|
|
398
518
|
"prismo_replay",
|
|
399
519
|
"prismo_boundaries",
|
|
520
|
+
"prismo_should_shield",
|
|
521
|
+
"prismo_loop_check",
|
|
522
|
+
"prismo_context_guard",
|
|
400
523
|
];
|
|
401
524
|
const toolNames = tools.map((tool) => tool.name);
|
|
402
525
|
const missingTools = requiredTools.filter((name) => !toolNames.includes(name));
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
module.exports = function createSessionsReport(deps) {
|
|
2
|
+
const {
|
|
3
|
+
path,
|
|
4
|
+
getUsageSummary,
|
|
5
|
+
estimateWaste,
|
|
6
|
+
formatTokenCount,
|
|
7
|
+
} = deps;
|
|
8
|
+
|
|
9
|
+
const CAUSE_LABELS = {
|
|
10
|
+
"tool-output-flood": "Tool-output floods",
|
|
11
|
+
"repeated-file-reads": "Repeated file reads",
|
|
12
|
+
"generated-artifacts": "Generated artifacts",
|
|
13
|
+
"context-loop": "Context loops",
|
|
14
|
+
"long-session-buildup": "Long-session buildup",
|
|
15
|
+
"low-signal": "No strong waste signal",
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
const NEXT_ACTION = {
|
|
19
|
+
"tool-output-flood": "Run noisy commands through `prismo shield -- <command>` so output stays out of context.",
|
|
20
|
+
"repeated-file-reads": "Run `prismo repair repeated-file-reads`, then start sessions from .prismo context packs.",
|
|
21
|
+
"generated-artifacts": "Run `prismo repair generated-artifacts` to ignore build output and artifacts.",
|
|
22
|
+
"context-loop": "Run `prismo repair context-loop` and stop retrying failing commands.",
|
|
23
|
+
"long-session-buildup": "Split work at task boundaries; start fresh sessions from `prismo context`.",
|
|
24
|
+
"low-signal": "Nothing urgent. Run `prismo doctor` for a baseline.",
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
function repoBasename(session, fallbackRoot) {
|
|
28
|
+
const cwd = session && session.cwd ? session.cwd : fallbackRoot;
|
|
29
|
+
try {
|
|
30
|
+
return path.basename(path.resolve(cwd || process.cwd()));
|
|
31
|
+
} catch {
|
|
32
|
+
return "unknown";
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function collect(rootDir, options = {}) {
|
|
37
|
+
const root = path.resolve(rootDir || process.cwd());
|
|
38
|
+
const summary = getUsageSummary({
|
|
39
|
+
cwd: root,
|
|
40
|
+
tool: options.tool || "all",
|
|
41
|
+
limit: options.limit || 10,
|
|
42
|
+
allRepos: Boolean(options.allRepos),
|
|
43
|
+
});
|
|
44
|
+
const sessions = (summary.sessions || []).map((session) => {
|
|
45
|
+
const waste = estimateWaste(session);
|
|
46
|
+
return {
|
|
47
|
+
tool: session.tool || "unknown",
|
|
48
|
+
repo: repoBasename(session, root),
|
|
49
|
+
title: session.title || null,
|
|
50
|
+
model: session.model || null,
|
|
51
|
+
updatedAt: session.updatedAt || session.startedAt || null,
|
|
52
|
+
risk: session.contextRisk || "Unknown",
|
|
53
|
+
tokens: Number(waste.tokens || 0),
|
|
54
|
+
wastedTokens: Number(waste.wastedTokens || 0),
|
|
55
|
+
wastePercent: Number(waste.wastePercent || 0),
|
|
56
|
+
topCause: waste.topCause || "low-signal",
|
|
57
|
+
repeatedCommands: session.repeatedCommands || [],
|
|
58
|
+
repeatedPaths: session.repeatedPathMentions || [],
|
|
59
|
+
loopSuspicion: Boolean(session.loopSuspicion),
|
|
60
|
+
};
|
|
61
|
+
});
|
|
62
|
+
return { root, sessions, generatedAt: new Date().toISOString() };
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function buildSessionsView(rootDir, options = {}) {
|
|
66
|
+
const { root, sessions, generatedAt } = collect(rootDir, options);
|
|
67
|
+
const totals = sessions.reduce((acc, s) => {
|
|
68
|
+
acc.sessions += 1;
|
|
69
|
+
acc.tokens += s.tokens;
|
|
70
|
+
acc.wastedTokens += s.wastedTokens;
|
|
71
|
+
return acc;
|
|
72
|
+
}, { sessions: 0, tokens: 0, wastedTokens: 0 });
|
|
73
|
+
totals.wastePercent = totals.tokens > 0 ? Math.round((totals.wastedTokens / totals.tokens) * 100) : 0;
|
|
74
|
+
return {
|
|
75
|
+
schemaVersion: 1,
|
|
76
|
+
command: "sessions",
|
|
77
|
+
scannedPath: root,
|
|
78
|
+
allRepos: Boolean(options.allRepos),
|
|
79
|
+
sessions: sessions.sort((a, b) => String(b.updatedAt || "").localeCompare(String(a.updatedAt || ""))),
|
|
80
|
+
totals,
|
|
81
|
+
generatedAt,
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function renderSessionsTerminal(view) {
|
|
86
|
+
const lines = [];
|
|
87
|
+
lines.push("");
|
|
88
|
+
lines.push("PrismoDev Sessions");
|
|
89
|
+
lines.push("");
|
|
90
|
+
if (!view.sessions.length) {
|
|
91
|
+
lines.push("No recent local agent sessions found.");
|
|
92
|
+
lines.push(view.allRepos ? "" : "Tip: add --all-repos to look across every repo on this machine.");
|
|
93
|
+
return lines.join("\n").trimEnd();
|
|
94
|
+
}
|
|
95
|
+
for (const s of view.sessions) {
|
|
96
|
+
const when = s.updatedAt ? new Date(s.updatedAt).toLocaleString() : "unknown time";
|
|
97
|
+
lines.push(`${s.tool.padEnd(12)} ${s.repo} · ${when}`);
|
|
98
|
+
lines.push(` ${formatTokenCount(s.tokens)} tokens | ~${formatTokenCount(s.wastedTokens)} wasted (${s.wastePercent}%) | ${s.risk} risk | ${CAUSE_LABELS[s.topCause] || s.topCause}`);
|
|
99
|
+
}
|
|
100
|
+
lines.push("");
|
|
101
|
+
lines.push(`Totals: ${view.totals.sessions} session(s) · ${formatTokenCount(view.totals.tokens)} tokens · ~${formatTokenCount(view.totals.wastedTokens)} likely wasted (${view.totals.wastePercent}%)`);
|
|
102
|
+
return lines.join("\n");
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function buildLocalReport(rootDir, options = {}) {
|
|
106
|
+
const { root, sessions, generatedAt } = collect(rootDir, { ...options, allRepos: options.allRepos });
|
|
107
|
+
const observed = sessions.reduce((sum, s) => sum + s.tokens, 0);
|
|
108
|
+
const wasted = sessions.reduce((sum, s) => sum + s.wastedTokens, 0);
|
|
109
|
+
|
|
110
|
+
const causeTotals = new Map();
|
|
111
|
+
for (const s of sessions) {
|
|
112
|
+
if (s.topCause === "low-signal") continue;
|
|
113
|
+
causeTotals.set(s.topCause, (causeTotals.get(s.topCause) || 0) + s.wastedTokens);
|
|
114
|
+
}
|
|
115
|
+
const topCauses = Array.from(causeTotals, ([cause, tokens]) => ({
|
|
116
|
+
cause,
|
|
117
|
+
label: CAUSE_LABELS[cause] || cause,
|
|
118
|
+
tokens,
|
|
119
|
+
})).sort((a, b) => b.tokens - a.tokens).slice(0, 5);
|
|
120
|
+
|
|
121
|
+
const repeatedReads = aggregate(sessions, (s) => s.repeatedPaths).slice(0, 5);
|
|
122
|
+
const repeatedCommands = aggregate(sessions, (s) => s.repeatedCommands).slice(0, 5);
|
|
123
|
+
const loopSessions = sessions.filter((s) => s.loopSuspicion).length;
|
|
124
|
+
const primaryCause = topCauses[0] ? topCauses[0].cause : "low-signal";
|
|
125
|
+
|
|
126
|
+
return {
|
|
127
|
+
schemaVersion: 1,
|
|
128
|
+
command: "report",
|
|
129
|
+
scannedPath: root,
|
|
130
|
+
allRepos: Boolean(options.allRepos),
|
|
131
|
+
sessions: sessions.length,
|
|
132
|
+
observedTokens: observed,
|
|
133
|
+
wastedTokens: wasted,
|
|
134
|
+
wastePercent: observed > 0 ? Math.round((wasted / observed) * 100) : 0,
|
|
135
|
+
topCauses,
|
|
136
|
+
repeatedReads,
|
|
137
|
+
repeatedCommands,
|
|
138
|
+
loopSessions,
|
|
139
|
+
nextAction: NEXT_ACTION[primaryCause] || NEXT_ACTION["low-signal"],
|
|
140
|
+
note: "Local estimate from your own session logs. Connect with `prismo connect` for verified, dollar-denominated savings.",
|
|
141
|
+
generatedAt,
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function aggregate(sessions, pick) {
|
|
146
|
+
const totals = new Map();
|
|
147
|
+
for (const s of sessions) {
|
|
148
|
+
for (const item of pick(s) || []) {
|
|
149
|
+
if (!item || !item.value) continue;
|
|
150
|
+
totals.set(item.value, (totals.get(item.value) || 0) + Number(item.count || 0));
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
return Array.from(totals, ([value, count]) => ({ value, count })).sort((a, b) => b.count - a.count);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function renderLocalReportTerminal(report) {
|
|
157
|
+
const lines = [];
|
|
158
|
+
lines.push("");
|
|
159
|
+
lines.push("PrismoDev Report");
|
|
160
|
+
lines.push("");
|
|
161
|
+
if (!report.sessions) {
|
|
162
|
+
lines.push("No recent local agent sessions found.");
|
|
163
|
+
lines.push(report.allRepos ? "" : "Tip: add --all-repos to look across every repo on this machine.");
|
|
164
|
+
return lines.join("\n").trimEnd();
|
|
165
|
+
}
|
|
166
|
+
lines.push(`Observed: ${formatTokenCount(report.observedTokens)} tokens across ${report.sessions} session(s)`);
|
|
167
|
+
lines.push(`Likely wasted: ${formatTokenCount(report.wastedTokens)} (${report.wastePercent}%)`);
|
|
168
|
+
if (report.loopSessions) lines.push(`Loop-suspect sessions: ${report.loopSessions}`);
|
|
169
|
+
if (report.topCauses.length) {
|
|
170
|
+
lines.push("");
|
|
171
|
+
lines.push("Top causes:");
|
|
172
|
+
report.topCauses.forEach((c) => lines.push(`- ${c.label}: ~${formatTokenCount(c.tokens)} tokens`));
|
|
173
|
+
}
|
|
174
|
+
if (report.repeatedReads.length) {
|
|
175
|
+
lines.push("");
|
|
176
|
+
lines.push("Most repeated reads:");
|
|
177
|
+
report.repeatedReads.forEach((r) => lines.push(`- ${r.value} (${r.count}x)`));
|
|
178
|
+
}
|
|
179
|
+
if (report.repeatedCommands.length) {
|
|
180
|
+
lines.push("");
|
|
181
|
+
lines.push("Most repeated commands:");
|
|
182
|
+
report.repeatedCommands.forEach((r) => lines.push(`- ${r.value} (${r.count}x)`));
|
|
183
|
+
}
|
|
184
|
+
lines.push("");
|
|
185
|
+
lines.push(`Next: ${report.nextAction}`);
|
|
186
|
+
lines.push(report.note);
|
|
187
|
+
return lines.join("\n");
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
return {
|
|
191
|
+
buildSessionsView,
|
|
192
|
+
renderSessionsTerminal,
|
|
193
|
+
buildLocalReport,
|
|
194
|
+
renderLocalReportTerminal,
|
|
195
|
+
};
|
|
196
|
+
};
|
package/lib/prismo-dev-scan.js
CHANGED
|
@@ -355,6 +355,18 @@ const {
|
|
|
355
355
|
runFirewall,
|
|
356
356
|
});
|
|
357
357
|
|
|
358
|
+
const {
|
|
359
|
+
buildSessionsView,
|
|
360
|
+
renderSessionsTerminal,
|
|
361
|
+
buildLocalReport,
|
|
362
|
+
renderLocalReportTerminal,
|
|
363
|
+
} = require("./prismo-dev/sessions-report")({
|
|
364
|
+
path,
|
|
365
|
+
getUsageSummary,
|
|
366
|
+
estimateWaste,
|
|
367
|
+
formatTokenCount,
|
|
368
|
+
});
|
|
369
|
+
|
|
358
370
|
const repairPlanner = require("./prismo-dev/repair-planner")({
|
|
359
371
|
fs,
|
|
360
372
|
path,
|
|
@@ -510,6 +522,10 @@ const { runCli } = require("./prismo-dev/cli")({
|
|
|
510
522
|
runDisconnect,
|
|
511
523
|
runStatus,
|
|
512
524
|
runSync,
|
|
525
|
+
buildSessionsView,
|
|
526
|
+
renderSessionsTerminal,
|
|
527
|
+
buildLocalReport,
|
|
528
|
+
renderLocalReportTerminal,
|
|
513
529
|
renderGuardTerminal,
|
|
514
530
|
runGuard,
|
|
515
531
|
REPAIR_CAUSES,
|
package/package.json
CHANGED