getprismo 0.1.32 → 0.1.34

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
@@ -30,18 +30,21 @@ prismodev covers the full AI coding session:
30
30
 
31
31
  ```
32
32
  before you code npx getprismo doctor
33
- while you code npx getprismo watch
33
+ while you code npx getprismo guard --watch
34
34
  noisy commands npx getprismo shield -- npm test
35
35
  after you code npx getprismo receipt
36
36
  postmortem npx getprismo replay
37
+ workspace agent npx getprismo agent --watch
37
38
  agent-native npx getprismo mcp
38
39
  ```
39
40
 
40
41
  **doctor** diagnoses the repo, applies safe fixes, and shows the before/after score.
41
- **watch** monitors context pressure live and warns when things go wrong.
42
- **receipt** explains what repeated, what output dominated, what artifacts leaked, and what likely influenced the run.
42
+ **guard** runs live guardrails, context throttle, rescue prompts, context firewall, and dashboard-ready prevention events.
43
+ **watch** monitors context pressure live and is the lower-level diagnostic view behind guard.
44
+ **receipt** explains what repeated, what output dominated, what artifacts leaked, what likely influenced the run, and a heuristic context-efficiency score.
43
45
  **replay** reconstructs why a session went sideways and prints a recovery prompt.
44
46
  **shield** runs noisy commands without dumping full output back into the agent context.
47
+ **agent** connects Prismo Cloud to your local repo so dashboard actions can safely run on this machine.
45
48
  **mcp** exposes PrismoDev as local tools so compatible agents can scan, search shield output, and request scoped context directly.
46
49
 
47
50
  ---
@@ -269,9 +272,54 @@ this is intentionally not magic interception yet. it is a safe local-first primi
269
272
 
270
273
  ---
271
274
 
275
+ ## workspace agent
276
+
277
+ Prismo Cloud can guide the work from the dashboard, but your repo still lives on your machine. `agent` is the local bridge.
278
+
279
+ ```bash
280
+ npx getprismo connect --token <your Prismo API key>
281
+ npx getprismo agent --watch
282
+ ```
283
+
284
+ After that, the Prismo workspace can queue safe actions like `doctor`, `sync`, `guard`, `context`, `optimize`, and allowlisted `shield` commands. The local agent claims those actions, executes them in the selected repo, and reports the status back to Prismo Cloud.
285
+
286
+ This keeps the product flow simple:
287
+
288
+ ```text
289
+ dashboard recommends fix -> local agent runs safe command -> dashboard refreshes with the result
290
+ ```
291
+
292
+ `agent` does not upload prompts, source code, file contents, stdout, stderr, or full command logs. It uploads action status and safe aggregate metrics. Cloud actions are intentionally limited; arbitrary shell commands and shell metacharacters are rejected.
293
+
294
+ For CI-style polling or debugging, run one pass:
295
+
296
+ ```bash
297
+ npx getprismo agent --once
298
+ npx getprismo agent --once --json
299
+ ```
300
+
301
+ ---
302
+
272
303
  ## new: live guardrails mode
273
304
 
274
- the easiest proactive mode is:
305
+ the easiest proactive mode is guard:
306
+
307
+ ```bash
308
+ npx getprismo connect --token <your Prismo API key>
309
+ npx getprismo guard --watch
310
+ ```
311
+
312
+ `guard` packages the local prevention loop: live guardrails, context throttling, context firewall updates, guard event history, and dashboard-ready prevention events. it never uploads prompts, source code, file contents, stdout, stderr, or full command logs.
313
+
314
+ run it once for a snapshot:
315
+
316
+ ```bash
317
+ npx getprismo guard
318
+ npx getprismo guard --json
319
+ npx getprismo guard --no-sync
320
+ ```
321
+
322
+ the lower-level watch mode is:
275
323
 
276
324
  ```bash
277
325
  npx getprismo watch --auto
@@ -600,7 +648,7 @@ npx getprismo receipt
600
648
  npx getprismo receipt codex --json
601
649
  ```
602
650
 
603
- it summarizes repeated reads, generated artifacts, tool-output floods, repeated commands, likely influence, and the next scoped action to take.
651
+ it summarizes repeated reads, generated artifacts, tool-output floods, repeated commands, likely influence, and the next scoped action to take. it also reports a heuristic context-efficiency metric: decision/progress signals per 1k tokens, with drag factors such as repeated reads, artifact leaks, tool-output floods, and command loops.
604
652
 
