getprismo 0.1.43 → 0.1.44

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 CHANGED
@@ -4,7 +4,7 @@
4
4
  [![npm downloads](https://img.shields.io/npm/dw/getprismo.svg)](https://www.npmjs.com/package/getprismo)
5
5
  [![license: MIT](https://img.shields.io/badge/license-MIT-green.svg)](LICENSE)
6
6
 
7
- an autonomous cost agent for ai coding. it finds token waste, fixes the cause, verifies the fix against your next sessions in dollars, and escalates or backs off based on what actually worked. unattended.
7
+ an agent control plane for ai coding. it watches local coding agents, finds token waste, stages or executes safe interventions, verifies the fix against your next sessions in dollars, and escalates or backs off based on what actually worked. unattended.
8
8
 
9
9
  ```bash
10
10
  npx getprismo doctor
@@ -20,7 +20,7 @@ ai coding agents (claude code, codex, cursor) burn tokens on things that don't h
20
20
 
21
21
  most developers don't realize this is happening until the bill arrives or the agent starts looping.
22
22
 
23
- prismodev catches it before, during, and after.
23
+ prismodev gives you a control plane for it before, during, and after.
24
24
 
25
25
  ---
26
26
 
@@ -39,12 +39,13 @@ postmortem npx getprismo replay
39
39
  weekly receipt npx getprismo digest
40
40
  workspace agent npx getprismo agent --watch
41
41
  agent-native npx getprismo mcp
42
+ optional bridge npx getprismo bridge
42
43
  ```
43
44
 
44
45
  **doctor** diagnoses the repo, applies safe fixes, and shows the before/after score.
45
46
  **repair** runs the targeted fix for one waste cause; `repair auto` lets the planner pick.
46
47
  **enforce** turns the context firewall into actual runtime enforcement via Claude Code hooks.
47
- **digest** prints the verified-savings summary for the week, ready to paste into Slack.
48
+ **digest** prints the launch report: verified saved tokens/dollars first, live prevention clearly labeled as estimated, ready to post or paste into Slack.
48
49
  **guard** runs live guardrails, context throttle, rescue prompts, context firewall, and dashboard-ready prevention events.
49
50
  **watch** monitors context pressure live and is the lower-level diagnostic view behind guard.
50
51
  **receipt** explains what repeated, what output dominated, what artifacts leaked, what likely influenced the run, and a heuristic context-efficiency score.
@@ -52,6 +53,7 @@ agent-native npx getprismo mcp
52
53
  **shield** runs noisy commands without dumping full output back into the agent context.
53
54
  **agent** connects Prismo Cloud to your local repo so dashboard actions can safely run on this machine.
54
55
  **mcp** exposes PrismoDev as local tools so compatible agents can scan, search shield output, and request scoped context directly.
56
+ **bridge** explains the optional tighter control layer for teams that want Prismo closer to the agent execution path.
55
57
 
56
58
  ---
57
59
 
@@ -107,6 +109,24 @@ enforcement fails open — malformed events or missing policy files allow the ca
107
109
 
108
110
  ---
109
111
 
112
+ ## new: optional bridge mode
113
+
114
+ the background connector is the default. it observes local sessions, syncs safe aggregate telemetry, applies queued repairs, verifies the next sessions, and shows live events in the dashboard. it does not sit in front of every agent action.
115
+
116
+ bridge mode is optional context for teams that want Prismo closer to the agent execution path, especially for live loop stopping:
117
+
118
+ ```bash
119
+ npx getprismo bridge
120
+ ```
121
+
122
+ - **Claude Code**: hard-block capable today through `npx getprismo enforce install`, which adds a `PreToolUse` hook that can deny blocked-context reads and repeated command loops before they run.
123
+ - **Codex**: visible and repairable through local session logs, guardrails, shield, and MCP. universal hard-blocking needs Codex to run through a wrapper/bridge or expose a pre-tool hook.
124
+ - **Cursor**: visible and repairable through local telemetry and staged repairs. universal hard-blocking needs Cursor to run through a wrapper/bridge or expose a pre-tool hook.
125
+
126
+ that is why Prismo is not described as a proxy by default. connector mode is safer and simpler; bridge mode is the opt-in path when stronger live interception matters more than staying fully out of the agent execution path.
127
+
128
+ ---
129
+
110
130
  ## what prismodev catches
111
131
 
112
132
  - missing `.claudeignore` / `.cursorignore` (the biggest single fix for most repos)
@@ -846,6 +866,7 @@ no install needed. npx runs it directly.
846
866
  | `shield` | run noisy commands while keeping full output out of chat |
847
867
  | `agent` | claim and execute safe Prismo Cloud workspace actions locally |
848
868
  | `mcp` | expose PrismoDev tools over local MCP stdio |
869
+ | `bridge` | explain optional bridge mode and live interception levels for Claude Code, Codex, and Cursor |
849
870
  | `setup` | detect tools, logs, proxy readiness |
850
871
  | `usage` | show raw session token usage |
851
872
  | `init` | add npm scripts and .prismo/README.md |
@@ -1156,6 +1177,7 @@ npx getprismo --version
1156
1177
  npx getprismo doctor --help
1157
1178
  npx getprismo repair --help
1158
1179
  npx getprismo enforce --help
1180
+ npx getprismo bridge --help
1159
1181
  npx getprismo watch --help
1160
1182
  npx getprismo shield --help
1161
1183
  npx getprismo mcp --help
@@ -15,6 +15,7 @@ module.exports = function createAgent(deps) {
15
15
  openUrl,
16
16
  repairExecutors,
17
17
  repairPlanner,
18
+ getUsageSummary,
18
19
  } = deps;
19
20
 
20
21
  const DEFAULT_WORKSPACE_URL = "https://getprismo.dev/dashboard/dev";
@@ -117,6 +118,29 @@ module.exports = function createAgent(deps) {
117
118
  return rootDir;
118
119
  }
119
120
 
121
+ function repoPayload(rootDir) {
122
+ return { pathBasename: path.basename(path.resolve(rootDir || process.cwd())) };
123
+ }
124
+
125
+ function safeReadJson(filePath) {
126
+ try {
127
+ if (!fs.existsSync(filePath)) return null;
128
+ return JSON.parse(fs.readFileSync(filePath, "utf8"));
129
+ } catch {
130
+ return null;
131
+ }
132
+ }
133
+
134
+ function enforceStatePath(rootDir) {
135
+ return path.join(path.resolve(rootDir || process.cwd()), ".prismo", "enforce-state.json");
136
+ }
137
+
138
+ function summarizeCommand(command) {
139
+ const value = String(command || "").replace(/\s+/g, " ").trim();
140
+ if (!value) return "a repeated command";
141
+ return value.length > 90 ? `${value.slice(0, 87)}...` : value;
142
+ }
143
+
120
144
  async function updateAction(config, actionId, payload, options = {}) {
121
145
  const endpoint = options.endpoint || `${apiBase(config)}/v1/dev/workspace/actions/${actionId}`;
122
146
  const response = await requestJson("PATCH", endpoint, config.token, payload, options.timeoutMs || 15000);
@@ -142,6 +166,103 @@ module.exports = function createAgent(deps) {
142
166
  return response.data;
143
167
  }
144
168
 
169
+ async function sendLiveEvent(config, event, options = {}) {
170
+ const endpoint = options.liveEventEndpoint || `${apiBase(config)}/v1/dev/workspace/live-events`;
171
+ try {
172
+ const body = {
173
+ phase: "watching",
174
+ eventType: "status",
175
+ severity: "info",
176
+ occurredAt: new Date().toISOString(),
177
+ ...event,
178
+ };
179
+ await requestJson("POST", endpoint, config.token, body, options.timeoutMs || 5000);
180
+ return true;
181
+ } catch (_) {
182
+ return false;
183
+ }
184
+ }
185
+
186
+ async function publishClaudeLoopStops(config, rootDir, repo, options = {}) {
187
+ const state = safeReadJson(enforceStatePath(rootDir));
188
+ const stops = Array.isArray(state?.loopStops) ? state.loopStops : [];
189
+ if (!stops.length) return 0;
190
+ let sent = 0;
191
+ for (const stop of stops.slice(0, 10)) {
192
+ const ok = await sendLiveEvent(config, {
193
+ eventId: stop.eventId || `claude-loop-stop-${stop.at || Date.now()}`,
194
+ phase: "stopped",
195
+ eventType: "loop_stopped",
196
+ severity: "success",
197
+ headline: "Stopped a Claude Code retry loop",
198
+ detail: `${summarizeCommand(stop.command)} was blocked after ${stop.failures || stop.attempts || 3} repeated ${stop.failures ? "failure" : "attempt"}${(stop.failures || stop.attempts || 3) === 1 ? "" : "s"}.`,
199
+ repo,
200
+ targetCause: "context-loop",
201
+ tokensPrevented: Number(stop.estimatedTokensSaved || 0),
202
+ occurredAt: stop.at || new Date().toISOString(),
203
+ payload: {
204
+ tool: "claude-code",
205
+ reason: stop.reason || "repeated-command",
206
+ sessionId: stop.sessionId || null,
207
+ rawPrompts: false,
208
+ rawCode: false,
209
+ rawStdout: false,
210
+ rawStderr: false,
211
+ },
212
+ }, options);
213
+ if (ok) sent += 1;
214
+ }
215
+ return sent;
216
+ }
217
+
218
+ async function publishAgentLoopSignals(config, rootDir, repo, options = {}) {
219
+ if (!getUsageSummary) return 0;
220
+ let summary = null;
221
+ try {
222
+ summary = getUsageSummary({ cwd: rootDir, limit: options.loopSignalLimit || 8, tool: "all" });
223
+ } catch {
224
+ return 0;
225
+ }
226
+ const sessions = Array.isArray(summary?.sessions) ? summary.sessions : [];
227
+ let sent = 0;
228
+ for (const session of sessions) {
229
+ const tool = String(session.tool || "unknown").toLowerCase();
230
+ if (!tool.includes("codex") && !tool.includes("cursor")) continue;
231
+ const repeated = Array.isArray(session.repeatedCommands)
232
+ ? session.repeatedCommands.filter((item) => Number(item.count || 0) >= 3)
233
+ : [];
234
+ if (!session.loopSuspicion && repeated.length === 0) continue;
235
+ const command = repeated[0]?.value || null;
236
+ const count = Number(repeated[0]?.count || 0);
237
+ const eventTool = tool.includes("codex") ? "codex" : "cursor";
238
+ const sessionId = String(session.sessionId || session.updatedAt || eventTool);
239
+ const ok = await sendLiveEvent(config, {
240
+ eventId: `${eventTool}-loop-detected-${sessionId.replace(/[^a-z0-9_-]/gi, "").slice(0, 48)}-${count || "suspicion"}`,
241
+ phase: "detected",
242
+ eventType: "loop_detected",
243
+ severity: "warning",
244
+ headline: `${eventTool === "codex" ? "Codex" : "Cursor"} loop pattern detected`,
245
+ detail: command
246
+ ? `${summarizeCommand(command)} appeared ${count} times. Prismo can stage guard or shield repairs, but this integration cannot hard-block that agent yet.`
247
+ : "Repeated tool activity suggests a loop. Prismo can stage guard repairs, but this integration cannot hard-block that agent yet.",
248
+ repo,
249
+ targetCause: "context-loop",
250
+ occurredAt: session.updatedAt || new Date().toISOString(),
251
+ payload: {
252
+ tool: eventTool,
253
+ repeatedCommandCount: count,
254
+ loopSuspicion: Boolean(session.loopSuspicion),
255
+ rawPrompts: false,
256
+ rawCode: false,
257
+ rawStdout: false,
258
+ rawStderr: false,
259
+ },
260
+ }, options);
261
+ if (ok) sent += 1;
262
+ }
263
+ return sent;
264
+ }
265
+
145
266
  function runAutoDetect(rootDir, options = {}) {
146
267
  const mode = options.mode || "autopilot";
147
268
  const startedAt = new Date().toISOString();
@@ -417,23 +538,72 @@ module.exports = function createAgent(deps) {
417
538
 
418
539
  const mode = options.mode || "autopilot";
419
540
  const pollTime = new Date().toISOString();
541
+ const repo = repoPayload(rootDir);
420
542
 
421
543
  try {
422
544
  await sendHeartbeat(config, { mode, status: "online", lastPollAt: pollTime }, options);
423
545
  } catch (_) {}
546
+ await sendLiveEvent(config, {
547
+ eventId: `heartbeat-${repo.pathBasename}-${pollTime.slice(0, 16)}`,
548
+ phase: "watching",
549
+ eventType: "heartbeat",
550
+ headline: "Connector is watching this repo",
551
+ detail: `Mode: ${mode}. Polling for safe repairs and syncing telemetry.`,
552
+ repo,
553
+ }, options);
554
+ await publishClaudeLoopStops(config, rootDir, repo, options);
555
+ await publishAgentLoopSignals(config, rootDir, repo, options);
424
556
 
425
557
  let autoDetectResult = null;
426
558
  if (options.autoDetect) {
427
559
  autoDetectResult = runAutoDetect(rootDir, { mode });
428
560
  await reportAutoDetect(config, autoDetectResult, options);
561
+ await sendLiveEvent(config, {
562
+ eventId: `auto-detect-${repo.pathBasename}-${autoDetectResult.completedAt || pollTime}`,
563
+ phase: autoDetectResult.applied ? "fixed" : "detected",
564
+ eventType: "auto_detect",
565
+ severity: autoDetectResult.findings.length ? "warning" : "info",
566
+ headline: autoDetectResult.applied ? "Auto-detect applied safe context fixes" : "Auto-detect scanned the repo",
567
+ detail: `Score: ${autoDetectResult.score ?? "unknown"}/100. Findings: ${autoDetectResult.findings.length}. Generated ${autoDetectResult.generatedFiles.length} file(s).`,
568
+ repo,
569
+ payload: {
570
+ score: autoDetectResult.score,
571
+ findings: autoDetectResult.findings.length,
572
+ generatedFiles: autoDetectResult.generatedFiles,
573
+ rawPrompts: false,
574
+ rawCode: false,
575
+ },
576
+ }, options);
429
577
  }
430
578
 
431
579
  const actions = await claimActions(config, options);
580
+ if (actions.length > 0) {
581
+ await sendLiveEvent(config, {
582
+ eventId: `actions-claimed-${repo.pathBasename}-${pollTime}`,
583
+ phase: "detected",
584
+ eventType: "action_claimed",
585
+ severity: "info",
586
+ headline: `Claimed ${actions.length} repair action${actions.length === 1 ? "" : "s"}`,
587
+ detail: "The local connector is about to apply queued repairs from the workspace.",
588
+ repo,
589
+ }, options);
590
+ }
432
591
  const results = [];
433
592
  for (const action of actions) {
434
593
  if (TERMINAL_STATUSES.has(action.status)) continue;
435
594
 
436
595
  if (mode === "observe") {
596
+ await sendLiveEvent(config, {
597
+ eventId: `action-observed-${action.id}`,
598
+ phase: "detected",
599
+ eventType: "action_observed",
600
+ headline: action.label || "Repair observed",
601
+ detail: "Observe mode is on, so Prismo did not execute this repair.",
602
+ repo,
603
+ actionId: action.id,
604
+ actionType: action.actionType,
605
+ targetCause: action.targetCause,
606
+ }, options);
437
607
  results.push({ id: action.id, label: action.label, status: "observed", statusMessage: "Agent is in observe mode. Action not executed." });
438
608
  continue;
439
609
  }
@@ -443,6 +613,17 @@ module.exports = function createAgent(deps) {
443
613
  status: "pending_approval",
444
614
  statusMessage: "Agent recommends this action. Waiting for approval in workspace.",
445
615
  }, options);
616
+ await sendLiveEvent(config, {
617
+ eventId: `action-suggested-${action.id}`,
618
+ phase: "detected",
619
+ eventType: "action_suggested",
620
+ headline: action.label || "Repair needs approval",
621
+ detail: "Suggest mode is on, so Prismo is waiting for dashboard approval.",
622
+ repo,
623
+ actionId: action.id,
624
+ actionType: action.actionType,
625
+ targetCause: action.targetCause,
626
+ }, options);
446
627
  results.push({ id: action.id, label: action.label, status: "pending_approval", statusMessage: "Suggested; awaiting approval." });
447
628
  continue;
448
629
  }
@@ -451,8 +632,39 @@ module.exports = function createAgent(deps) {
451
632
  status: "running",
452
633
  statusMessage: "Running locally through PrismoDev agent.",
453
634
  }, options);
635
+ await sendLiveEvent(config, {
636
+ eventId: `action-running-${action.id}`,
637
+ phase: "detected",
638
+ eventType: "action_running",
639
+ headline: action.label || "Repair is running locally",
640
+ detail: "The connector is applying this repair in your repo now.",
641
+ repo,
642
+ actionId: action.id,
643
+ actionType: action.actionType,
644
+ targetCause: action.targetCause,
645
+ }, options);
454
646
  const result = await executeAction(action, rootDir, { ...options, _config: config });
455
647
  await updateAction(config, action.id, result, options);
648
+ await sendLiveEvent(config, {
649
+ eventId: `action-${result.status}-${action.id}`,
650
+ phase: result.status === "completed" ? "fixed" : "detected",
651
+ eventType: "action_completed",
652
+ severity: result.status === "completed" ? "success" : "warning",
653
+ headline: result.status === "completed" ? `${action.label || "Repair"} applied` : `${action.label || "Repair"} did not complete`,
654
+ detail: result.statusMessage || null,
655
+ repo,
656
+ actionId: action.id,
657
+ actionType: action.actionType,
658
+ targetCause: action.targetCause,
659
+ payload: {
660
+ status: result.status,
661
+ result: result.result || null,
662
+ rawPrompts: false,
663
+ rawCode: false,
664
+ rawStdout: false,
665
+ rawStderr: false,
666
+ },
667
+ }, options);
456
668
  results.push({ id: action.id, label: action.label, ...result });
457
669
  }
458
670
 
@@ -478,6 +690,27 @@ module.exports = function createAgent(deps) {
478
690
  : false;
479
691
  plannerResult.registered = registered;
480
692
  if (!registered) await reportPlanner(config, plannerResult, mode, options);
693
+ await sendLiveEvent(config, {
694
+ eventId: plannerResult.decision
695
+ ? `self-repair-${plannerResult.decision.cause}-${plannerResult.generatedAt || pollTime}`
696
+ : `self-repair-none-${repo.pathBasename}-${pollTime.slice(0, 16)}`,
697
+ phase: plannerResult.executed ? "fixed" : "watching",
698
+ eventType: "self_repair",
699
+ severity: plannerResult.decision ? "info" : "info",
700
+ headline: plannerResult.decision ? `Self-repair checked ${plannerResult.decision.cause}` : "Self-repair found nothing to run",
701
+ detail: plannerResult.decision
702
+ ? (plannerResult.outcome?.statusMessage || plannerResult.decision.reason)
703
+ : "Current sessions do not need another automated repair.",
704
+ repo,
705
+ targetCause: plannerResult.decision?.cause || null,
706
+ payload: {
707
+ decision: plannerResult.decision || null,
708
+ executed: Boolean(plannerResult.executed),
709
+ registered: Boolean(plannerResult.registered),
710
+ rawPrompts: false,
711
+ rawCode: false,
712
+ },
713
+ }, options);
481
714
  } catch (error) {
482
715
  plannerResult = { error: error && error.message ? error.message : String(error) };
483
716
  }
@@ -493,11 +726,39 @@ module.exports = function createAgent(deps) {
493
726
  estimatedWastedTokens: Number(result.aggregate?.estimatedWastedTokens || 0),
494
727
  wastePercent: Number(result.aggregate?.wastePercent || 0),
495
728
  };
729
+ await sendLiveEvent(config, {
730
+ eventId: `sync-${repo.pathBasename}-${new Date().toISOString().slice(0, 16)}`,
731
+ phase: "watching",
732
+ eventType: "sync",
733
+ severity: result.synced ? "info" : "warning",
734
+ headline: result.synced ? "Telemetry synced" : "Telemetry sync did not complete",
735
+ detail: result.synced
736
+ ? `${syncResult.sessions} session(s), ${syncResult.estimatedWastedTokens.toLocaleString()} likely wasted tokens.`
737
+ : (result.error || "Sync did not complete."),
738
+ repo,
739
+ tokensObserved: Number(result.aggregate?.displayTokens || result.aggregate?.contextTokens || result.aggregate?.exactTokens || 0),
740
+ payload: {
741
+ aggregate: result.aggregate || null,
742
+ rawPrompts: false,
743
+ rawCode: false,
744
+ rawStdout: false,
745
+ rawStderr: false,
746
+ },
747
+ }, options);
496
748
  } catch (error) {
497
749
  syncResult = {
498
750
  synced: false,
499
751
  error: error && error.message ? error.message : String(error),
500
752
  };
753
+ await sendLiveEvent(config, {
754
+ eventId: `sync-failed-${repo.pathBasename}-${new Date().toISOString().slice(0, 16)}`,
755
+ phase: "detected",
756
+ eventType: "sync_failed",
757
+ severity: "warning",
758
+ headline: "Telemetry sync failed",
759
+ detail: syncResult.error,
760
+ repo,
761
+ }, options);
501
762
  }
502
763
  }
503
764
 
@@ -682,6 +943,7 @@ module.exports = function createAgent(deps) {
682
943
  runAgentOnce,
683
944
  runAutoDetect,
684
945
  sendHeartbeat,
946
+ sendLiveEvent,
685
947
  updateAction,
686
948
  VALID_MODES,
687
949
  };
@@ -6,7 +6,7 @@ const VALID_COMMANDS = new Set([
6
6
  "connect", "sync", "status", "disconnect", "agent", "connector", "setup", "scan", "digest",
7
7
  "optimize", "context", "cc", "cursor", "receipt", "instructions",
8
8
  "timeline", "replay", "boundaries", "usage", "guard", "watch", "demo", "repair",
9
- "enforce", "hook",
9
+ "enforce", "bridge", "hook",
10
10
  ]);
11
11
 
12
12
  function parseTokenBudget(value) {
@@ -151,7 +151,7 @@ function createCli(deps) {
151
151
  return;
152
152
  }
153
153
  if (!VALID_COMMANDS.has(command)) {
154
- throw new Error(`Unknown command: ${command}. Try: prismo connect, prismo connector, prismo agent, prismo guard, prismo sync, prismo doctor, prismo watch, prismo receipt, prismo benchmark, prismo shield, prismo mcp, prismo firewall, prismo init, prismo scan, prismo optimize, prismo context, prismo cc, prismo cursor, or prismo usage`);
154
+ throw new Error(`Unknown command: ${command}. Try: prismo connect, prismo connector, prismo bridge, prismo agent, prismo guard, prismo sync, prismo doctor, prismo watch, prismo receipt, prismo benchmark, prismo shield, prismo mcp, prismo firewall, prismo init, prismo scan, prismo optimize, prismo context, prismo cc, prismo cursor, or prismo usage`);
155
155
  }
156
156
 
157
157
  if (command === "demo") {
@@ -851,6 +851,70 @@ function createCli(deps) {
851
851
  return;
852
852
  }
853
853
 
854
+ if (command === "bridge") {
855
+ const json = rest.includes("--json");
856
+ const target = getPositionals(rest, new Set())[0] || process.cwd();
857
+ const result = {
858
+ schemaVersion: 1,
859
+ command: "bridge",
860
+ optional: true,
861
+ root: path.resolve(target),
862
+ why: "The connector observes, repairs, and verifies by default. Bridge mode is optional when you want Prismo closer to the agent execution path so loops and blocked context can be stopped earlier.",
863
+ defaultMode: {
864
+ name: "connector",
865
+ command: `${NPX_COMMAND} connector install`,
866
+ behavior: "syncs telemetry, applies safe repairs, and shows live events without sitting in front of every agent action",
867
+ },
868
+ agents: [
869
+ {
870
+ tool: "Claude Code",
871
+ level: "hard-block",
872
+ command: `${NPX_COMMAND} enforce install`,
873
+ behavior: "uses Claude hooks to deny blocked-context reads and repeated failing command loops before they run",
874
+ },
875
+ {
876
+ tool: "Codex",
877
+ level: "detect-and-repair",
878
+ command: `${NPX_COMMAND} mcp`,
879
+ behavior: "Prismo can detect loops from local sessions and expose tools via MCP/shield; universal hard-blocking needs a Codex pre-tool hook or wrapper",
880
+ },
881
+ {
882
+ tool: "Cursor",
883
+ level: "detect-and-repair",
884
+ command: `${NPX_COMMAND} mcp`,
885
+ behavior: "Prismo can detect loop patterns from Cursor telemetry and stage repairs; universal hard-blocking needs a Cursor pre-tool hook or wrapper",
886
+ },
887
+ ],
888
+ privacy: {
889
+ rawPrompts: false,
890
+ rawCode: false,
891
+ rawStdout: false,
892
+ rawStderr: false,
893
+ },
894
+ };
895
+ if (json) console.log(JSON.stringify(result, null, 2));
896
+ else {
897
+ console.log("");
898
+ console.log("PrismoDev Bridge");
899
+ console.log("");
900
+ console.log("Optional control layer for teams that want stronger live interception.");
901
+ console.log("");
902
+ console.log(`Default connector: ${result.defaultMode.command}`);
903
+ console.log(` ${result.defaultMode.behavior}`);
904
+ console.log("");
905
+ console.log("Agent control levels");
906
+ result.agents.forEach((agent) => {
907
+ console.log(`- ${agent.tool}: ${agent.level}`);
908
+ console.log(` ${agent.behavior}`);
909
+ console.log(` Start with: ${agent.command}`);
910
+ });
911
+ console.log("");
912
+ console.log("Why this exists");
913
+ console.log(` ${result.why}`);
914
+ }
915
+ return;
916
+ }
917
+
854
918
  if (command === "usage" || command === "watch") {
855
919
  const json = rest.includes("--json");
856
920
  const knownTools = new Set(["codex", "claude", "cursor", "all"]);
@@ -458,7 +458,10 @@ module.exports = function createCloudSync(deps) {
458
458
  lines.push(`Could not load digest${result.error ? `: ${result.error}` : "."}`);
459
459
  return lines.join("\n");
460
460
  }
461
- (result.digest.lines || [result.digest.headline]).forEach((line) => lines.push(line));
461
+ const reportLines = result.digest.launchReportLines && result.digest.launchReportLines.length
462
+ ? result.digest.launchReportLines
463
+ : (result.digest.lines || [result.digest.headline]);
464
+ reportLines.forEach((line) => lines.push(line));
462
465
  if (result.localEnforcement) {
463
466
  lines.push(`Local enforcement: ${result.localEnforcement.denials} denial(s), ~${result.localEnforcement.estimatedTokensSaved.toLocaleString()} tokens kept out of context on this machine.`);
464
467
  }
@@ -8,9 +8,9 @@ module.exports = function createConnector(deps) {
8
8
  } = deps;
9
9
 
10
10
  const LABEL = "dev.getprismo.connector";
11
- const BACKGROUND_COMMAND = String(NPX_COMMAND || "").includes(" -y ")
11
+ const BACKGROUND_COMMAND = process.env.PRISMO_CONNECTOR_COMMAND || (String(NPX_COMMAND || "").includes(" -y ")
12
12
  ? NPX_COMMAND
13
- : "npx -y getprismo@latest";
13
+ : "npx -y getprismo@latest");
14
14
 
15
15
  function prismoHome() {
16
16
  return process.env.PRISMO_HOME || path.join(os.homedir(), ".prismo");
@@ -87,6 +87,7 @@ module.exports = function createConnector(deps) {
87
87
  const contents = [
88
88
  "#!/bin/sh",
89
89
  "set -eu",
90
+ "export PATH=\"/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin:$PATH\"",
90
91
  `cd ${shellEscape(root)}`,
91
92
  `exec ${command}`,
92
93
  "",
@@ -91,6 +91,27 @@ module.exports = function createEnforce(deps) {
91
91
  writeState(root, state);
92
92
  }
93
93
 
94
+ function recordLoopStop(root, state, payload) {
95
+ const loopStops = Array.isArray(state.loopStops) ? state.loopStops : [];
96
+ const at = new Date().toISOString();
97
+ const command = String(payload.command || "").slice(0, 240);
98
+ const reason = payload.reason || "repeated-command";
99
+ const sessionId = payload.sessionId || "unknown";
100
+ const eventId = `claude-loop-stop-${sessionId}-${Buffer.from(`${reason}:${command}`).toString("base64").replace(/[^a-z0-9]/gi, "").slice(0, 24)}-${at.slice(0, 16)}`;
101
+ state.loopStops = [{
102
+ eventId,
103
+ at,
104
+ tool: "claude-code",
105
+ command,
106
+ reason,
107
+ failures: payload.failures || 0,
108
+ attempts: payload.attempts || 0,
109
+ estimatedTokensSaved: LOOP_DENY_TOKEN_ESTIMATE,
110
+ sessionId,
111
+ }, ...loopStops].slice(0, DENIAL_LOG_LIMIT);
112
+ writeState(root, state);
113
+ }
114
+
94
115
  function estimateBlockedFileTokens(root, target) {
95
116
  try {
96
117
  const fullPath = path.isAbsolute(target) ? target : path.join(root, target);
@@ -184,6 +205,13 @@ module.exports = function createEnforce(deps) {
184
205
  const deniedByAttempts = record.outcomes === 0 && record.attempts >= MAX_IDENTICAL_COMMANDS;
185
206
  if (deniedByFailures || deniedByAttempts) {
186
207
  recordDenial(root, state, "loop", command, LOOP_DENY_TOKEN_ESTIMATE);
208
+ recordLoopStop(root, state, {
209
+ command,
210
+ sessionId,
211
+ reason: deniedByFailures ? "repeated-failing-command" : "repeated-identical-command",
212
+ failures: record.failures,
213
+ attempts: record.attempts,
214
+ });
187
215
  const observation = deniedByFailures
188
216
  ? `this exact command has already failed ${record.failures} times in this session`
189
217
  : `this exact command has already run ${record.attempts} times in this session`;
@@ -14,6 +14,7 @@ Usage:
14
14
  prismo mcp doctor [--json] [path]
15
15
  prismo connect [--json] [--token TOKEN] [--api-url URL] [--org ORG] [--user USER] [--device NAME]
16
16
  prismo connector [status|install|start|stop|uninstall] [--json] [--interval N] [--sync-interval N] [--mode observe|suggest|autopilot] [path]
17
+ prismo bridge [--json] [path]
17
18
  prismo sync [--json] [--dry-run] [--watch] [--interval N] [--limit N] [--tool all|codex|claude|cursor] [path]
18
19
  prismo status [--json]
19
20
  prismo digest [--json] [--days N]
@@ -49,9 +50,10 @@ Commands:
49
50
  mcp Start a local MCP server exposing Prismo tools over stdio.
50
51
  connect Store a PrismoDev cloud connection for seamless dashboard sync.
51
52
  connector Install or manage the background Prismo Workspace connector.
53
+ bridge Explain optional agent bridge mode and live interception levels.
52
54
  sync Send safe aggregate local agent telemetry to Prismo; use --watch for background-style sync.
53
55
  status Show local PrismoDev connection and last sync state.
54
- digest Print the verified-savings summary for the last N days, ready to paste into Slack.
56
+ digest Print the launch report: verified saved tokens/dollars first, with live prevention labeled estimated.
55
57
  disconnect Remove the local PrismoDev cloud connection.
56
58
  agent Claim and execute safe workspace actions queued from Prismo Cloud.
57
59
  scan Run PrismoDev for Claude Code, Codex, Cursor, and AI coding workflows.
@@ -520,6 +522,24 @@ Output:
520
522
  On macOS this creates a LaunchAgent so Prismo stays online after the terminal closes.
521
523
  The connector claims safe repairs queued from Prismo Cloud, runs them locally, continuously syncs aggregate telemetry, and reports status back.
522
524
  It does not upload prompts, source code, file contents, stdout, stderr, or full command logs.`,
525
+ bridge: `PrismoDev Bridge
526
+
527
+ Usage:
528
+ prismo bridge [--json] [path]
529
+
530
+ Examples:
531
+ prismo bridge
532
+ prismo bridge --json
533
+
534
+ What this explains:
535
+ Bridge mode is optional. The connector is still the default: it observes local agent sessions, applies safe queued repairs, verifies impact, and shows live events without sitting in front of every agent action.
536
+
537
+ Agent control levels:
538
+ Claude Code can use "prismo enforce install" for hard-blocking through PreToolUse hooks.
539
+ Codex and Cursor can be detected and repaired through local logs, MCP, shield, and guardrails. Universal hard-blocking needs a wrapper, bridge, or deeper pre-tool hook from those agents.
540
+
541
+ Privacy:
542
+ Bridge status does not upload raw prompts, source code, stdout, stderr, or full command logs.`,
523
543
  sync: `PrismoDev Sync
524
544
 
525
545
  Usage:
@@ -381,6 +381,7 @@ const {
381
381
  openUrl,
382
382
  repairExecutors,
383
383
  repairPlanner,
384
+ getUsageSummary,
384
385
  });
385
386
 
386
387
  const {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "getprismo",
3
- "version": "0.1.43",
3
+ "version": "0.1.44",
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",