getprismo 0.1.33 → 0.1.35

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
@@ -34,6 +34,7 @@ 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
 
@@ -43,6 +44,7 @@ agent-native npx getprismo mcp
43
44
  **receipt** explains what repeated, what output dominated, what artifacts leaked, what likely influenced the run, and a heuristic context-efficiency score.
44
45
  **replay** reconstructs why a session went sideways and prints a recovery prompt.
45
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.
46
48
  **mcp** exposes PrismoDev as local tools so compatible agents can scan, search shield output, and request scoped context directly.
47
49
 
48
50
  ---
@@ -270,6 +272,34 @@ this is intentionally not magic interception yet. it is a safe local-first primi
270
272
 
271
273
  ---
272
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
+
273
303
  ## new: live guardrails mode
274
304
 
275
305
  the easiest proactive mode is guard:
@@ -753,6 +783,7 @@ no install needed. npx runs it directly.
753
783
  | `optimize` | generate `.prismo/` context packs |
754
784
  | `context` | print paste-ready prompt for agents |
755
785
  | `shield` | run noisy commands while keeping full output out of chat |
786
+ | `agent` | claim and execute safe Prismo Cloud workspace actions locally |
756
787
  | `mcp` | expose PrismoDev tools over local MCP stdio |
757
788
  | `setup` | detect tools, logs, proxy readiness |
758
789
  | `usage` | show raw session token usage |
@@ -818,6 +849,17 @@ npx getprismo shield last
818
849
  npx getprismo shield search "auth failure"
819
850
  ```
820
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
+
821
863
  ### mcp mode
822
864
 
823
865
  ```bash