605
653
  `replay` is the postmortem view:
606
654
 
@@ -623,9 +671,10 @@ it surfaces recurring waste patterns such as the same lockfile leaking into many
623
671
  ```bash
624
672
  npx getprismo instructions audit
625
673
  npx getprismo instructions ablate --dry-run
674
+ npx getprismo instructions apply --dry-run
626
675
  ```
627
676
 
628
- it scores rules in `CLAUDE.md`, `AGENTS.md`, `.codex/AGENTS.md`, `.codex/instructions.md`, and `.openai/instructions.md`, then separates observable violations, partial compliance, duplicated rules, trim candidates, and influence-unknown rules. `instructions ablate --dry-run` creates a conservative ablation plan with candidates, sample-count guidance, rollback notes, and variance warnings; it does not edit files.
677
+ it scores rules in `CLAUDE.md`, `AGENTS.md`, `.codex/AGENTS.md`, `.codex/instructions.md`, and `.openai/instructions.md`, then separates observable violations, partial compliance, duplicated rules, trim candidates, and influence-unknown rules. `instructions ablate --dry-run` creates a conservative ablation plan with candidates, sample-count guidance, rollback notes, and variance warnings; it does not edit files. `instructions apply` safely removes exact duplicate instruction lines only, writes backups first, and leaves uncertain rules as recommendations.
629
678
 
630
679
  `boundaries` checks parallel-agent isolation:
631
680
 
@@ -717,11 +766,12 @@ no install needed. npx runs it directly.
717
766
  | `cc` | claude code cost breakdown |
718
767
  | `cc timeline` | session reconstruction with events |
719
768
  | `cursor` | cursor session tracking and ai authorship |
720
- | `receipt` | run receipt for reads, repeats, output, artifacts, likely influence, and next-run scope |
769
+ | `receipt` | run receipt for reads, repeats, output, artifacts, context efficiency, likely influence, and next-run scope |
721
770
  | `replay` | incident replay with root cause and recovery prompt |
722
771
  | `timeline` | recurring context-waste patterns across recent sessions |
723
772
  | `instructions audit` | instruction ROI audit for CLAUDE.md / AGENTS.md violations, partial compliance, duplicates, and influence-unknown rules |
724
773
  | `instructions ablate --dry-run` | conservative ablation plan for instruction candidates without editing files |
774
+ | `instructions apply` | safely dedupe exact duplicate instruction lines with backups |
725
775
  | `boundaries` | multi-agent boundary check for shared files/artifacts and worktree overlap |
726
776
  | `scan --usage` | full repo scan with local usage data |
727
777
  | `scan --optimizer-fit` | recommend which token-optimization path fits your repo/session |
@@ -733,6 +783,7 @@ no install needed. npx runs it directly.
733
783
  | `optimize` | generate `.prismo/` context packs |
734
784
  | `context` | print paste-ready prompt for agents |
735
785
  | `shield` | run noisy commands while keeping full output out of chat |
786
+ | `agent` | claim and execute safe Prismo Cloud workspace actions locally |
736
787
  | `mcp` | expose PrismoDev tools over local MCP stdio |
737
788
  | `setup` | detect tools, logs, proxy readiness |
738
789
  | `usage` | show raw session token usage |
@@ -759,6 +810,14 @@ npx getprismo doctor --json # machine-readable output
759
810
 
760
811
  ## watch modes
761
812
 
813
+ ```bash
814
+ npx getprismo guard # proactive local guard snapshot
815
+ npx getprismo guard --watch # keep guardrails active and sync prevention events
816
+ npx getprismo guard --no-sync # keep all guard events local
817
+ npx getprismo guard --dry-run # preview guard actions without writing state
818
+ npx getprismo guard --json # dashboard-ready guard payload
819
+ ```
820
+
762
821
  ```bash
763
822
  npx getprismo watch # live refresh
764
823
  npx getprismo watch --once # single snapshot
@@ -790,6 +849,17 @@ npx getprismo shield last
790
849
  npx getprismo shield search "auth failure"
791
850
  ```
792
851
 
