getprismo 0.1.36 → 0.1.38

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.
@@ -341,6 +341,24 @@ module.exports = function createAgent(deps) {
341
341
  results.push({ id: action.id, label: action.label, ...result });
342
342
  }
343
343
 
344
+ let syncResult = null;
345
+ if (options.syncTelemetry) {
346
+ try {
347
+ const result = await runSync(rootDir, { limit: options.syncLimit || 20 });
348
+ syncResult = {
349
+ synced: Boolean(result.synced),
350
+ sessions: Number(result.aggregate?.sessions || 0),
351
+ estimatedWastedTokens: Number(result.aggregate?.estimatedWastedTokens || 0),
352
+ wastePercent: Number(result.aggregate?.wastePercent || 0),
353
+ };
354
+ } catch (error) {
355
+ syncResult = {
356
+ synced: false,
357
+ error: error && error.message ? error.message : String(error),
358
+ };
359
+ }
360
+ }
361
+
344
362
  return {
345
363
  schemaVersion: 1,
346
364
  command: "agent",
@@ -352,6 +370,7 @@ module.exports = function createAgent(deps) {
352
370
  actionsFailed: results.filter((item) => item.status === "failed").length,
353
371
  actionsObserved: results.filter((item) => item.status === "observed" || item.status === "pending_approval").length,
354
372
  autoDetect: autoDetectResult,
373
+ sync: syncResult,
355
374
  results,
356
375
  privacy: {
357
376
  rawPrompts: false,
@@ -400,6 +419,16 @@ module.exports = function createAgent(deps) {
400
419
  lines.push(` - ${f.message}`);
401
420
  });
402
421
  }
422
+ if (result.sync) {
423
+ lines.push("");
424
+ lines.push("Sync");
425
+ if (result.sync.synced) {
426
+ lines.push(` Sessions: ${result.sync.sessions}`);
427
+ lines.push(` Likely wasted: ${result.sync.estimatedWastedTokens.toLocaleString()} (${result.sync.wastePercent}%)`);
428
+ } else {
429
+ lines.push(` Status: not synced${result.sync.error ? ` (${result.sync.error})` : ""}`);
430
+ }
431
+ }
403
432
  if (result.results.length) {
404
433
  lines.push("");
405
434
  lines.push("Actions");
@@ -418,9 +447,11 @@ module.exports = function createAgent(deps) {
418
447
  if (!options.watch) return runAgentOnce(rootDir, options);
419
448
 
420
449
  const intervalMs = Math.max(5, Number(options.interval || 15)) * 1000;
450
+ const syncIntervalMs = Math.max(30, Number(options.syncInterval || 60)) * 1000;
421
451
  let running = true;
422
452
  let sleepResolve = null;
423
453
  let firstRun = true;
454
+ let lastSyncAt = 0;
424
455
 
425
456
  if (options.open) {
426
457
  const config = loadConfig();
@@ -445,7 +476,14 @@ module.exports = function createAgent(deps) {
445
476
  process.on("SIGTERM", shutdown);
446
477
 
447
478
  while (running) {
448
- const runOptions = { ...options, autoDetect: firstRun && options.autoDetect !== false };
479
+ const now = Date.now();
480
+ const shouldSync = options.noSync !== true && (lastSyncAt === 0 || now - lastSyncAt >= syncIntervalMs);
481
+ if (shouldSync) lastSyncAt = now;
482
+ const runOptions = {
483
+ ...options,
484
+ autoDetect: firstRun && options.autoDetect !== false,
485
+ syncTelemetry: shouldSync,
486
+ };
449
487
  firstRun = false;
450
488
  const result = await runAgentOnce(rootDir, runOptions);
451
489
  if (!running) break;
@@ -3,7 +3,7 @@ const { printHelp, printCommandHelp } = require("./help");
3
3
 
4
4
  const VALID_COMMANDS = new Set([
5
5
  "dev", "init", "doctor", "firewall", "benchmark", "shield", "mcp",
6
- "connect", "sync", "status", "disconnect", "agent", "setup", "scan",
6
+ "connect", "sync", "status", "disconnect", "agent", "connector", "setup", "scan",
7
7
  "optimize", "context", "cc", "cursor", "receipt", "instructions",
8
8
  "timeline", "replay", "boundaries", "usage", "guard", "watch", "demo",
9
9
  ]);
@@ -70,6 +70,12 @@ function createCli(deps) {
70
70
  runGuard,
71
71
  renderAgentTerminal,
72
72
  runAgent,
73
+ renderConnectorTerminal,
74
+ runConnectorInstall,
75
+ runConnectorStart,
76
+ runConnectorStatus,
77
+ runConnectorStop,
78
+ runConnectorUninstall,
73
79
  renderInstructionsAblationTerminal,
74
80
  renderInstructionsApplyTerminal,
75
81
  renderInstructionsAuditTerminal,
@@ -131,7 +137,7 @@ function createCli(deps) {
131
137
  return;
132
138
  }
133
139
  if (!VALID_COMMANDS.has(command)) {
134
- throw new Error(`Unknown command: ${command}. Try: prismo connect, 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`);
140
+ 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`);
135
141
  }
136
142
 
137
143
  if (command === "demo") {
@@ -314,6 +320,15 @@ function createCli(deps) {
314
320
  const userIndex = rest.indexOf("--user");
315
321
  const deviceIndex = rest.indexOf("--device");
316
322
  const limitIndex = rest.indexOf("--limit");
323
+ const intervalIndex = rest.indexOf("--interval");
324
+ const syncIntervalIndex = rest.indexOf("--sync-interval");
325
+ const modeIndex = rest.indexOf("--mode");
326
+ const modeValue = modeIndex >= 0 ? rest[modeIndex + 1] : "autopilot";
327
+ if (!AGENT_VALID_MODES.has(modeValue)) {
328
+ throw new Error(`Invalid connector mode: ${modeValue}. Valid modes: observe, suggest, autopilot`);
329
+ }
330
+ const positional = getPositionals(rest, new Set(["--token", "--api-url", "--org", "--user", "--device", "--limit", "--interval", "--sync-interval", "--mode"]));
331
+ const target = positional[0] || process.cwd();
317
332
  const result = runConnect({
318
333
  token: tokenIndex >= 0 ? rest[tokenIndex + 1] : null,
319
334
  apiUrl: apiUrlIndex >= 0 ? rest[apiUrlIndex + 1] : null,
@@ -322,6 +337,17 @@ function createCli(deps) {
322
337
  device: deviceIndex >= 0 ? rest[deviceIndex + 1] : null,
323
338
  limit: parsePositiveInt(limitIndex >= 0 ? rest[limitIndex + 1] : null, 20),
324
339
  });
340
+ if (result.connected && !rest.includes("--no-agent") && runConnectorInstall) {
341
+ result.connector = runConnectorInstall(target, {
342
+ interval: parsePositiveInt(intervalIndex >= 0 ? rest[intervalIndex + 1] : null, 15),
343
+ syncInterval: parsePositiveInt(syncIntervalIndex >= 0 ? rest[syncIntervalIndex + 1] : null, 60),
344
+ mode: modeValue,
345
+ dryRun: rest.includes("--dry-run"),
346
+ });
347
+ result.next = result.connector?.started
348
+ ? [`${NPX_COMMAND} status`, "Refresh Prismo Workspace"]
349
+ : [`${NPX_COMMAND} connector install`, `${NPX_COMMAND} status`];
350
+ }
325
351
  if (result.connected && (rest.includes("--open") || !rest.includes("--no-open"))) {
326
352
  openUrl("https://getprismo.dev/dashboard/dev");
327
353
  }
@@ -330,6 +356,40 @@ function createCli(deps) {
330
356
  return;
331
357
  }
332
358
 
359
+ if (command === "connector") {
360
+ const json = rest.includes("--json");
361
+ const intervalIndex = rest.indexOf("--interval");
362
+ const syncIntervalIndex = rest.indexOf("--sync-interval");
363
+ const modeIndex = rest.indexOf("--mode");
364
+ const modeValue = modeIndex >= 0 ? rest[modeIndex + 1] : "autopilot";
365
+ if (!AGENT_VALID_MODES.has(modeValue)) {
366
+ throw new Error(`Invalid connector mode: ${modeValue}. Valid modes: observe, suggest, autopilot`);
367
+ }
368
+ const positional = getPositionals(rest, new Set(["--interval", "--sync-interval", "--mode"]));
369
+ const action = ["install", "start", "stop", "status", "uninstall"].includes(positional[0]) ? positional[0] : "status";
370
+ const target = ["install"].includes(action) ? positional[1] || process.cwd() : positional[0] || process.cwd();
371
+ let result;
372
+ if (action === "install") {
373
+ result = runConnectorInstall(target, {
374
+ interval: parsePositiveInt(intervalIndex >= 0 ? rest[intervalIndex + 1] : null, 15),
375
+ syncInterval: parsePositiveInt(syncIntervalIndex >= 0 ? rest[syncIntervalIndex + 1] : null, 60),
376
+ mode: modeValue,
377
+ dryRun: rest.includes("--dry-run"),
378
+ });
379
+ } else if (action === "start") {
380
+ result = runConnectorStart({ dryRun: rest.includes("--dry-run") });
381
+ } else if (action === "stop") {
382
+ result = runConnectorStop({ dryRun: rest.includes("--dry-run") });
383
+ } else if (action === "uninstall") {
384
+ result = runConnectorUninstall({ dryRun: rest.includes("--dry-run") });
385
+ } else {
386
+ result = runConnectorStatus();
387
+ }
388
+ if (json) console.log(JSON.stringify(result, null, 2));
389
+ else console.log(renderConnectorTerminal(result));
390
+ return;
391
+ }
392
+
333
393
  if (command === "sync") {
334
394
  const json = rest.includes("--json");
335
395
  const limitIndex = rest.indexOf("--limit");
@@ -363,6 +423,7 @@ function createCli(deps) {
363
423
  if (command === "agent") {
364
424
  const json = rest.includes("--json");
365
425
  const intervalIndex = rest.indexOf("--interval");
426
+ const syncIntervalIndex = rest.indexOf("--sync-interval");
366
427
  const limitIndex = rest.indexOf("--limit");
367
428
  const budgetIndex = rest.indexOf("--budget");
368
429
  const modeIndex = rest.indexOf("--mode");
@@ -370,7 +431,7 @@ function createCli(deps) {
370
431
  if (!AGENT_VALID_MODES.has(modeValue)) {
371
432
  throw new Error(`Invalid agent mode: ${modeValue}. Valid modes: observe, suggest, autopilot`);
372
433
  }
373
- const positional = getPositionals(rest, new Set(["--interval", "--limit", "--budget", "--mode"]));
434
+ const positional = getPositionals(rest, new Set(["--interval", "--sync-interval", "--limit", "--budget", "--mode"]));
374
435
  const target = positional[0] || process.cwd();
375
436
  const agentOptions = {
376
437
  json,
@@ -378,8 +439,11 @@ function createCli(deps) {
378
439
  watch: rest.includes("--watch") && !rest.includes("--once"),
379
440
  open: rest.includes("--open"),
380
441
  autoDetect: !rest.includes("--no-detect"),
442
+ noSync: rest.includes("--no-sync"),
381
443
  interval: parsePositiveInt(intervalIndex >= 0 ? rest[intervalIndex + 1] : null, 15),
444
+ syncInterval: parsePositiveInt(syncIntervalIndex >= 0 ? rest[syncIntervalIndex + 1] : null, 60),
382
445
  limit: parsePositiveInt(limitIndex >= 0 ? rest[limitIndex + 1] : null, 5),
446
+ syncLimit: parsePositiveInt(limitIndex >= 0 ? rest[limitIndex + 1] : null, 20),
383
447
  tokenBudget: parseTokenBudget(budgetIndex >= 0 ? rest[budgetIndex + 1] : null) || 600000,
384
448
  };
385
449
  const result = await runAgent(target, agentOptions);
@@ -411,6 +411,11 @@ module.exports = function createCloudSync(deps) {
411
411
  lines.push(`Config: ${result.configPath}`);
412
412
  lines.push(`API: ${result.apiUrl}`);
413
413
  lines.push(`Device: ${result.device.name}`);
414
+ if (result.connector) {
415
+ lines.push(`Connector: ${result.connector.started ? "started" : result.connector.installed ? "installed" : "not started"}`);
416
+ if (result.connector.reason) lines.push(`Connector note: ${result.connector.reason}`);
417
+ if (result.connector.error) lines.push(`Connector error: ${result.connector.error}`);
418
+ }
414
419
  lines.push("");
415
420
  lines.push("Next");
416
421
  result.next.forEach((item, index) => lines.push(`${index + 1}. ${item}`));
@@ -0,0 +1,306 @@
1
+ module.exports = function createConnector(deps) {
2
+ const {
3
+ fs,
4
+ os,
5
+ path,
6
+ spawnSync,
7
+ NPX_COMMAND,
8
+ } = deps;
9
+
10
+ const LABEL = "dev.getprismo.connector";
11
+ const BACKGROUND_COMMAND = String(NPX_COMMAND || "").includes(" -y ")
12
+ ? NPX_COMMAND
13
+ : "npx -y getprismo@latest";
14
+
15
+ function prismoHome() {
16
+ return process.env.PRISMO_HOME || path.join(os.homedir(), ".prismo");
17
+ }
18
+
19
+ function connectorDir() {
20
+ return path.join(prismoHome(), "connector");
21
+ }
22
+
23
+ function statePath() {
24
+ return path.join(connectorDir(), "state.json");
25
+ }
26
+
27
+ function scriptPath() {
28
+ return path.join(connectorDir(), "run.sh");
29
+ }
30
+
31
+ function logPath() {
32
+ return path.join(connectorDir(), "connector.log");
33
+ }
34
+
35
+ function errorLogPath() {
36
+ return path.join(connectorDir(), "connector.err.log");
37
+ }
38
+
39
+ function plistPath() {
40
+ return path.join(os.homedir(), "Library", "LaunchAgents", `${LABEL}.plist`);
41
+ }
42
+
43
+ function readJson(filePath) {
44
+ try {
45
+ if (!fs.existsSync(filePath)) return null;
46
+ return JSON.parse(fs.readFileSync(filePath, "utf8"));
47
+ } catch {
48
+ return null;
49
+ }
50
+ }
51
+
52
+ function writeJson(filePath, payload) {
53
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
54
+ fs.writeFileSync(filePath, `${JSON.stringify(payload, null, 2)}\n`, "utf8");
55
+ }
56
+
57
+ function shellEscape(value) {
58
+ return `'${String(value).replace(/'/g, "'\\''")}'`;
59
+ }
60
+
61
+ function xmlEscape(value) {
62
+ return String(value)
63
+ .replace(/&/g, "&")
64
+ .replace(/</g, "&lt;")
65
+ .replace(/>/g, "&gt;")
66
+ .replace(/"/g, "&quot;");
67
+ }
68
+
69
+ function launchctl(args, options = {}) {
70
+ if (options.dryRun) return { status: 0, stdout: "", stderr: "" };
71
+ const result = spawnSync("launchctl", args, { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] });
72
+ return {
73
+ status: result.status,
74
+ stdout: String(result.stdout || ""),
75
+ stderr: String(result.stderr || ""),
76
+ };
77
+ }
78
+
79
+ function writeRunner(rootDir, options = {}) {
80
+ const root = path.resolve(rootDir || process.cwd());
81
+ const interval = Math.max(5, Number(options.interval || 15));
82
+ const syncInterval = Math.max(30, Number(options.syncInterval || 60));
83
+ const mode = options.mode || "autopilot";
84
+ fs.mkdirSync(connectorDir(), { recursive: true });
85
+ const command = `${BACKGROUND_COMMAND} agent --watch --interval ${interval} --sync-interval ${syncInterval} --mode ${shellEscape(mode)} ${shellEscape(root)}`;
86
+ const contents = [
87
+ "#!/bin/sh",
88
+ "set -eu",
89
+ `cd ${shellEscape(root)}`,
90
+ `exec ${command}`,
91
+ "",
92
+ ].join("\n");
93
+ fs.writeFileSync(scriptPath(), contents, { encoding: "utf8", mode: 0o700 });
94
+ writeJson(statePath(), {
95
+ schemaVersion: 1,
96
+ installedAt: new Date().toISOString(),
97
+ root,
98
+ interval,
99
+ syncInterval,
100
+ mode,
101
+ command,
102
+ platform: process.platform,
103
+ runner: scriptPath(),
104
+ logPath: logPath(),
105
+ errorLogPath: errorLogPath(),
106
+ });
107
+ return { root, interval, syncInterval, mode, command };
108
+ }
109
+
110
+ function writePlist() {
111
+ const plist = `<?xml version="1.0" encoding="UTF-8"?>
112
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
113
+ <plist version="1.0">
114
+ <dict>
115
+ <key>Label</key>
116
+ <string>${LABEL}</string>
117
+ <key>ProgramArguments</key>
118
+ <array>
119
+ <string>/bin/sh</string>
120
+ <string>${xmlEscape(scriptPath())}</string>
121
+ </array>
122
+ <key>RunAtLoad</key>
123
+ <true/>
124
+ <key>KeepAlive</key>
125
+ <true/>
126
+ <key>StandardOutPath</key>
127
+ <string>${xmlEscape(logPath())}</string>
128
+ <key>StandardErrorPath</key>
129
+ <string>${xmlEscape(errorLogPath())}</string>
130
+ </dict>
131
+ </plist>
132
+ `;
133
+ fs.mkdirSync(path.dirname(plistPath()), { recursive: true });
134
+ fs.writeFileSync(plistPath(), plist, "utf8");
135
+ }
136
+
137
+ function isMacLaunchdAvailable() {
138
+ return process.platform === "darwin";
139
+ }
140
+
141
+ function runConnectorInstall(rootDir = process.cwd(), options = {}) {
142
+ const runner = writeRunner(rootDir, options);
143
+ if (!isMacLaunchdAvailable()) {
144
+ return {
145
+ schemaVersion: 1,
146
+ command: "connector",
147
+ action: "install",
148
+ installed: false,
149
+ started: false,
150
+ platform: process.platform,
151
+ statePath: statePath(),
152
+ runner: scriptPath(),
153
+ reason: "background-service-not-supported",
154
+ next: [`${NPX_COMMAND} agent --watch`],
155
+ };
156
+ }
157
+
158
+ writePlist();
159
+ launchctl(["bootout", `gui/${process.getuid()}`, plistPath()], options);
160
+ const bootstrap = launchctl(["bootstrap", `gui/${process.getuid()}`, plistPath()], options);
161
+ const kickstart = launchctl(["kickstart", "-k", `gui/${process.getuid()}/${LABEL}`], options);
162
+ const started = bootstrap.status === 0 || kickstart.status === 0 || options.dryRun;
163
+ return {
164
+ schemaVersion: 1,
165
+ command: "connector",
166
+ action: "install",
167
+ installed: true,
168
+ started,
169
+ label: LABEL,
170
+ root: runner.root,
171
+ mode: runner.mode,
172
+ interval: runner.interval,
173
+ syncInterval: runner.syncInterval,
174
+ plistPath: plistPath(),
175
+ statePath: statePath(),
176
+ runner: scriptPath(),
177
+ logPath: logPath(),
178
+ errorLogPath: errorLogPath(),
179
+ error: started ? null : (bootstrap.stderr || kickstart.stderr || "launchctl failed").trim(),
180
+ };
181
+ }
182
+
183
+ function runConnectorStart(options = {}) {
184
+ if (!isMacLaunchdAvailable()) {
185
+ return { schemaVersion: 1, command: "connector", action: "start", started: false, reason: "background-service-not-supported" };
186
+ }
187
+ if (!fs.existsSync(plistPath())) {
188
+ return { schemaVersion: 1, command: "connector", action: "start", started: false, reason: "not-installed", next: [`${NPX_COMMAND} connector install`] };
189
+ }
190
+ const result = launchctl(["kickstart", "-k", `gui/${process.getuid()}/${LABEL}`], options);
191
+ return {
192
+ schemaVersion: 1,
193
+ command: "connector",
194
+ action: "start",
195
+ started: result.status === 0 || options.dryRun,
196
+ label: LABEL,
197
+ error: result.status === 0 || options.dryRun ? null : result.stderr.trim(),
198
+ };
199
+ }
200
+
201
+ function runConnectorStop(options = {}) {
202
+ if (!isMacLaunchdAvailable()) {
203
+ return { schemaVersion: 1, command: "connector", action: "stop", stopped: false, reason: "background-service-not-supported" };
204
+ }
205
+ const result = launchctl(["bootout", `gui/${process.getuid()}`, plistPath()], options);
206
+ return {
207
+ schemaVersion: 1,
208
+ command: "connector",
209
+ action: "stop",
210
+ stopped: result.status === 0 || options.dryRun,
211
+ label: LABEL,
212
+ error: result.status === 0 || options.dryRun ? null : result.stderr.trim(),
213
+ };
214
+ }
215
+
216
+ function runConnectorUninstall(options = {}) {
217
+ const stop = runConnectorStop(options);
218
+ if (!options.dryRun) {
219
+ if (fs.existsSync(plistPath())) fs.rmSync(plistPath(), { force: true });
220
+ if (fs.existsSync(scriptPath())) fs.rmSync(scriptPath(), { force: true });
221
+ }
222
+ return {
223
+ schemaVersion: 1,
224
+ command: "connector",
225
+ action: "uninstall",
226
+ uninstalled: true,
227
+ stopped: stop.stopped,
228
+ removed: [plistPath(), scriptPath()],
229
+ };
230
+ }
231
+
232
+ function runConnectorStatus() {
233
+ const state = readJson(statePath());
234
+ const installed = fs.existsSync(plistPath()) || fs.existsSync(scriptPath());
235
+ let online = false;
236
+ let raw = "";
237
+ if (isMacLaunchdAvailable() && fs.existsSync(plistPath())) {
238
+ const result = launchctl(["print", `gui/${process.getuid()}/${LABEL}`]);
239
+ raw = `${result.stdout}${result.stderr}`;
240
+ online = result.status === 0 && /state = running|pid = \d+/i.test(raw);
241
+ }
242
+ return {
243
+ schemaVersion: 1,
244
+ command: "connector",
245
+ action: "status",
246
+ installed,
247
+ online,
248
+ label: LABEL,
249
+ platform: process.platform,
250
+ state,
251
+ plistPath: plistPath(),
252
+ runner: scriptPath(),
253
+ logPath: logPath(),
254
+ errorLogPath: errorLogPath(),
255
+ };
256
+ }
257
+
258
+ function renderConnectorTerminal(result) {
259
+ const lines = [];
260
+ lines.push("");
261
+ lines.push("PrismoDev Connector");
262
+ lines.push("");
263
+ if (result.action === "status") {
264
+ lines.push(`Installed: ${result.installed ? "yes" : "no"}`);
265
+ lines.push(`Status: ${result.online ? "online" : "idle"}`);
266
+ if (result.state?.root) lines.push(`Repo: ${result.state.root}`);
267
+ if (result.state?.interval) lines.push(`Poll: every ${result.state.interval}s`);
268
+ if (result.state?.syncInterval) lines.push(`Sync: every ${result.state.syncInterval}s`);
269
+ lines.push(`Logs: ${result.logPath}`);
270
+ } else if (result.action === "install") {
271
+ lines.push(`Status: ${result.started ? "started" : result.installed ? "installed" : "not installed"}`);
272
+ if (result.root) lines.push(`Repo: ${result.root}`);
273
+ if (result.interval) lines.push(`Poll: every ${result.interval}s`);
274
+ if (result.syncInterval) lines.push(`Sync: every ${result.syncInterval}s`);
275
+ if (result.reason) lines.push(`Note: ${result.reason}`);
276
+ if (result.error) lines.push(`Error: ${result.error}`);
277
+ } else if (result.action === "start") {
278
+ lines.push(`Status: ${result.started ? "started" : "not started"}`);
279
+ if (result.reason) lines.push(`Note: ${result.reason}`);
280
+ if (result.error) lines.push(`Error: ${result.error}`);
281
+ } else if (result.action === "stop") {
282
+ lines.push(`Status: ${result.stopped ? "stopped" : "not stopped"}`);
283
+ if (result.reason) lines.push(`Note: ${result.reason}`);
284
+ if (result.error) lines.push(`Error: ${result.error}`);
285
+ } else if (result.action === "uninstall") {
286
+ lines.push("Status: uninstalled");
287
+ }
288
+ if (result.next?.length) {
289
+ lines.push("");
290
+ lines.push("Next");
291
+ result.next.forEach((item, index) => lines.push(`${index + 1}. ${item}`));
292
+ }
293
+ return lines.join("\n");
294
+ }
295
+
296
+ return {
297
+ LABEL,
298
+ renderConnectorTerminal,
299
+ runConnectorInstall,
300
+ runConnectorStart,
301
+ runConnectorStatus,
302
+ runConnectorStop,
303
+ runConnectorUninstall,
304
+ writeRunner,
305
+ };
306
+ };
@@ -13,10 +13,11 @@ Usage:
13
13
  prismo mcp [path]
14
14
  prismo mcp doctor [--json] [path]
15
15
  prismo connect [--json] [--token TOKEN] [--api-url URL] [--org ORG] [--user USER] [--device NAME]
16
+ prismo connector [status|install|start|stop|uninstall] [--json] [--interval N] [--sync-interval N] [--mode observe|suggest|autopilot] [path]
16
17
  prismo sync [--json] [--dry-run] [--watch] [--interval N] [--limit N] [--tool all|codex|claude|cursor] [path]
17
18
  prismo status [--json]
18
19
  prismo disconnect [--json]
19
- prismo agent [--json] [--once] [--watch] [--interval N] [--limit N] [--mode MODE] [path]
20
+ prismo agent [--json] [--once] [--watch] [--interval N] [--sync-interval N] [--limit N] [--mode MODE] [path]
20
21
  prismo setup [--json] [--proxy-url URL] [path]
21
22
  prismo scan [--fix] [--ci] [--json] [--usage] [--optimizer-fit] [--report-card] [--simple] [--no-report] [path]
22
23
  prismo optimize [scope] [--json] [path]
@@ -44,6 +45,7 @@ Commands:
44
45
  shield Run a noisy command, store full output locally, and return a compact summary.
45
46
  mcp Start a local MCP server exposing Prismo tools over stdio.
46
47
  connect Store a PrismoDev cloud connection for seamless dashboard sync.
48
+ connector Install or manage the background Prismo Workspace connector.
47
49
  sync Send safe aggregate local agent telemetry to Prismo; use --watch for background-style sync.
48
50
  status Show local PrismoDev connection and last sync state.
49
51
  disconnect Remove the local PrismoDev cloud connection.
@@ -77,6 +79,7 @@ Options:
77
79
  --firewall Generate cc timeline-derived firewall suggestion files.
78
80
  --task TASK Name the task for timeline-derived firewall suggestions.
79
81
  --interval N Refresh interval in seconds for watch mode.
82
+ --sync-interval N Telemetry sync interval in seconds for the background connector.
80
83
  --dry-run Preview doctor/fix actions without writing files.
81
84
  --apply-ignores-only Only create/suggest AI ignore files in doctor mode.
82
85
  --apply-suggestions Append missing recommended ignore rules with backups.
@@ -98,6 +101,7 @@ Options:
98
101
  --org ORG Organization identifier for connect mode.
99
102
  --user USER User email or identifier for connect mode.
100
103
  --device NAME Local device name for connect mode.
104
+ --no-agent Connect without installing the background workspace connector.
101
105
  --tool TOOL Tool filter for sync mode: all, codex, claude, or cursor.
102
106
  --watch Keep sync running and send updates on an interval.
103
107
  --once Run watch mode once, useful for tests and scripts.
@@ -406,6 +410,7 @@ Output:
406
410
 
407
411
  Usage:
408
412
  prismo connect [--json] [--token TOKEN] [--api-url URL] [--org ORG] [--user USER] [--device NAME]
413
+ prismo connector [status|install|start|stop|uninstall] [--json] [--interval N] [--sync-interval N] [--mode observe|suggest|autopilot] [path]
409
414
  prismo sync [--json] [--dry-run] [--watch] [--interval N] [--limit N] [--tool all|codex|claude|cursor] [path]
410
415
  prismo status [--json]
411
416
  prismo disconnect [--json]
@@ -413,6 +418,10 @@ Usage:
413
418
  Examples:
414
419
  prismo connect --token <token>
415
420
  prismo connect --token <token> --api-url https://api.getprismo.dev
421
+ prismo connect --token <token> --no-agent
422
+ prismo connector status
423
+ prismo connector install
424
+ prismo connector install --sync-interval 120
416
425
  prismo sync --dry-run
417
426
  prismo sync
418
427
  prismo sync --watch --interval 60
@@ -420,10 +429,38 @@ Examples:
420
429
  prismo disconnect
421
430
 
422
431
  Output:
423
- connect stores a local PrismoDev device connection in ~/.prismo/config.json.
432
+ connect stores a local PrismoDev device connection in ~/.prismo/config.json and starts the background workspace connector by default.
433
+ connector keeps Prismo Workspace online so repairs queued in the dashboard run locally without copy/paste commands, and continuously syncs aggregate telemetry on a controlled interval.
424
434
  sync reads local Codex, Claude Code, and Cursor session logs, builds aggregate agent-efficiency telemetry, and sends it to Prismo.
425
435
  sync --watch keeps running so a local service manager can keep the Waste Scanner dashboard fresh.
426
436
  Sync does not upload prompts, source code, file contents, stdout, stderr, or full command logs.`,
437
+ connector: `PrismoDev Connector
438
+
439
+ Usage:
440
+ prismo connector status [--json]
441
+ prismo connector install [--json] [--interval N] [--sync-interval N] [--mode observe|suggest|autopilot] [path]
442
+ prismo connector start [--json]
443
+ prismo connector stop [--json]
444
+ prismo connector uninstall [--json]
445
+
446
+ Modes:
447
+ observe Keep the workspace connected but do not execute queued repairs.
448
+ suggest Stage repairs as pending approval.
449
+ autopilot Execute safe queued repairs automatically. (default)
450
+
451
+ Examples:
452
+ prismo connector status
453
+ prismo connector install
454
+ prismo connector install --mode suggest --interval 30
455
+ prismo connector install --sync-interval 120
456
+ prismo connector stop
457
+ prismo connector uninstall
458
+
459
+ Output:
460
+ Installs and manages the local Prismo Workspace connector.
461
+ On macOS this creates a LaunchAgent so Prismo stays online after the terminal closes.
462
+ The connector claims safe repairs queued from Prismo Cloud, runs them locally, continuously syncs aggregate telemetry, and reports status back.
463
+ It does not upload prompts, source code, file contents, stdout, stderr, or full command logs.`,
427
464
  sync: `PrismoDev Sync
428
465
 
429
466
  Usage:
@@ -455,7 +492,7 @@ Output:
455
492
  agent: `PrismoDev Agent
456
493
 
457
494
  Usage:
458
- prismo agent [--json] [--once] [--watch] [--open] [--no-detect] [--interval N] [--limit N] [--mode MODE] [path]
495
+ prismo agent [--json] [--once] [--watch] [--open] [--no-detect] [--no-sync] [--interval N] [--sync-interval N] [--limit N] [--mode MODE] [path]
459
496
 
460
497
  Modes:
461
498
  observe Watch and report actions without executing. No changes made.
@@ -465,6 +502,7 @@ Modes:
465
502
  Examples:
466
503
  prismo agent --once
467
504
  prismo agent --watch --interval 15
505
+ prismo agent --watch --sync-interval 120
468
506
  prismo agent --watch --mode observe
469
507
  prismo agent --watch --mode suggest
470
508
  prismo agent --watch --open
@@ -473,12 +511,13 @@ Examples:
473
511
  Options:
474
512
  --open Open the Prismo workspace in the browser on start.
475
513
  --no-detect Skip the initial auto-detect scan on first poll.
514
+ --no-sync Keep watch mode from continuously syncing aggregate telemetry.
476
515
 
477
516
  Output:
478
517
  On first poll, the agent proactively runs doctor to detect context issues.
479
518
  In autopilot mode it auto-fixes safe issues. In suggest mode it reports them as pending_approval in the workspace.
480
519
  Claims safe workspace actions queued from Prismo Cloud, runs them locally, and reports status back.
481
- Sends heartbeat to Prismo Cloud on each poll so the dashboard shows agent online/offline status.
520
+ Sends heartbeat to Prismo Cloud on each poll and syncs aggregate telemetry on a controlled interval so the dashboard stays fresh.
482
521
  Handles SIGINT/SIGTERM gracefully and marks the agent offline before exiting.
483
522
  Supported actions are doctor, sync, guard, context/optimize, and shield with a conservative command allowlist.
484
523
  Agent does not upload prompts, source code, file contents, stdout, stderr, or full command logs.`,
@@ -3,7 +3,7 @@ const http = require("http");
3
3
  const https = require("https");
4
4
  const os = require("os");
5
5
  const path = require("path");
6
- const { execSync } = require("child_process");
6
+ const { execSync, spawnSync } = require("child_process");
7
7
  const { version: PACKAGE_VERSION } = require("../package.json");
8
8
 
9
9
  function openUrl(url) {
@@ -333,6 +333,21 @@ const {
333
333
  openUrl,
334
334
  });
335
335
 
336
+ const {
337
+ renderConnectorTerminal,
338
+ runConnectorInstall,
339
+ runConnectorStart,
340
+ runConnectorStatus,
341
+ runConnectorStop,
342
+ runConnectorUninstall,
343
+ } = require("./prismo-dev/connector")({
344
+ fs,
345
+ os,
346
+ path,
347
+ spawnSync,
348
+ NPX_COMMAND,
349
+ });
350
+
336
351
  const {
337
352
  buildInstructionsAblationPlan,
338
353
  buildInstructionsApply,
@@ -439,6 +454,12 @@ const { runCli } = require("./prismo-dev/cli")({
439
454
  runGuard,
440
455
  renderAgentTerminal,
441
456
  runAgent,
457
+ renderConnectorTerminal,
458
+ runConnectorInstall,
459
+ runConnectorStart,
460
+ runConnectorStatus,
461
+ runConnectorStop,
462
+ runConnectorUninstall,
442
463
  renderInstructionsAblationTerminal,
443
464
  renderInstructionsApplyTerminal,
444
465
  renderInstructionsAuditTerminal,
@@ -531,6 +552,11 @@ module.exports = {
531
552
  runConnect,
532
553
  runDisconnect,
533
554
  runGuard,
555
+ runConnectorInstall,
556
+ runConnectorStart,
557
+ runConnectorStatus,
558
+ runConnectorStop,
559
+ runConnectorUninstall,
534
560
  runStatus,
535
561
  runSync,
536
562
  getUsageSummary,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "getprismo",
3
- "version": "0.1.36",
3
+ "version": "0.1.38",
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",