getprismo 0.1.54 → 0.1.56

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.
@@ -905,9 +905,9 @@ module.exports = function createAgent(deps) {
905
905
  async function runAgent(rootDir = process.cwd(), options = {}) {
906
906
  if (!options.watch) return runAgentOnce(rootDir, options);
907
907
 
908
- const intervalMs = Math.max(5, Number(options.interval || 15)) * 1000;
909
- const syncIntervalMs = Math.max(30, Number(options.syncInterval || 60)) * 1000;
910
- const detectIntervalMs = Math.max(60, Number(options.detectInterval || 300)) * 1000;
908
+ const intervalMs = Math.max(15, Number(options.interval || 60)) * 1000;
909
+ const syncIntervalMs = Math.max(60, Number(options.syncInterval || 300)) * 1000;
910
+ const detectIntervalMs = Math.max(300, Number(options.detectInterval || 900)) * 1000;
911
911
  const plannerIntervalMs = Math.max(60, Number(options.plannerInterval || 600)) * 1000;
912
912
  let running = true;
913
913
  let sleepResolve = null;
@@ -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,
@@ -353,8 +357,8 @@ function createCli(deps) {
353
357
  });
354
358
  if (result.connected && !rest.includes("--no-agent") && runConnectorInstall) {
355
359
  result.connector = runConnectorInstall(target, {
356
- interval: parsePositiveInt(intervalIndex >= 0 ? rest[intervalIndex + 1] : null, 15),
357
- syncInterval: parsePositiveInt(syncIntervalIndex >= 0 ? rest[syncIntervalIndex + 1] : null, 60),
360
+ interval: parsePositiveInt(intervalIndex >= 0 ? rest[intervalIndex + 1] : null, 60),
361
+ syncInterval: parsePositiveInt(syncIntervalIndex >= 0 ? rest[syncIntervalIndex + 1] : null, 300),
358
362
  mode: modeValue,
359
363
  dryRun: rest.includes("--dry-run"),
360
364
  });
@@ -385,8 +389,8 @@ function createCli(deps) {
385
389
  let result;
386
390
  if (action === "install") {
387
391
  result = runConnectorInstall(target, {
388
- interval: parsePositiveInt(intervalIndex >= 0 ? rest[intervalIndex + 1] : null, 15),
389
- syncInterval: parsePositiveInt(syncIntervalIndex >= 0 ? rest[syncIntervalIndex + 1] : null, 60),
392
+ interval: parsePositiveInt(intervalIndex >= 0 ? rest[intervalIndex + 1] : null, 60),
393
+ syncInterval: parsePositiveInt(syncIntervalIndex >= 0 ? rest[syncIntervalIndex + 1] : null, 300),
390
394
  mode: modeValue,
391
395
  dryRun: rest.includes("--dry-run"),
392
396
  });
@@ -458,9 +462,9 @@ function createCli(deps) {
458
462
  autoDetect: !rest.includes("--no-detect"),
459
463
  planRepairs: !rest.includes("--no-planner"),
460
464
  noSync: rest.includes("--no-sync"),
461
- interval: parsePositiveInt(intervalIndex >= 0 ? rest[intervalIndex + 1] : null, 15),
462
- syncInterval: parsePositiveInt(syncIntervalIndex >= 0 ? rest[syncIntervalIndex + 1] : null, 60),
463
- detectInterval: parsePositiveInt(detectIntervalIndex >= 0 ? rest[detectIntervalIndex + 1] : null, 300),
465
+ interval: parsePositiveInt(intervalIndex >= 0 ? rest[intervalIndex + 1] : null, 60),
466
+ syncInterval: parsePositiveInt(syncIntervalIndex >= 0 ? rest[syncIntervalIndex + 1] : null, 300),
467
+ detectInterval: parsePositiveInt(detectIntervalIndex >= 0 ? rest[detectIntervalIndex + 1] : null, 900),
464
468
  plannerInterval: parsePositiveInt(plannerIntervalIndex >= 0 ? rest[plannerIntervalIndex + 1] : null, 600),
465
469
  limit: parsePositiveInt(limitIndex >= 0 ? rest[limitIndex + 1] : null, 5),
466
470
  syncLimit: parsePositiveInt(limitIndex >= 0 ? rest[limitIndex + 1] : null, 20),
@@ -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");
@@ -378,6 +378,13 @@ module.exports = function createCloudSync(deps) {
378
378
  };
379
379
  }
380
380
 
381
+ function sessionsFingerprint(sessions) {
382
+ const parts = (sessions || [])
383
+ .map((s) => `${s.sessionId || ""}:${s.updatedAt || ""}:${s.tokens?.display || 0}:${s.waste?.wastedTokens || 0}`)
384
+ .sort();
385
+ return `${parts.length}|${parts.join("|")}`;
386
+ }
387
+
381
388
  async function runSync(rootDir = process.cwd(), options = {}) {
382
389
  const config = loadConfig();
383
390
  const payload = buildSyncPayload(rootDir, {
@@ -408,16 +415,29 @@ module.exports = function createCloudSync(deps) {
408
415
  };
409
416
  }
410
417
  const endpoint = options.endpoint || `${String(config.apiUrl || DEFAULT_API_URL).replace(/\/$/, "")}/v1/dev/sessions/sync`;
418
+
419
+ // Skip the upload when nothing changed since the last sync. The connector
420
+ // re-runs on a short interval, but session data only changes when the user
421
+ // is actually coding, so most syncs are no-ops; sending them anyway is the
422
+ // main driver of needless backend load and network egress.
423
+ const fingerprint = sessionsFingerprint(payload.sessions);
424
+ if (!options.force) {
425
+ const previous = readJson(statePath());
426
+ if (previous && previous.fingerprint === fingerprint) {
427
+ return { schemaVersion: 1, command: "sync", synced: true, skipped: true, reason: "unchanged", statePath: statePath(), aggregate: payload.aggregate };
428
+ }
429
+ }
430
+
411
431
  const response = await requestJson("POST", endpoint, config.token, payload, options.timeoutMs || 8000);
412
- const state = {
432
+ writeJson(statePath(), {
413
433
  schemaVersion: 1,
414
434
  lastSyncAt: new Date().toISOString(),
415
435
  endpoint,
436
+ fingerprint,
416
437
  repo: payload.repo,
417
438
  aggregate: payload.aggregate,
418
439
  response: response.data,
419
- };
420
- writeJson(statePath(), state);
440
+ });
421
441
  return {
422
442
  schemaVersion: 1,
423
443
  command: "sync",
@@ -78,9 +78,12 @@ module.exports = function createConnector(deps) {
78
78
 
79
79
  function writeRunner(rootDir, options = {}) {
80
80
  const root = path.resolve(rootDir || process.cwd());
81
- const interval = Math.max(5, Number(options.interval || 15));
82
- const syncInterval = Math.max(30, Number(options.syncInterval || 60));
83
- const detectInterval = Math.max(60, Number(options.detectInterval || 300));
81
+ // Background poll cadence. Kept conservative so an always-on connector
82
+ // stays light on API/database traffic; agent sessions change on the order
83
+ // of minutes, not seconds, so fast polling only adds idle load.
84
+ const interval = Math.max(15, Number(options.interval || 60));
85
+ const syncInterval = Math.max(60, Number(options.syncInterval || 300));
86
+ const detectInterval = Math.max(300, Number(options.detectInterval || 900));
84
87
  const mode = options.mode || "autopilot";
85
88
  fs.mkdirSync(connectorDir(), { recursive: true });
86
89
  const command = `${BACKGROUND_COMMAND} agent --watch --interval ${interval} --sync-interval ${syncInterval} --detect-interval ${detectInterval} --mode ${shellEscape(mode)} ${shellEscape(root)}`;
@@ -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.
@@ -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
+ };
@@ -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "getprismo",
3
- "version": "0.1.54",
3
+ "version": "0.1.56",
4
4
  "description": "Local AI coding workflow scanner for Codex, Claude Code, Cursor, and token-waste diagnostics.",
5
5
  "license": "MIT",
6
6
  "homepage": "https://github.com/shanirsh/prismodev#readme",