852
+ ### workspace agent mode
853
+
854
+ ```bash
855
+ npx getprismo agent # claim queued workspace actions once
856
+ npx getprismo agent --watch # keep polling Prismo Cloud for safe actions
857
+ npx getprismo agent --interval 15 # poll every 15 seconds
858
+ npx getprismo agent --limit 3 # claim up to 3 actions per poll
859
+ npx getprismo agent --json # machine-readable action result
860
+ npx getprismo agent /path/to/repo # run actions against a specific repo
861
+ ```
862
+
793
863
  ### mcp mode
794
864
 
795
865
  ```bash
@@ -0,0 +1,399 @@
1
+ module.exports = function createAgent(deps) {
2
+ const {
3
+ fs,
4
+ http,
5
+ https,
6
+ path,
7
+ NPX_COMMAND,
8
+ PACKAGE_VERSION,
9
+ loadConfig,
10
+ runDoctor,
11
+ runSync,
12
+ runGuard,
13
+ runShield,
14
+ runOptimize,
15
+ } = deps;
16
+
17
+ const DEFAULT_API_URL = "https://api.getprismo.dev";
18
+ const TERMINAL_STATUSES = new Set(["completed", "failed", "cancelled"]);
19
+ const SAFE_SHIELD_COMMANDS = new Set(["npm", "pnpm", "yarn", "bun", "npx", "pytest", "python", "python3", "node"]);
20
+ const VALID_MODES = new Set(["observe", "suggest", "autopilot"]);
21
+
22
+ function apiBase(config) {
23
+ return String(config?.apiUrl || DEFAULT_API_URL).replace(/\/$/, "");
24
+ }
25
+
26
+ function requestJson(method, urlValue, token, payload, timeoutMs = 15000) {
27
+ return new Promise((resolve, reject) => {
28
+ let parsed;
29
+ try {
30
+ parsed = new URL(urlValue);
31
+ } catch {
32
+ reject(new Error(`Invalid URL: ${urlValue}`));
33
+ return;
34
+ }
35
+ const body = payload ? JSON.stringify(payload) : null;
36
+ const client = parsed.protocol === "https:" ? https : http;
37
+ const request = client.request({
38
+ method,
39
+ hostname: parsed.hostname,
40
+ port: parsed.port,
41
+ path: `${parsed.pathname}${parsed.search}`,
42
+ timeout: timeoutMs,
43
+ headers: {
44
+ "content-type": "application/json",
45
+ "user-agent": `prismodev-agent/${PACKAGE_VERSION}`,
46
+ ...(token ? { authorization: `Bearer ${token}` } : {}),
47
+ ...(body ? { "content-length": Buffer.byteLength(body) } : {}),
48
+ },
49
+ }, (response) => {
50
+ const chunks = [];
51
+ response.on("data", (chunk) => chunks.push(chunk));
52
+ response.on("end", () => {
53
+ const text = Buffer.concat(chunks).toString("utf8");
54
+ let data = null;
55
+ try {
56
+ data = text ? JSON.parse(text) : null;
57
+ } catch {
58
+ data = { text };
59
+ }
60
+ if (response.statusCode >= 200 && response.statusCode < 300) {
61
+ resolve({ statusCode: response.statusCode, data });
62
+ } else {
63
+ reject(new Error(`HTTP ${response.statusCode}: ${text || response.statusMessage}`));
64
+ }
65
+ });
66
+ });
67
+ request.on("timeout", () => {
68
+ request.destroy();
69
+ reject(new Error("Request timed out"));
70
+ });
71
+ request.on("error", reject);
72
+ if (body) request.write(body);
73
+ request.end();
74
+ });
75
+ }
76
+
77
+ function parseCommand(command) {
78
+ const parts = String(command || "").trim().split(/\s+/).filter(Boolean);
79
+ const getprismoIndex = parts.findIndex((part) => part === "getprismo" || part === "prismo" || part === "getprismo@latest");
80
+ const commandIndex = getprismoIndex >= 0 ? getprismoIndex + 1 : 0;
81
+ return {
82
+ raw: parts,
83
+ command: parts[commandIndex] || "",
84
+ args: parts.slice(commandIndex + 1),
85
+ };
86
+ }
87
+
88
+ function parseShieldArgs(args) {
89
+ const separatorIndex = args.indexOf("--");
90
+ const commandArgs = separatorIndex >= 0 ? args.slice(separatorIndex + 1) : args.slice(1);
91
+ if (!commandArgs.length) return null;
92
+ const binary = commandArgs[0];
93
+ if (!SAFE_SHIELD_COMMANDS.has(binary)) return null;
94
+ if (commandArgs.some((arg) => /[;&|`$<>]/.test(arg))) return null;
95
+ return commandArgs;
96
+ }
97
+
98
+ function repoRoot(rootDir, action) {
99
+ if (action.repo && path.basename(rootDir) !== action.repo) {
100
+ const sibling = path.join(path.dirname(rootDir), action.repo);
101
+ if (fs.existsSync(sibling) && fs.statSync(sibling).isDirectory()) return sibling;
102
+ }
103
+ return rootDir;
104
+ }
105
+
106
+ async function updateAction(config, actionId, payload, options = {}) {
107
+ const endpoint = options.endpoint || `${apiBase(config)}/v1/dev/workspace/actions/${actionId}`;
108
+ const response = await requestJson("PATCH", endpoint, config.token, payload, options.timeoutMs || 15000);
109
+ return response.data;
110
+ }
111
+
112
+ async function claimActions(config, options = {}) {
113
+ const limit = Number(options.limit || 5);
114
+ const endpoint = options.endpoint || `${apiBase(config)}/v1/dev/workspace/actions/claim?limit=${encodeURIComponent(limit)}`;
115
+ const response = await requestJson("POST", endpoint, config.token, null, options.timeoutMs || 15000);
116
+ return response.data?.actions || [];
117
+ }
118
+
119
+ async function sendHeartbeat(config, payload = {}, options = {}) {
120
+ const endpoint = options.heartbeatEndpoint || `${apiBase(config)}/v1/dev/workspace/heartbeat`;
121
+ const body = {
122
+ agent: `prismodev/${PACKAGE_VERSION}`,
123
+ mode: payload.mode || "autopilot",
124
+ status: payload.status || "online",
125
+ ...(payload.lastPollAt ? { lastPollAt: payload.lastPollAt } : {}),
126
+ };
127
+ const response = await requestJson("POST", endpoint, config.token, body, options.timeoutMs || 10000);
128
+ return response.data;
129
+ }
130
+
131
+ async function executeAction(action, rootDir, options = {}) {
132
+ const root = repoRoot(path.resolve(rootDir || process.cwd()), action);
133
+ const parsed = parseCommand(action.command);
134
+ const startedAt = new Date().toISOString();
135
+
136
+ if (parsed.command === "doctor" || action.actionType === "doctor") {
137
+ const result = runDoctor(root, { limit: options.limit || 3, applySuggestions: true, json: true });
138
+ return {
139
+ status: "completed",
140
+ statusMessage: "Doctor completed and applied safe ignore/context fixes.",
141
+ result: {
142
+ command: "doctor",
143
+ startedAt,
144
+ completedAt: new Date().toISOString(),
145
+ score: result.after?.score ?? result.scan?.score ?? null,
146
+ generatedFiles: result.generatedFiles || result.optimize?.generatedFiles || [],
147
+ },
148
+ };
149
+ }
150
+
151
+ if (parsed.command === "sync" || action.actionType === "sync") {
152
+ const result = await runSync(root, { limit: options.limit || 20 });
153
+ return {
154
+ status: result.synced ? "completed" : "failed",
155
+ statusMessage: result.synced ? "Sync completed." : "Sync could not run because this machine is not connected.",
156
+ result: {
157
+ command: "sync",
158
+ startedAt,
159
+ completedAt: new Date().toISOString(),
160
+ synced: Boolean(result.synced),
161
+ aggregate: result.aggregate || null,
162
+ error: result.error || null,
163
+ },
164
+ };
165
+ }
166
+
167
+ if (parsed.command === "guard" || action.actionType === "guard") {
168
+ const result = await runGuard(root, {
169
+ tool: "all",
170
+ limit: options.limit || 5,
171
+ tokenBudget: options.tokenBudget || 600000,
172
+ noSync: false,
173
+ watch: false,
174
+ });
175
+ return {
176
+ status: "completed",
177
+ statusMessage: "Guard snapshot completed. Start agent watch mode for continuous protection.",
178
+ result: {
179
+ command: "guard",
180
+ startedAt,
181
+ completedAt: new Date().toISOString(),
182
+ guardRunning: Boolean(result.guardRunning),
183
+ events: result.events?.length || 0,
184
+ },
185
+ };
186
+ }
187
+
188
+ if (parsed.command === "context" || parsed.command === "optimize" || action.actionType === "context") {
189
+ const scope = parsed.args.find((arg) => !arg.startsWith("-")) || null;
190
+ const result = runOptimize(root, { scope });
191
+ return {
192
+ status: "completed",
193
+ statusMessage: "Context pack generated.",
194
+ result: {
195
+ command: "context",
196
+ startedAt,
197
+ completedAt: new Date().toISOString(),
198
+ scope,
199
+ generatedFiles: result.generatedFiles || [],
200
+ },
201
+ };
202
+ }
203
+
204
+ if (parsed.command === "shield" || action.actionType === "shield") {
205
+ const commandArgs = parseShieldArgs(parsed.args);
206
+ if (!commandArgs) {
207
+ return {
208
+ status: "failed",
209
+ statusMessage: "Shield action was rejected because the command is not on the safe allowlist.",
210
+ result: { command: "shield", rejected: true, reason: "unsafe-shield-command" },
211
+ };
212
+ }
213
+ const result = runShield(root, commandArgs);
214
+ return {
215
+ status: result.exitCode === 0 ? "completed" : "failed",
216
+ statusMessage: result.exitCode === 0 ? "Shielded command completed." : "Shielded command exited with an error.",
217
+ result: {
218
+ command: "shield",
219
+ startedAt,
220
+ completedAt: new Date().toISOString(),
221
+ exitCode: result.exitCode,
222
+ summary: result.summary || null,
223
+ runDir: result.runDir || null,
224
+ },
225
+ };
226
+ }
227
+
228
+ return {
229
+ status: "failed",
230
+ statusMessage: `Unsupported workspace action: ${action.actionType || parsed.command || "unknown"}`,
231
+ result: {
232
+ rejected: true,
233
+ reason: "unsupported-action",
234
+ actionType: action.actionType,
235
+ command: action.command,
236
+ },
237
+ };
238
+ }
239
+
240
+ async function runAgentOnce(rootDir = process.cwd(), options = {}) {
241
+ const config = loadConfig();
242
+ if (!config || !config.token) {
243
+ return {
244
+ schemaVersion: 1,
245
+ command: "agent",
246
+ connected: false,
247
+ mode: options.mode || "autopilot",
248
+ actionsClaimed: 0,
249
+ actionsCompleted: 0,
250
+ actionsFailed: 0,
251
+ actionsObserved: 0,
252
+ error: "not-connected",
253
+ next: [`${NPX_COMMAND} connect --token <token>`],
254
+ };
255
+ }
256
+
257
+ const mode = options.mode || "autopilot";
258
+ const pollTime = new Date().toISOString();
259
+
260
+ try {
261
+ await sendHeartbeat(config, { mode, status: "online", lastPollAt: pollTime }, options);
262
+ } catch (_) {}
263
+
264
+ const actions = await claimActions(config, options);
265
+ const results = [];
266
+ for (const action of actions) {
267
+ if (TERMINAL_STATUSES.has(action.status)) continue;
268
+
269
+ if (mode === "observe") {
270
+ results.push({ id: action.id, label: action.label, status: "observed", statusMessage: "Agent is in observe mode. Action not executed." });
271
+ continue;
272
+ }
273
+
274
+ if (mode === "suggest") {
275
+ await updateAction(config, action.id, {
276
+ status: "pending_approval",
277
+ statusMessage: "Agent recommends this action. Waiting for approval in workspace.",
278
+ }, options);
279
+ results.push({ id: action.id, label: action.label, status: "pending_approval", statusMessage: "Suggested; awaiting approval." });
280
+ continue;
281
+ }
282
+
283
+ await updateAction(config, action.id, {
284
+ status: "running",
285
+ statusMessage: "Running locally through PrismoDev agent.",
286
+ }, options);
287
+ const result = await executeAction(action, rootDir, options);
288
+ await updateAction(config, action.id, result, options);
289
+ results.push({ id: action.id, label: action.label, ...result });
290
+ }
291
+
292
+ return {
293
+ schemaVersion: 1,
294
+ command: "agent",
295
+ connected: true,
296
+ mode,
297
+ apiUrl: apiBase(config),
298
+ actionsClaimed: actions.length,
299
+ actionsCompleted: results.filter((item) => item.status === "completed").length,
300
+ actionsFailed: results.filter((item) => item.status === "failed").length,
301
+ actionsObserved: results.filter((item) => item.status === "observed" || item.status === "pending_approval").length,
302
+ results,
303
+ privacy: {
304
+ rawPrompts: false,
305
+ rawCode: false,
306
+ rawStdout: false,
307
+ rawStderr: false,
308
+ fileContents: false,
309
+ },
310
+ };
311
+ }
312
+
313
+ function renderAgentTerminal(result) {
314
+ const lines = [];
315
+ lines.push("");
316
+ lines.push("PrismoDev Agent");
317
+ lines.push("");
318
+ if (!result.connected) {
319
+ lines.push("Status: not connected");
320
+ lines.push(`Mode: ${result.mode || "autopilot"}`);
321
+ lines.push("");
322
+ lines.push("Next");
323
+ result.next.forEach((item, index) => lines.push(`${index + 1}. ${item}`));
324
+ return lines.join("\n");
325
+ }
326
+ lines.push("Status: connected");
327
+ lines.push(`Mode: ${result.mode}`);
328
+ lines.push(`API: ${result.apiUrl}`);
329
+ lines.push(`Actions claimed: ${result.actionsClaimed}`);
330
+ lines.push(`Completed: ${result.actionsCompleted}`);
331
+ lines.push(`Failed: ${result.actionsFailed}`);
332
+ if (result.actionsObserved > 0) {
333
+ lines.push(`Observed/Suggested: ${result.actionsObserved}`);
334
+ }
335
+ if (result.results.length) {
336
+ lines.push("");
337
+ lines.push("Actions");
338
+ result.results.forEach((item) => {
339
+ lines.push(`- ${item.status}: ${item.label}`);
340
+ if (item.statusMessage) lines.push(` ${item.statusMessage}`);
341
+ });
342
+ } else {
343
+ lines.push("");
344
+ lines.push("No queued workspace actions.");
345
+ }
346
+ return lines.join("\n");
347
+ }
348
+
349
+ async function runAgent(rootDir = process.cwd(), options = {}) {
350
+ if (!options.watch) return runAgentOnce(rootDir, options);
351
+
352
+ const intervalMs = Math.max(5, Number(options.interval || 15)) * 1000;
353
+ let running = true;
354
+ let sleepResolve = null;
355
+
356
+ async function shutdown() {
357
+ if (!running) return;
358
+ running = false;
359
+ if (sleepResolve) sleepResolve();
360
+ try {
361
+ const config = loadConfig();
362
+ if (config?.token) {
363
+ await sendHeartbeat(config, { mode: options.mode || "autopilot", status: "offline" }, options);
364
+ }
365
+ } catch (_) {}
366
+ if (options.json) console.log(JSON.stringify({ event: "shutdown", status: "offline" }));
367
+ else console.log("\nAgent going offline.");
368
+ }
369
+
370
+ process.on("SIGINT", shutdown);
371
+ process.on("SIGTERM", shutdown);
372
+
373
+ while (running) {
374
+ const result = await runAgentOnce(rootDir, options);
375
+ if (!running) break;
376
+ if (options.json) console.log(JSON.stringify(result, null, 2));
377
+ else console.log(renderAgentTerminal(result));
378
+ await new Promise((resolve) => {
379
+ sleepResolve = resolve;
380
+ setTimeout(resolve, intervalMs);
381
+ });
382
+ }
383
+
384
+ process.removeListener("SIGINT", shutdown);
385
+ process.removeListener("SIGTERM", shutdown);
386
+ }
387
+
388
+ return {
389
+ claimActions,
390
+ executeAction,
391
+ parseCommand,
392
+ renderAgentTerminal,
393
+ runAgent,
394
+ runAgentOnce,
395
+ sendHeartbeat,
396
+ updateAction,
397
+ VALID_MODES,
398
+ };
399
+ };