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 +25 -3
- package/lib/prismo-dev/agent.js +262 -0
- package/lib/prismo-dev/cli.js +66 -2
- package/lib/prismo-dev/cloud-sync.js +4 -1
- package/lib/prismo-dev/connector.js +3 -2
- package/lib/prismo-dev/enforce.js +28 -0
- package/lib/prismo-dev/help.js +21 -1
- package/lib/prismo-dev-scan.js +1 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
[](https://www.npmjs.com/package/getprismo)
|
|
5
5
|
[](LICENSE)
|
|
6
6
|
|
|
7
|
-
an
|
|
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
|
|
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
|
|
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
|
package/lib/prismo-dev/agent.js
CHANGED
|
@@ -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
|
};
|
package/lib/prismo-dev/cli.js
CHANGED
|
@@ -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
|
-
|
|
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`;
|
package/lib/prismo-dev/help.js
CHANGED
|
@@ -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
|
|
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:
|
package/lib/prismo-dev-scan.js
CHANGED
package/package.json
CHANGED