@@ -0,0 +1,478 @@
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
+ openUrl,
16
+ } = deps;
17
+
18
+ const DEFAULT_WORKSPACE_URL = "https://app.getprismo.dev/dashboard/dev";
19
+
20
+ const DEFAULT_API_URL = "https://api.getprismo.dev";
21
+ const TERMINAL_STATUSES = new Set(["completed", "failed", "cancelled"]);
22
+ const SAFE_SHIELD_COMMANDS = new Set(["npm", "pnpm", "yarn", "bun", "npx", "pytest", "python", "python3", "node"]);
23
+ const VALID_MODES = new Set(["observe", "suggest", "autopilot"]);
24
+
25
+ function apiBase(config) {
26
+ return String(config?.apiUrl || DEFAULT_API_URL).replace(/\/$/, "");
27
+ }
28
+
29
+ function requestJson(method, urlValue, token, payload, timeoutMs = 15000) {
30
+ return new Promise((resolve, reject) => {
31
+ let parsed;
32
+ try {
33
+ parsed = new URL(urlValue);
34
+ } catch {
35
+ reject(new Error(`Invalid URL: ${urlValue}`));
36
+ return;
37
+ }
38
+ const body = payload ? JSON.stringify(payload) : null;
39
+ const client = parsed.protocol === "https:" ? https : http;
40
+ const request = client.request({
41
+ method,
42
+ hostname: parsed.hostname,
43
+ port: parsed.port,
44
+ path: `${parsed.pathname}${parsed.search}`,
45
+ timeout: timeoutMs,
46
+ headers: {
47
+ "content-type": "application/json",
48
+ "user-agent": `prismodev-agent/${PACKAGE_VERSION}`,
49
+ ...(token ? { authorization: `Bearer ${token}` } : {}),
50
+ ...(body ? { "content-length": Buffer.byteLength(body) } : {}),
51
+ },
52
+ }, (response) => {
53
+ const chunks = [];
54
+ response.on("data", (chunk) => chunks.push(chunk));
55
+ response.on("end", () => {
56
+ const text = Buffer.concat(chunks).toString("utf8");
57
+ let data = null;
58
+ try {
59
+ data = text ? JSON.parse(text) : null;
60
+ } catch {
61
+ data = { text };
62
+ }
63
+ if (response.statusCode >= 200 && response.statusCode < 300) {
64
+ resolve({ statusCode: response.statusCode, data });
65
+ } else {
66
+ reject(new Error(`HTTP ${response.statusCode}: ${text || response.statusMessage}`));
67
+ }
68
+ });
69
+ });
70
+ request.on("timeout", () => {
71
+ request.destroy();
72
+ reject(new Error("Request timed out"));
73
+ });
74
+ request.on("error", reject);
75
+ if (body) request.write(body);
76
+ request.end();
77
+ });
78
+ }
79
+
80
+ function parseCommand(command) {
81
+ const parts = String(command || "").trim().split(/\s+/).filter(Boolean);
82
+ const getprismoIndex = parts.findIndex((part) => part === "getprismo" || part === "prismo" || part === "getprismo@latest");
83
+ const commandIndex = getprismoIndex >= 0 ? getprismoIndex + 1 : 0;
84
+ return {
85
+ raw: parts,
86
+ command: parts[commandIndex] || "",
87
+ args: parts.slice(commandIndex + 1),
88
+ };
89
+ }
90
+
91
+ function parseShieldArgs(args) {
92
+ const separatorIndex = args.indexOf("--");
93
+ const commandArgs = separatorIndex >= 0 ? args.slice(separatorIndex + 1) : args.slice(1);
94
+ if (!commandArgs.length) return null;
95
+ const binary = commandArgs[0];
96
+ if (!SAFE_SHIELD_COMMANDS.has(binary)) return null;
97
+ if (commandArgs.some((arg) => /[;&|`$<>]/.test(arg))) return null;
98
+ return commandArgs;
99
+ }
100
+
101
+ function repoRoot(rootDir, action) {
102
+ if (action.repo && path.basename(rootDir) !== action.repo) {
103
+ const sibling = path.join(path.dirname(rootDir), action.repo);
104
+ if (fs.existsSync(sibling) && fs.statSync(sibling).isDirectory()) return sibling;
105
+ }
106
+ return rootDir;
107
+ }
108
+
109
+ async function updateAction(config, actionId, payload, options = {}) {
110
+ const endpoint = options.endpoint || `${apiBase(config)}/v1/dev/workspace/actions/${actionId}`;
111
+ const response = await requestJson("PATCH", endpoint, config.token, payload, options.timeoutMs || 15000);
112
+ return response.data;
113
+ }
114
+
115
+ async function claimActions(config, options = {}) {
116
+ const limit = Number(options.limit || 5);
117
+ const endpoint = options.endpoint || `${apiBase(config)}/v1/dev/workspace/actions/claim?limit=${encodeURIComponent(limit)}`;
118
+ const response = await requestJson("POST", endpoint, config.token, null, options.timeoutMs || 15000);
119
+ return response.data?.actions || [];
120
+ }
121
+
122
+ async function sendHeartbeat(config, payload = {}, options = {}) {
123
+ const endpoint = options.heartbeatEndpoint || `${apiBase(config)}/v1/dev/workspace/heartbeat`;
124
+ const body = {
125
+ agent: `prismodev/${PACKAGE_VERSION}`,
126
+ mode: payload.mode || "autopilot",
127
+ status: payload.status || "online",
128
+ ...(payload.lastPollAt ? { lastPollAt: payload.lastPollAt } : {}),
129
+ };
130
+ const response = await requestJson("POST", endpoint, config.token, body, options.timeoutMs || 10000);
131
+ return response.data;
132
+ }
133
+
134
+ function runAutoDetect(rootDir, options = {}) {
135
+ const mode = options.mode || "autopilot";
136
+ const startedAt = new Date().toISOString();
137
+ const result = runDoctor(rootDir, { limit: 3, applySuggestions: mode === "autopilot", json: true, dryRun: mode === "observe" });
138
+ const score = result.after?.score ?? result.scan?.score ?? null;
139
+ const issues = result.scan?.issues || [];
140
+ const generatedFiles = result.generatedFiles || result.optimize?.generatedFiles || [];
141
+ const findings = [];
142
+
143
+ if (score !== null && score < 80) {
144
+ findings.push({ type: "low-score", score, message: `Context health score is ${score}/100.` });
145
+ }
146
+ for (const issue of issues.slice(0, 5)) {
147
+ findings.push({ type: "issue", severity: issue.severity, message: issue.message || issue.recommendation || issue.label });
148
+ }
149
+
150
+ return {
151
+ startedAt,
152
+ completedAt: new Date().toISOString(),
153
+ mode,
154
+ score,
155
+ findings,
156
+ generatedFiles,
157
+ applied: mode === "autopilot",
158
+ needsApproval: mode === "suggest" && findings.length > 0,
159
+ };
160
+ }
161
+
162
+ async function reportAutoDetect(config, detectResult, options = {}) {
163
+ const endpoint = options.detectEndpoint || `${apiBase(config)}/v1/dev/workspace/auto-detect`;
164
+ try {
165
+ await requestJson("POST", endpoint, config.token, detectResult, options.timeoutMs || 10000);
166
+ } catch (_) {}
167
+ }
168
+
169
+ function openWorkspace(config) {
170
+ const url = config?.workspaceUrl || DEFAULT_WORKSPACE_URL;
171
+ if (openUrl) {
172
+ openUrl(url);
173
+ }
174
+ return url;
175
+ }
176
+
177
+ async function executeAction(action, rootDir, options = {}) {
178
+ const root = repoRoot(path.resolve(rootDir || process.cwd()), action);
179
+ const parsed = parseCommand(action.command);
180
+ const startedAt = new Date().toISOString();
181
+
182
+ if (parsed.command === "doctor" || action.actionType === "doctor") {
183
+ const result = runDoctor(root, { limit: options.limit || 3, applySuggestions: true, json: true });
184
+ return {
185
+ status: "completed",
186
+ statusMessage: "Doctor completed and applied safe ignore/context fixes.",
187
+ result: {
188
+ command: "doctor",
189
+ startedAt,
190
+ completedAt: new Date().toISOString(),
191
+ score: result.after?.score ?? result.scan?.score ?? null,
192
+ generatedFiles: result.generatedFiles || result.optimize?.generatedFiles || [],
193
+ },
194
+ };
195
+ }
196
+
197
+ if (parsed.command === "sync" || action.actionType === "sync") {
198
+ const result = await runSync(root, { limit: options.limit || 20 });
199
+ return {
200
+ status: result.synced ? "completed" : "failed",
201
+ statusMessage: result.synced ? "Sync completed." : "Sync could not run because this machine is not connected.",
202
+ result: {
203
+ command: "sync",
204
+ startedAt,
205
+ completedAt: new Date().toISOString(),
206
+ synced: Boolean(result.synced),
207
+ aggregate: result.aggregate || null,
208
+ error: result.error || null,
209
+ },
210
+ };
211
+ }
212
+
213
+ if (parsed.command === "guard" || action.actionType === "guard") {
214
+ const result = await runGuard(root, {
215
+ tool: "all",
216
+ limit: options.limit || 5,
217
+ tokenBudget: options.tokenBudget || 600000,
218
+ noSync: false,
219
+ watch: false,
220
+ });
221
+ return {
222
+ status: "completed",
223
+ statusMessage: "Guard snapshot completed. Start agent watch mode for continuous protection.",
224
+ result: {
225
+ command: "guard",
226
+ startedAt,
227
+ completedAt: new Date().toISOString(),
228
+ guardRunning: Boolean(result.guardRunning),
229
+ events: result.events?.length || 0,
230
+ },
231
+ };
232
+ }
233
+
234
+ if (parsed.command === "context" || parsed.command === "optimize" || action.actionType === "context") {
235
+ const scope = parsed.args.find((arg) => !arg.startsWith("-")) || null;
236
+ const result = runOptimize(root, { scope });
237
+ return {
238
+ status: "completed",
239
+ statusMessage: "Context pack generated.",
240
+ result: {
241
+ command: "context",
242
+ startedAt,
243
+ completedAt: new Date().toISOString(),
244
+ scope,
245
+ generatedFiles: result.generatedFiles || [],
246
+ },
247
+ };
248
+ }
249
+
250
+ if (parsed.command === "shield" || action.actionType === "shield") {
251
+ const commandArgs = parseShieldArgs(parsed.args);
252
+ if (!commandArgs) {
253
+ return {
254
+ status: "failed",
255
+ statusMessage: "Shield action was rejected because the command is not on the safe allowlist.",
256
+ result: { command: "shield", rejected: true, reason: "unsafe-shield-command" },
257
+ };
258
+ }
259
+ const result = runShield(root, commandArgs);
260
+ return {
261
+ status: result.exitCode === 0 ? "completed" : "failed",
262
+ statusMessage: result.exitCode === 0 ? "Shielded command completed." : "Shielded command exited with an error.",
263
+ result: {
264
+ command: "shield",
265
+ startedAt,
266
+ completedAt: new Date().toISOString(),
267
+ exitCode: result.exitCode,
268
+ summary: result.summary || null,
269
+ runDir: result.runDir || null,
270
+ },
271
+ };
272
+ }
273
+
274
+ return {
275
+ status: "failed",
276
+ statusMessage: `Unsupported workspace action: ${action.actionType || parsed.command || "unknown"}`,
277
+ result: {
278
+ rejected: true,
279
+ reason: "unsupported-action",
280
+ actionType: action.actionType,
281
+ command: action.command,
282
+ },
283
+ };
284
+ }
285
+
286
+ async function runAgentOnce(rootDir = process.cwd(), options = {}) {
287
+ const config = loadConfig();
288
+ if (!config || !config.token) {
289
+ return {
290
+ schemaVersion: 1,
291
+ command: "agent",
292
+ connected: false,
293
+ mode: options.mode || "autopilot",
294
+ actionsClaimed: 0,
295
+ actionsCompleted: 0,
296
+ actionsFailed: 0,
297
+ actionsObserved: 0,
298
+ error: "not-connected",
299
+ next: [`${NPX_COMMAND} connect --token <token>`],
300
+ };
301
+ }
302
+
303
+ const mode = options.mode || "autopilot";
304
+ const pollTime = new Date().toISOString();
305
+
306
+ try {
307
+ await sendHeartbeat(config, { mode, status: "online", lastPollAt: pollTime }, options);
308
+ } catch (_) {}
309
+
310
+ let autoDetectResult = null;
311
+ if (options.autoDetect) {
312
+ autoDetectResult = runAutoDetect(rootDir, { mode });
313
+ await reportAutoDetect(config, autoDetectResult, options);
314
+ }
315
+
316
+ const actions = await claimActions(config, options);
317
+ const results = [];
318
+ for (const action of actions) {
319
+ if (TERMINAL_STATUSES.has(action.status)) continue;
320
+
321
+ if (mode === "observe") {
322
+ results.push({ id: action.id, label: action.label, status: "observed", statusMessage: "Agent is in observe mode. Action not executed." });
323
+ continue;
324
+ }
325
+
326
+ if (mode === "suggest") {
327
+ await updateAction(config, action.id, {
328
+ status: "pending_approval",
329
+ statusMessage: "Agent recommends this action. Waiting for approval in workspace.",
330
+ }, options);
331
+ results.push({ id: action.id, label: action.label, status: "pending_approval", statusMessage: "Suggested; awaiting approval." });
332
+ continue;
333
+ }
334
+
335
+ await updateAction(config, action.id, {
336
+ status: "running",
337
+ statusMessage: "Running locally through PrismoDev agent.",
338
+ }, options);
339
+ const result = await executeAction(action, rootDir, options);
340
+ await updateAction(config, action.id, result, options);
341
+ results.push({ id: action.id, label: action.label, ...result });
342
+ }
343
+
344
+ return {
345
+ schemaVersion: 1,
346
+ command: "agent",
347
+ connected: true,
348
+ mode,
349
+ apiUrl: apiBase(config),
350
+ actionsClaimed: actions.length,
351
+ actionsCompleted: results.filter((item) => item.status === "completed").length,
352
+ actionsFailed: results.filter((item) => item.status === "failed").length,
353
+ actionsObserved: results.filter((item) => item.status === "observed" || item.status === "pending_approval").length,
354
+ autoDetect: autoDetectResult,
355
+ results,
356
+ privacy: {
357
+ rawPrompts: false,
358
+ rawCode: false,
359
+ rawStdout: false,
360
+ rawStderr: false,
361
+ fileContents: false,
362
+ },
363
+ };
364
+ }
365
+
366
+ function renderAgentTerminal(result) {
367
+ const lines = [];
368
+ lines.push("");
369
+ lines.push("PrismoDev Agent");
370
+ lines.push("");
371
+ if (!result.connected) {
372
+ lines.push("Status: not connected");
373
+ lines.push(`Mode: ${result.mode || "autopilot"}`);
374
+ lines.push("");
375
+ lines.push("Next");
376
+ result.next.forEach((item, index) => lines.push(`${index + 1}. ${item}`));
377
+ return lines.join("\n");
378
+ }
379
+ lines.push("Status: connected");
380
+ lines.push(`Mode: ${result.mode}`);
381
+ lines.push(`API: ${result.apiUrl}`);
382
+ lines.push(`Actions claimed: ${result.actionsClaimed}`);
383
+ lines.push(`Completed: ${result.actionsCompleted}`);
384
+ lines.push(`Failed: ${result.actionsFailed}`);
385
+ if (result.actionsObserved > 0) {
386
+ lines.push(`Observed/Suggested: ${result.actionsObserved}`);
387
+ }
388
+ if (result.autoDetect) {
389
+ lines.push("");
390
+ lines.push("Auto-detect");
391
+ lines.push(` Score: ${result.autoDetect.score ?? "unknown"}/100`);
392
+ lines.push(` Findings: ${result.autoDetect.findings.length}`);
393
+ if (result.autoDetect.applied) lines.push(" Status: auto-fixed");
394
+ else if (result.autoDetect.needsApproval) lines.push(" Status: needs approval in workspace");
395
+ else lines.push(" Status: observed");
396
+ if (result.autoDetect.generatedFiles.length) {
397
+ lines.push(` Generated: ${result.autoDetect.generatedFiles.join(", ")}`);
398
+ }
399
+ result.autoDetect.findings.forEach((f) => {
400
+ lines.push(` - ${f.message}`);
401
+ });
402
+ }
403
+ if (result.results.length) {
404
+ lines.push("");
405
+ lines.push("Actions");
406
+ result.results.forEach((item) => {
407
+ lines.push(`- ${item.status}: ${item.label}`);
408
+ if (item.statusMessage) lines.push(` ${item.statusMessage}`);
409
+ });
410
+ } else if (!result.autoDetect) {
411
+ lines.push("");
412
+ lines.push("No queued workspace actions.");
413
+ }
414
+ return lines.join("\n");
415
+ }
416
+
417
+ async function runAgent(rootDir = process.cwd(), options = {}) {
418
+ if (!options.watch) return runAgentOnce(rootDir, options);
419
+
420
+ const intervalMs = Math.max(5, Number(options.interval || 15)) * 1000;
421
+ let running = true;
422
+ let sleepResolve = null;
423
+ let firstRun = true;
424
+
425
+ if (options.open) {
426
+ const config = loadConfig();
427
+ openWorkspace(config);
428
+ }
429
+
430
+ async function shutdown() {
431
+ if (!running) return;
432
+ running = false;
433
+ if (sleepResolve) sleepResolve();
434
+ try {
435
+ const config = loadConfig();
436
+ if (config?.token) {
437
+ await sendHeartbeat(config, { mode: options.mode || "autopilot", status: "offline" }, options);
438
+ }
439
+ } catch (_) {}
440
+ if (options.json) console.log(JSON.stringify({ event: "shutdown", status: "offline" }));
441
+ else console.log("\nAgent going offline.");
442
+ }
443
+
444
+ process.on("SIGINT", shutdown);
445
+ process.on("SIGTERM", shutdown);
446
+
447
+ while (running) {
448
+ const runOptions = { ...options, autoDetect: firstRun && options.autoDetect !== false };
449
+ firstRun = false;
450
+ const result = await runAgentOnce(rootDir, runOptions);
451
+ if (!running) break;
452
+ if (options.json) console.log(JSON.stringify(result, null, 2));
453
+ else console.log(renderAgentTerminal(result));
454
+ await new Promise((resolve) => {
455
+ sleepResolve = resolve;
456
+ setTimeout(resolve, intervalMs);
457
+ });
458
+ }
459
+
460
+ process.removeListener("SIGINT", shutdown);
461
+ process.removeListener("SIGTERM", shutdown);
462
+ }
463
+
464
+ return {
465
+ claimActions,
466
+ executeAction,
467
+ openWorkspace,
468
+ parseCommand,
469
+ renderAgentTerminal,
470
+ reportAutoDetect,
471
+ runAgent,
472
+ runAgentOnce,
473
+ runAutoDetect,
474
+ sendHeartbeat,
475
+ updateAction,
476
+ VALID_MODES,
477
+ };
478
+ };